Compare commits
No commits in common. "v1" and "master" have entirely different histories.
3
.env
@ -1,3 +0,0 @@
|
|||||||
APP_NAME=portfolio
|
|
||||||
APP_LANG=en_US
|
|
||||||
APP_URL=https://refansa.vercel.app
|
|
@ -1,2 +0,0 @@
|
|||||||
APP_NAME=
|
|
||||||
APP_LANG=
|
|
@ -1,7 +1,84 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "prettier"],
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es2020": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2022,
|
||||||
|
"requireConfigFile": "false",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": true,
|
||||||
|
"node": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:github/recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/no-unescaped-entities": "off",
|
"import/no-extraneous-dependencies": [
|
||||||
"@next/next/no-page-custom-font": "off"
|
"error",
|
||||||
}
|
{
|
||||||
|
"packageDir": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
"import/extensions": "off",
|
||||||
|
"no-console": "off",
|
||||||
|
"github/array-foreach": "off",
|
||||||
|
"camelcase": "off",
|
||||||
|
"i18n-text/no-en": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
|
"prefer-template": "off",
|
||||||
|
"filenames/match-regex": "off",
|
||||||
|
"no-constant-condition": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"github/no-then": "off",
|
||||||
|
"import/no-named-as-default-member": "off",
|
||||||
|
"one-var": "off",
|
||||||
|
"import/no-namespace": "off",
|
||||||
|
"import/no-anonymous-default-export": "off",
|
||||||
|
"object-shorthand": "off",
|
||||||
|
"eslint-comments/no-use": "off",
|
||||||
|
"no-empty": "off",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"import/no-named-as-default": "off",
|
||||||
|
"eslint-comments/no-unused-disable": "off",
|
||||||
|
"no-useless-concat": "off",
|
||||||
|
"func-style": "off",
|
||||||
|
"eslint-comments/no-unlimited-disable": "off",
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/*.tsx", "**/*.ts"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "primer-react", "jsx-a11y"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:primer-react/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"camelcase": "off",
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"jsx-a11y/no-onchange": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["tmp/*", "!/.*", "/.next/"]
|
||||||
}
|
}
|
||||||
|
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
m.refansa.am@gmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
2
.gitignore
vendored
@ -4,6 +4,7 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@ -23,7 +24,6 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
|||||||
|
pnpm lint
|
@ -1,53 +0,0 @@
|
|||||||
# Using this project's .gitignore file as base.
|
|
||||||
#
|
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next
|
|
||||||
/out
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# configs
|
|
||||||
package.json
|
|
||||||
package-lock.json
|
|
||||||
next.config.js
|
|
||||||
pnpm-lock.yaml
|
|
||||||
postcss.config.js
|
|
||||||
tsconfig.json
|
|
||||||
yarn.lock
|
|
||||||
.eslintrc.json
|
|
||||||
.prettierrc.json
|
|
||||||
|
|
||||||
# public
|
|
||||||
/public
|
|
||||||
README.md
|
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 80,
|
"overrides": [
|
||||||
"tabWidth": 2,
|
{
|
||||||
"semi": false,
|
"files": [
|
||||||
"singleQuote": true,
|
"**/*.{ts,tsx,js,mjs}"
|
||||||
"quoteProps": "as-needed",
|
],
|
||||||
"jsxSingleQuote": true,
|
"options": {
|
||||||
"trailingComma": "all",
|
"printWidth": 100,
|
||||||
"bracketSpacing": true,
|
"semi": false,
|
||||||
"bracketSameLine": true,
|
"singleQuote": true
|
||||||
"arrowParens": "always",
|
}
|
||||||
"endOfLine": "lf",
|
}
|
||||||
"singleAttributePerLine": true
|
]
|
||||||
}
|
}
|
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"discord.enabled": false
|
|
||||||
}
|
|
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Refansa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
20
README.md
@ -1,5 +1,19 @@
|
|||||||
# Mantine Next Template
|
> [!NOTE]
|
||||||
|
> Mirrorred from gitea.tumbleweed.my.id/refansa/refansa.my.id
|
||||||
|
|
||||||
Get started with the template by clicking `Use this template` button on the top of the page.
|
# Site - [refansa.my.id](https://refansa.my.id)
|
||||||
|
|
||||||
[Documentation](https://mantine.dev/guides/next/)
|
The source code of my frontend website, [refansa.my.id](https://refansa.my.id)
|
||||||
|
|
||||||
|
Built with [Next.JS](https://nextjs.org), [shadcn/ui](https://ui.shadcn.com), and [tailwindcss](https://tailwindcss.com)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
After you cloned this repo you could easily run by;
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:3000 with your browser to see the result.
|
||||||
|
17
components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
swcMinify: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
4
next.config.mjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
export default nextConfig
|
71
package.json
@ -1,40 +1,63 @@
|
|||||||
{
|
{
|
||||||
"name": "portfolio",
|
"name": "refansa.my.id",
|
||||||
"version": "0.1.3",
|
"version": "0.0.1c",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"homepage": "https://refansa.my.id",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Refansa",
|
"name": "Muhammad Refansa Ali Muzky",
|
||||||
|
"nickname": "Refansa",
|
||||||
"email": "m.refansa.am@gmail.com",
|
"email": "m.refansa.am@gmail.com",
|
||||||
"url": "https://github.com/Refansa"
|
"url": "https://github.com/refansa"
|
||||||
|
},
|
||||||
|
"description": "A humble internet abode.",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/refansa/refansa.my.id"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"prettier": "prettier . \"./**/*.{js,jsx,ts,tsx}\" --write"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "7.1.3",
|
"@icons-pack/react-simple-icons": "^9.6.0",
|
||||||
"@mantine/hooks": "7.1.3",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@tabler/icons-react": "^2.39.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"framer-motion": "^10.16.4",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"next": "13.4.4",
|
"@react-spring/web": "^9.7.4",
|
||||||
"react": "18.2.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"react-dom": "18.2.0",
|
"clsx": "^2.1.1",
|
||||||
"react-intersection-observer": "^9.5.2"
|
"gray-matter": "^4.0.3",
|
||||||
|
"lucide-react": "^0.408.0",
|
||||||
|
"next": "14.2.5",
|
||||||
|
"next-mdx-remote": "^5.0.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-toggle-dark-mode": "^1.1.1",
|
||||||
|
"slug": "^9.1.0",
|
||||||
|
"tailwind-merge": "^2.4.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.2.5",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/react": "18.2.7",
|
"@types/node": "^20",
|
||||||
"@types/react-dom": "18.2.4",
|
"@types/react": "^18",
|
||||||
"eslint": "8.41.0",
|
"@types/react-dom": "^18",
|
||||||
"eslint-config-next": "13.4.4",
|
"@types/slug": "^5.0.8",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint": "^8",
|
||||||
"postcss": "^8.4.31",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss-preset-mantine": "1.8.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"prettier": "3.0.3",
|
"eslint-plugin-github": "^5.0.1",
|
||||||
"typescript": "5.0.4"
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||||
|
"eslint-plugin-primer-react": "^5.3.0",
|
||||||
|
"husky": "^9.1.3",
|
||||||
|
"postcss": "^8",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
11635
pnpm-lock.yaml
@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
"postcss-preset-mantine": {},
|
|
||||||
"postcss-simple-vars": {
|
|
||||||
variables: {
|
|
||||||
"mantine-breakpoint-xs": "36em",
|
|
||||||
"mantine-breakpoint-sm": "48em",
|
|
||||||
"mantine-breakpoint-md": "62em",
|
|
||||||
"mantine-breakpoint-lg": "75em",
|
|
||||||
"mantine-breakpoint-xl": "88em",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 710 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 91 KiB |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>
|
|
Before Width: | Height: | Size: 937 B |
@ -1,4 +0,0 @@
|
|||||||
<svg id="download" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
|
|
||||||
<path id="Path_3" data-name="Path 3" d="M96,95h4v1H96v4H95V96H86v4H85V96H76v4H75V96H66v4H65V96H56v4H55V96H46v4H45V96H36v4H35V96H26v4H25V96H16v4H15V96H0V95H15V86H0V85H15V76H0V75H15V66H0V65H15V56H0V55H15V46H0V45H15V36H0V35H15V26H0V25H15V16H0V15H15V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96Zm-1,0V86H86v9ZM85,95V86H76v9ZM75,95V86H66v9ZM65,95V86H56v9ZM55,95V86H46v9ZM45,95V86H36v9ZM35,95V86H26v9ZM25,95V86H16v9ZM16,85h9V76H16Zm10,0h9V76H26Zm10,0h9V76H36Zm10,0h9V76H46Zm10,0h9V76H56Zm10,0h9V76H66Zm10,0h9V76H76Zm10,0h9V76H86Zm9-10V66H86v9ZM85,75V66H76v9ZM75,75V66H66v9ZM65,75V66H56v9ZM55,75V66H46v9ZM45,75V66H36v9ZM35,75V66H26v9ZM25,75V66H16v9ZM16,65h9V56H16Zm10,0h9V56H26Zm10,0h9V56H36Zm10,0h9V56H46Zm10,0h9V56H56Zm10,0h9V56H66Zm10,0h9V56H76Zm10,0h9V56H86Zm9-10V46H86v9ZM85,55V46H76v9ZM75,55V46H66v9ZM65,55V46H56v9ZM55,55V46H46v9ZM45,55V46H36v9ZM35,55V46H26v9ZM25,55V46H16v9ZM16,45h9V36H16Zm10,0h9V36H26Zm10,0h9V36H36Zm10,0h9V36H46Zm10,0h9V36H56Zm10,0h9V36H66Zm10,0h9V36H76Zm10,0h9V36H86Zm9-10V26H86v9ZM85,35V26H76v9ZM75,35V26H66v9ZM65,35V26H56v9ZM55,35V26H46v9ZM45,35V26H36v9ZM35,35V26H26v9ZM25,35V26H16v9ZM16,25h9V16H16Zm10,0h9V16H26Zm10,0h9V16H36Zm10,0h9V16H46Zm10,0h9V16H56Zm10,0h9V16H66Zm10,0h9V16H76Zm10,0h9V16H86Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd" opacity="0.5"/>
|
|
||||||
<path id="Path_4" data-name="Path 4" d="M6,5V0H5V5H0V6H5v94H6V6h94V5Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
After Width: | Height: | Size: 629 B |
@ -1,6 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server'
|
|
||||||
import data from '../../../data/projectList.json'
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json(data)
|
|
||||||
}
|
|
20
src/app/blog/[slug]/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import DefaultLayout from '@/components/layouts/default-layout'
|
||||||
|
|
||||||
|
export interface PostLayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
params: {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PostLayout({ children }: PostLayoutProps) {
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
<main>
|
||||||
|
<article className="flex flex-col gap-4 text-xs md:text-base my-6">{children}</article>
|
||||||
|
</main>
|
||||||
|
</DefaultLayout>
|
||||||
|
)
|
||||||
|
}
|
127
src/app/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import matter from 'gray-matter'
|
||||||
|
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||||
|
|
||||||
|
import { Metadata } from 'next'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
import { components } from '@/components/mdx-components'
|
||||||
|
|
||||||
|
const ROOT_PATH = process.cwd()
|
||||||
|
const BLOG_PATH = path.join(ROOT_PATH, 'src', 'contents', 'posts')
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
params: {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FrontMatterMetadata = {
|
||||||
|
siteTitle: string
|
||||||
|
postTitle: string
|
||||||
|
siteDescription: string
|
||||||
|
postDescription: string
|
||||||
|
publishedOn: string
|
||||||
|
updatedOn: string
|
||||||
|
isPublished: boolean
|
||||||
|
tags: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostMetadata {
|
||||||
|
slug: string
|
||||||
|
frontMatter: FrontMatterMetadata
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the post from file system, matching the provided slug from the route params.
|
||||||
|
*/
|
||||||
|
function getPost(slug: string): PostMetadata {
|
||||||
|
try {
|
||||||
|
const markdownFile = fs.readFileSync(path.join(BLOG_PATH, slug + '.mdx'), 'utf-8')
|
||||||
|
|
||||||
|
const { data: frontMatter, content } = matter(markdownFile)
|
||||||
|
|
||||||
|
if (!frontMatter.isPublished) {
|
||||||
|
throw new Error('Post is currently not published yet!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
frontMatter: frontMatter as FrontMatterMetadata,
|
||||||
|
slug,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/*
|
||||||
|
* Catch any possible error (could be no slug exists, post has not been published,
|
||||||
|
* or something wrong with the fs) above and just render not found.
|
||||||
|
*/
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statically generate routes at build time.
|
||||||
|
*
|
||||||
|
* See: <https://nextjs.org/docs/app/api-reference/functions/generate-static-params>
|
||||||
|
*/
|
||||||
|
export async function generateStaticParams(): Promise<string[]> {
|
||||||
|
const files = fs.readdirSync(BLOG_PATH)
|
||||||
|
|
||||||
|
const slugPaths = files.map((filename) => filename.replace('.mdx', ''))
|
||||||
|
|
||||||
|
return slugPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
|
const { slug } = params
|
||||||
|
|
||||||
|
const post = getPost(slug)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.frontMatter.siteTitle,
|
||||||
|
description: post.frontMatter.siteDescription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Post({ params }: Props) {
|
||||||
|
const { slug } = params
|
||||||
|
|
||||||
|
const post = getPost(slug)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-4 my-8">
|
||||||
|
<div id="post-detail" className="flex flex-col gap-2 mb-2">
|
||||||
|
<h1 id="post-title" className="text-3xl md:text-4xl font-bold">
|
||||||
|
{post.frontMatter.postTitle}
|
||||||
|
</h1>
|
||||||
|
<span id="post-description" className="text-base md:text-lg">
|
||||||
|
{post.frontMatter.postDescription}
|
||||||
|
</span>
|
||||||
|
<div id="post-tags" className="flex gap-2 mb-4">
|
||||||
|
{post.frontMatter.tags.map((tag: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<span key={index} className="px-2 py-1 rounded-lg bg-primary/30 text-xs">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div id="post-publication-date" className="flex gap-2 font-bold text-xs">
|
||||||
|
<span>PUBLISHED ON: {post.frontMatter.publishedOn}</span>
|
||||||
|
{post.frontMatter.updatedOn !== post.frontMatter.publishedOn ? (
|
||||||
|
<>
|
||||||
|
<span>|</span>
|
||||||
|
<span>UPDATED ON: {post.frontMatter.updatedOn}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<article id="post-content" className="flex flex-col gap-4">
|
||||||
|
<MDXRemote source={post.content} components={components} />
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
18
src/app/blog/page.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
|
import DefaultLayout from '@/components/layouts/default-layout'
|
||||||
|
import UnderConstruction from '@/components/blocks/error/under-construction'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Blog',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Blog() {
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
<main className="flex justify-center items-center h-[85vh]">
|
||||||
|
<UnderConstruction />
|
||||||
|
</main>
|
||||||
|
</DefaultLayout>
|
||||||
|
)
|
||||||
|
}
|
BIN
src/app/favicon.ico
Normal file
After Width: | Height: | Size: 25 KiB |
@ -1,3 +0,0 @@
|
|||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
@ -1,69 +1,56 @@
|
|||||||
import { MantineProvider, ColorSchemeScript } from '@mantine/core'
|
import '@/styles/globals.css'
|
||||||
import { Metadata } from 'next'
|
|
||||||
|
|
||||||
import './global.css'
|
import type { Metadata, Viewport } from 'next'
|
||||||
import '@mantine/core/styles.css'
|
import { ReactNode } from 'react'
|
||||||
import { resolver, theme } from '../styles/theme'
|
|
||||||
|
import { siteConfig } from '@/config/site'
|
||||||
|
|
||||||
|
import { ThemeProvider } from '@/components/providers/theme-provider'
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: siteConfig.name,
|
||||||
|
template: `%s | ${siteConfig.name}`,
|
||||||
|
},
|
||||||
|
metadataBase: new URL(siteConfig.url),
|
||||||
|
description: siteConfig.description,
|
||||||
|
keywords: ['Muhammad Refansa Ali Muzky', 'Refansa'],
|
||||||
authors: [
|
authors: [
|
||||||
{
|
{
|
||||||
name: 'Refansa',
|
name: 'Muhammad Refansa Ali Muzky',
|
||||||
url: 'https://github.com/Refansa',
|
url: 'https://refansa.my.id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
title: {
|
creator: 'Muhammad Refansa Ali Muzky',
|
||||||
template: 'Refansa - Software Developer | %s',
|
|
||||||
default: 'Refansa - Software Developer',
|
|
||||||
},
|
|
||||||
description: "Refansa's portfolio website",
|
|
||||||
viewport: 'width=device-width, initial-scale=1',
|
|
||||||
colorScheme: 'dark',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export const viewport: Viewport = {
|
||||||
children,
|
themeColor: [
|
||||||
}: {
|
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||||
children: React.ReactNode
|
{ media: '(prefers-color-scheme: dark)', color: 'black' },
|
||||||
}) {
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootLayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<html lang={process.env.APP_LANG ?? 'en'}>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head />
|
||||||
<link
|
|
||||||
rel='preconnect'
|
|
||||||
href='https://fonts.googleapis.com'
|
|
||||||
crossOrigin='anonymous'
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel='preconnect'
|
|
||||||
href='https://fonts.gstatic.com'
|
|
||||||
crossOrigin='anonymous'
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href='https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'
|
|
||||||
rel='preload'
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href='https://fonts.googleapis.com/css2?family=Gabarito:wght@400;500;600;700;800;900&display=swap'
|
|
||||||
rel='preload'
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'
|
|
||||||
rel='preload'
|
|
||||||
/>
|
|
||||||
<ColorSchemeScript
|
|
||||||
defaultColorScheme={'dark'}
|
|
||||||
forceColorScheme={'dark'}
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider
|
<ThemeProvider
|
||||||
defaultColorScheme={'dark'}
|
attribute="class"
|
||||||
forceColorScheme={'dark'}
|
defaultTheme="system"
|
||||||
cssVariablesResolver={resolver}
|
enableSystem
|
||||||
theme={theme}>
|
disableTransitionOnChange
|
||||||
{children}
|
>
|
||||||
</MantineProvider>
|
<TooltipProvider>
|
||||||
|
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
18
src/app/not-found.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
|
import DefaultLayout from '@/components/layouts/default-layout'
|
||||||
|
import PageNotFound from '@/components/blocks/error/page-not-found'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'You seem to be lost...',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
<main className="flex justify-center items-center h-[85vh]">
|
||||||
|
<PageNotFound />
|
||||||
|
</main>
|
||||||
|
</DefaultLayout>
|
||||||
|
)
|
||||||
|
}
|
@ -1,34 +1,16 @@
|
|||||||
'use client'
|
import DefaultLayout from '@/components/layouts/default-layout'
|
||||||
|
import IntroductionSection from '@/components/blocks/home/introduction-section'
|
||||||
|
import AboutSection from '@/components/blocks/home/about-section'
|
||||||
|
import ContactSection from '@/components/blocks/home/contact-section'
|
||||||
|
|
||||||
import { AppShell, Container, Stack } from '@mantine/core'
|
export default function Home() {
|
||||||
import Introduction from '../partials/Introduction'
|
|
||||||
import About from '../partials/About'
|
|
||||||
import Navbar from '../components/Navbar'
|
|
||||||
import Footer from '../components/Footer'
|
|
||||||
import Experiences from '../partials/Experiences'
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<DefaultLayout>
|
||||||
withBorder={false}
|
<main className="flex flex-col gap-24 mb-24">
|
||||||
padding={'xl'}
|
<IntroductionSection />
|
||||||
header={{ height: 72 }}
|
<AboutSection />
|
||||||
className={'shell-root'}>
|
<ContactSection />
|
||||||
<AppShell.Header>
|
</main>
|
||||||
<Navbar />
|
</DefaultLayout>
|
||||||
</AppShell.Header>
|
|
||||||
<AppShell.Main>
|
|
||||||
<Container
|
|
||||||
size={1920}
|
|
||||||
px={{ base: '0rem', lg: '4rem', xl: '8rem' }}>
|
|
||||||
<Stack>
|
|
||||||
<Introduction />
|
|
||||||
<About />
|
|
||||||
<Experiences />
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import Project from './project'
|
|
||||||
import data from '../../data/projectList.json'
|
import DefaultLayout from '@/components/layouts/default-layout'
|
||||||
|
import UnderConstruction from '@/components/blocks/error/under-construction'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Projects',
|
title: 'Projects',
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProjectLists() {
|
export default function Projects() {
|
||||||
return data
|
return (
|
||||||
}
|
<DefaultLayout>
|
||||||
|
<main className="flex justify-center items-center h-[85vh]">
|
||||||
export default async function Projects() {
|
<UnderConstruction />
|
||||||
const projectLists = await getProjectLists()
|
</main>
|
||||||
|
</DefaultLayout>
|
||||||
return <Project projectLists={projectLists} />
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { AppShell, Container, Stack } from '@mantine/core'
|
|
||||||
import Footer from '../../components/Footer'
|
|
||||||
import Navbar from '../../components/Navbar'
|
|
||||||
import Projects, { ProjectItemProps } from '../../partials/Projects'
|
|
||||||
|
|
||||||
export default function Project({
|
|
||||||
projectLists,
|
|
||||||
}: {
|
|
||||||
projectLists: ProjectItemProps[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
withBorder={false}
|
|
||||||
padding={'xl'}
|
|
||||||
header={{ height: 72 }}
|
|
||||||
className={'shell-root'}>
|
|
||||||
<AppShell.Header>
|
|
||||||
<Navbar />
|
|
||||||
</AppShell.Header>
|
|
||||||
<AppShell.Main>
|
|
||||||
<Container
|
|
||||||
size={1920}
|
|
||||||
px={{ base: '0rem', lg: '4rem', xl: '8rem' }}>
|
|
||||||
<Stack>
|
|
||||||
<Projects projectLists={projectLists} />
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { Anchor, Badge, Box, SimpleGrid, Stack, Text } from '@mantine/core'
|
|
||||||
import pkg from '../../package.json'
|
|
||||||
|
|
||||||
const { version } = pkg
|
|
||||||
|
|
||||||
export default function Footer() {
|
|
||||||
return (
|
|
||||||
<Box mt='xl'>
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{ base: 1, sm: 2 }}
|
|
||||||
h={120}
|
|
||||||
spacing={{ base: 'sm', md: 'md', lg: 'lg' }}>
|
|
||||||
<Stack>
|
|
||||||
<Text fw='bold'>
|
|
||||||
Email:{' '}
|
|
||||||
<Anchor href='mailto:m.refansa.am@gmail.com'>
|
|
||||||
m.refansa.am@gmail.com
|
|
||||||
</Anchor>
|
|
||||||
</Text>
|
|
||||||
<Text fw='bold'>
|
|
||||||
Tel:{' '}
|
|
||||||
<Anchor href='tel:+62-812-8543-3284'>(+62) 812-8543-3284</Anchor>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack>{/* empty space */}</Stack>
|
|
||||||
</SimpleGrid>
|
|
||||||
<Stack align='center'>
|
|
||||||
<Badge variant='outline'>V. {version}</Badge>
|
|
||||||
<Text fw='bold'>
|
|
||||||
Created with ❤️ by{' '}
|
|
||||||
<Anchor
|
|
||||||
fw='bold'
|
|
||||||
href='https://github.com/Refansa'>
|
|
||||||
Refansa
|
|
||||||
</Anchor>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
.bracket {
|
|
||||||
color: var(--mantine-color-dark-4);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { Anchor, Title } from '@mantine/core'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import classes from './NavIcon.module.css'
|
|
||||||
|
|
||||||
function NavIcon() {
|
|
||||||
return (
|
|
||||||
<Anchor
|
|
||||||
component={Link}
|
|
||||||
href='/'
|
|
||||||
underline='never'>
|
|
||||||
<Title ff='Fira Code'>
|
|
||||||
<span className={classes.bracket}>{'{'}</span>
|
|
||||||
<span>R</span>
|
|
||||||
<span className={classes.bracket}>{'}'}</span>
|
|
||||||
</Title>
|
|
||||||
</Anchor>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NavIcon
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Button } from '@mantine/core'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { MouseEventHandler } from 'react'
|
|
||||||
|
|
||||||
interface NavLinkProps {
|
|
||||||
content: string
|
|
||||||
href: string
|
|
||||||
onClick?: MouseEventHandler<HTMLElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavLink({ content, href, onClick }: NavLinkProps) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
variant='subtle'
|
|
||||||
size='md'
|
|
||||||
onClick={onClick}
|
|
||||||
href={href}>
|
|
||||||
{content}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NavLink
|
|
@ -1,35 +0,0 @@
|
|||||||
import { useDisclosure } from '@mantine/hooks'
|
|
||||||
import { Box, Burger, Drawer, Stack } from '@mantine/core'
|
|
||||||
import NavLink from './NavLink'
|
|
||||||
|
|
||||||
export default function NavMobile() {
|
|
||||||
const [opened, { open, close }] = useDisclosure(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box hiddenFrom='sm'>
|
|
||||||
<Drawer
|
|
||||||
opened={opened}
|
|
||||||
onClose={close}
|
|
||||||
overlayProps={{ backgroundOpacity: 0.5, blur: 1 }}
|
|
||||||
position='right'
|
|
||||||
size='xs'>
|
|
||||||
<Stack>
|
|
||||||
<NavLink
|
|
||||||
content='About'
|
|
||||||
href='/#about'
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
<NavLink
|
|
||||||
content='Projects'
|
|
||||||
href='/projects'
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Drawer>
|
|
||||||
<Burger
|
|
||||||
opened={opened}
|
|
||||||
onClick={open}
|
|
||||||
aria-label='Toggle navigation'
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
.navigation {
|
|
||||||
display: flex;
|
|
||||||
padding-inline: 3vw;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100%;
|
|
||||||
border-bottom: 1px solid var(--mantine-color-dark-7);
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Box, Group } from '@mantine/core'
|
|
||||||
|
|
||||||
import classes from './Navbar.module.css'
|
|
||||||
import NavIcon from './NavIcon'
|
|
||||||
import NavLink from './NavLink'
|
|
||||||
import NavMobile from './NavMobile'
|
|
||||||
|
|
||||||
function Navbar() {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
component='nav'
|
|
||||||
className={classes.navigation}>
|
|
||||||
<NavIcon />
|
|
||||||
<Group visibleFrom='sm'>
|
|
||||||
<NavLink
|
|
||||||
content='About'
|
|
||||||
href='/'
|
|
||||||
/>
|
|
||||||
<NavLink
|
|
||||||
content='Projects'
|
|
||||||
href='/projects'
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<NavMobile />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Navbar
|
|
27
src/components/anchor.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { UrlObject } from 'url'
|
||||||
|
|
||||||
|
import { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||||
|
href: string | UrlObject
|
||||||
|
/**
|
||||||
|
* Whether the type of the anchor is for external or internal links.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isExternal?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Anchor({ href, isExternal = false, children, ...rest }: Props) {
|
||||||
|
return !isExternal ? (
|
||||||
|
<Link className="underline hover:text-foreground/80" href={href} {...rest}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a className="underline hover:text-foreground/80" href={href.toString()} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
21
src/components/blocks/error/page-not-found.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { HomeIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export default function PageNotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex font-mono gap-4 flex-col items-center tracking-wide">
|
||||||
|
<span className="text-7xl md:text-9xl">404</span>
|
||||||
|
<i className="text-xl md:text-2xl">Not Found</i>
|
||||||
|
<p className="text-center">You are trying to access a page that doesn't exists.</p>
|
||||||
|
<Button className="font-sans font-bold" variant="secondary">
|
||||||
|
<Link className="flex gap-2 items-center" href="/">
|
||||||
|
<HomeIcon />
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
21
src/components/blocks/error/under-construction.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { HomeIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export default function UnderConstruction() {
|
||||||
|
return (
|
||||||
|
<div className="flex font-mono gap-4 flex-col items-center tracking-wide">
|
||||||
|
<span className="text-7xl md:text-9xl">501</span>
|
||||||
|
<i className="text-xl md:text-2xl">Not Implemented</i>
|
||||||
|
<p className="text-center">Sorry! The page is currently under construction.</p>
|
||||||
|
<Button className="font-sans font-bold" variant="secondary">
|
||||||
|
<Link className="flex gap-2 items-center" href="/">
|
||||||
|
<HomeIcon />
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
15
src/components/blocks/footer/footer.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Anchor from '@/components/anchor'
|
||||||
|
import Package from '../../../../package.json'
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="flex flex-col gap-1 items-center mb-8 text-xs md:text-base">
|
||||||
|
<p className="font-semibold text-center">
|
||||||
|
Created with ❤️ by{' '}
|
||||||
|
<Anchor href={Package.author.url} isExternal>
|
||||||
|
{Package.author.nickname}
|
||||||
|
</Anchor>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
90
src/components/blocks/header/header-navigation.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
|
import { MenuIcon, XIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@/components/ui/sheet'
|
||||||
|
import NavigationItem from '@/components/blocks/header/navigation-item'
|
||||||
|
|
||||||
|
const Clock = dynamic(() => import('@/components/clock').then((mod) => mod.Clock), {
|
||||||
|
loading: () => <Skeleton className="md:w-52 w-[100px] h-7" />,
|
||||||
|
ssr: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ThemeSwitch = dynamic(
|
||||||
|
() => import('@/components/theme-switch').then((mod) => mod.ThemeSwitch),
|
||||||
|
{
|
||||||
|
loading: () => <Skeleton className="w-10 h-10" />,
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function HeaderNavigation() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
const smoothHeaderScroll = (e: any) => {
|
||||||
|
if (pathname === '/') {
|
||||||
|
e.preventDefault()
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="flex h-16 p-2 items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Link href="/" className="font-bold text-xl" onClick={smoothHeaderScroll}>
|
||||||
|
Refansa
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex justify-center">
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
<div id="Desktop" className="flex-1 hidden md:flex gap-4 items-center justify-end">
|
||||||
|
<NavigationItem href="/blog">Blog</NavigationItem>
|
||||||
|
<NavigationItem href="/projects">Projects</NavigationItem>
|
||||||
|
<ThemeSwitch />
|
||||||
|
</div>
|
||||||
|
<div id="Mobile" className="flex-1 flex md:hidden justify-end">
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MenuIcon />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="right">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle className="flex gap-2 justify-between px-2">
|
||||||
|
<ThemeSwitch starterId={10} />
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription className="flex flex-col gap-2">
|
||||||
|
<NavigationItem href="/blog">Blog</NavigationItem>
|
||||||
|
<NavigationItem href="/projects">Projects</NavigationItem>
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
9
src/components/blocks/header/header.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import HeaderNavigation from './header-navigation'
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 backdrop-blur-xl bg-background/80">
|
||||||
|
<HeaderNavigation />
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
20
src/components/blocks/header/navigation-item.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
children: ReactNode
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavigationItem({ children, href }: Props) {
|
||||||
|
return (
|
||||||
|
<Button asChild variant="ghost">
|
||||||
|
<Link href={href}>
|
||||||
|
<span className="font-bold">{children}</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
40
src/components/blocks/home/about-section.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import Anchor from '@/components/anchor'
|
||||||
|
import TermWord from '@/components/term-word'
|
||||||
|
import { Heading } from '@/components/ui/heading'
|
||||||
|
|
||||||
|
export default function AboutSection() {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col gap-4 tracking-wide leading-relaxed text-xs md:text-base">
|
||||||
|
<Heading level={3}>A bit about me</Heading>
|
||||||
|
<p>
|
||||||
|
I'm a Software Developer from Jakarta, Indonesia 🇮🇩,{' '}
|
||||||
|
<TermWord description="Nice to meet you!">Senang berkenalan denganmu!</TermWord>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is my humble internet abode, where I sometimes <Anchor href="/blog">blog</Anchor> about
|
||||||
|
programming, software development, game development, and some 3D modeling in my daily work.
|
||||||
|
But I mainly do web development, so that's probably what you will commonly see.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I love nothing more than diving into complex projects, but that doesn't mean I admire
|
||||||
|
complexity over simplicity, quite the contrary in fact. It always amaze me how people turn a
|
||||||
|
complex problems into a simple, digestable format for a simpleton like me to understand.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
As a supporter of open source, I believe that sharing knowledge and collaborating on
|
||||||
|
projects is essential for the advancement of technologies.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Oh! And before I forget, I always have this urge to say that{' '}
|
||||||
|
<em className="text-foreground/50">
|
||||||
|
I use{' '}
|
||||||
|
<TermWord description="Arch Linux, a lightweight and flexible Linux® distribution that tries to Keep It Simple.">
|
||||||
|
<em>arch</em>
|
||||||
|
</TermWord>{' '}
|
||||||
|
btw
|
||||||
|
</em>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
20
src/components/blocks/home/contact-section.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { siteConfig } from '@/config/site'
|
||||||
|
|
||||||
|
import { Heading } from '@/components/ui/heading'
|
||||||
|
import Anchor from '@/components/anchor'
|
||||||
|
|
||||||
|
export default function ContactSection() {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col gap-4 text-xs md:text-base tracking-wide leading-relaxed">
|
||||||
|
<Heading level={3}>Contact</Heading>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
Email: <Anchor href={siteConfig.links.email}>{siteConfig.email}</Anchor>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Tel: <Anchor href={siteConfig.links.tel}>{siteConfig.tel}</Anchor>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
51
src/components/blocks/home/introduction-section.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { SiGithub } from '@icons-pack/react-simple-icons'
|
||||||
|
|
||||||
|
import { Mail } from 'lucide-react'
|
||||||
|
|
||||||
|
import { siteConfig } from '@/config/site'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import TermWord from '@/components/term-word'
|
||||||
|
|
||||||
|
export default function IntroductionSection() {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col py-24 gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-16 h-[2px] bg-primary" />
|
||||||
|
<span className="md:text-xl font-bold text-primary">Welcome, New & Old Friends!</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-5xl md:text-7xl font-bold"
|
||||||
|
style={{ textShadow: '3px 3px hsla(var(--primary) / 0.4)' }}
|
||||||
|
>
|
||||||
|
I'm Refansa
|
||||||
|
</span>
|
||||||
|
<div className="mt-4 flex flex-col">
|
||||||
|
<span className="text-lg md:text-2xl font-bold">
|
||||||
|
A Passionate,{' '}
|
||||||
|
<TermWord description="College is too expensive nowadays.">
|
||||||
|
<i>self-taught</i>
|
||||||
|
</TermWord>{' '}
|
||||||
|
Software Developer
|
||||||
|
</span>
|
||||||
|
<span className="text-lg md:text-2xl font-bold text-foreground/50">
|
||||||
|
And a Patron of Open Source Software.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex gap-4">
|
||||||
|
<Button className="text-lg font-bold flex gap-2" size="lg" asChild>
|
||||||
|
<a href={siteConfig.links.github}>
|
||||||
|
<SiGithub />
|
||||||
|
Github
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button className="text-lg font-bold flex gap-2" variant="secondary" size="lg" asChild>
|
||||||
|
<a href={siteConfig.links.email}>
|
||||||
|
<Mail />
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
35
src/components/clock.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function Clock() {
|
||||||
|
const [time, setTime] = useState(
|
||||||
|
new Intl.DateTimeFormat('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZone: 'Asia/Jakarta',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timerInterval = setInterval(() => {
|
||||||
|
setTime(
|
||||||
|
new Intl.DateTimeFormat('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZone: 'Asia/Jakarta',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timerInterval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="font-bold text-lg md:text-xl">
|
||||||
|
<span>{time.format()}</span>
|
||||||
|
<span className="md:inline hidden"> - </span>
|
||||||
|
<span className="md:inline hidden">Jakarta</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
18
src/components/layouts/default-layout.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import Header from '@/components/blocks/header/header'
|
||||||
|
import Footer from '@/components/blocks/footer/footer'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DefaultLayout({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-screen-lg w-full mx-auto px-6">
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
46
src/components/mdx-components.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { MDXComponents } from 'mdx/types'
|
||||||
|
|
||||||
|
import Anchor from '@/components/anchor'
|
||||||
|
import TermWord from '@/components/term-word'
|
||||||
|
import { Heading } from '@/components/ui/heading'
|
||||||
|
|
||||||
|
export const components: MDXComponents = {
|
||||||
|
Anchor,
|
||||||
|
TermWord,
|
||||||
|
Heading,
|
||||||
|
p(props) {
|
||||||
|
return <p className="tracking-wide leading-relaxed">{props.children}</p>
|
||||||
|
},
|
||||||
|
h1(props) {
|
||||||
|
return <Heading level={1}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
h2(props) {
|
||||||
|
return <Heading level={2}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
h3(props) {
|
||||||
|
return <Heading level={3}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
h4(props) {
|
||||||
|
return <Heading level={4}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
h5(props) {
|
||||||
|
return <Heading level={5}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
h6(props) {
|
||||||
|
return <Heading level={6}>{props.children as string}</Heading>
|
||||||
|
},
|
||||||
|
code(props) {
|
||||||
|
return (
|
||||||
|
<code className="bg-gray-500/20 text-foreground/50 p-1 tracking-normal">
|
||||||
|
{props.children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
blockquote(props) {
|
||||||
|
return (
|
||||||
|
<blockquote className="bg-gray-500/20 my-3 px-4 py-6 border-l-8 border-gray-500/50">
|
||||||
|
{props.children}
|
||||||
|
</blockquote>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
9
src/components/providers/theme-provider.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||||
|
import { type ThemeProviderProps } from 'next-themes/dist/types'
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
26
src/components/term-word.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HTMLAttributes, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
export interface Props extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
children: ReactNode
|
||||||
|
/**
|
||||||
|
* The description of the term word.
|
||||||
|
*/
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TermWord({ children, description, ...rest }: Props) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span className="underline decoration-dashed underline-offset-2" {...rest}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<span className="not-italic font-normal">{description}</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
137
src/components/theme-switch.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
import { animated, useSpring } from '@react-spring/web'
|
||||||
|
import { useEffect, useState, HTMLAttributes } from 'react'
|
||||||
|
import { Button } from './ui/button'
|
||||||
|
|
||||||
|
type SVGProps = Omit<HTMLAttributes<HTMLOrSVGElement>, 'onChange'>
|
||||||
|
|
||||||
|
export interface Props extends SVGProps {
|
||||||
|
onChange?: (checked: boolean) => void
|
||||||
|
size?: number | string
|
||||||
|
moonColor?: string
|
||||||
|
sunColor?: string
|
||||||
|
starterId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeSwitch({
|
||||||
|
onChange,
|
||||||
|
size = 24,
|
||||||
|
moonColor = 'white',
|
||||||
|
sunColor = 'dark',
|
||||||
|
starterId = 0,
|
||||||
|
}: Props) {
|
||||||
|
let REACT_TOGGLE_DARK_MODE_GLOBAL_ID = starterId
|
||||||
|
|
||||||
|
const { theme, systemTheme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
const [id, setId] = useState(REACT_TOGGLE_DARK_MODE_GLOBAL_ID)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
REACT_TOGGLE_DARK_MODE_GLOBAL_ID += 1
|
||||||
|
setId(REACT_TOGGLE_DARK_MODE_GLOBAL_ID)
|
||||||
|
}, [setId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the theme is currently using system theme, set the theme according to the system theme value.
|
||||||
|
if (theme === 'system') {
|
||||||
|
setTheme(systemTheme as 'dark' | 'light')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const properties = {
|
||||||
|
circle: {
|
||||||
|
r: theme === 'dark' ? 9 : 5,
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
cx: theme === 'dark' ? '50%' : '100',
|
||||||
|
cy: theme === 'dark' ? '23%' : '0%',
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
transform: theme === 'dark' ? 'rotate(40deg)' : 'rotate(90deg)',
|
||||||
|
},
|
||||||
|
lines: {
|
||||||
|
opacity: theme === 'dark' ? 0 : 1,
|
||||||
|
},
|
||||||
|
config: { mass: 4, tension: 250, friction: 35 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgContainerProps = useSpring({
|
||||||
|
...properties.svg,
|
||||||
|
config: properties.config,
|
||||||
|
})
|
||||||
|
|
||||||
|
const centerCircleProps = useSpring({
|
||||||
|
...properties.circle,
|
||||||
|
config: properties.config,
|
||||||
|
})
|
||||||
|
const maskedCircleProps = useSpring({
|
||||||
|
...properties.mask,
|
||||||
|
config: properties.config,
|
||||||
|
})
|
||||||
|
const linesProps = useSpring({
|
||||||
|
...properties.lines,
|
||||||
|
config: properties.config,
|
||||||
|
})
|
||||||
|
|
||||||
|
const uniqueMaskId = `circle-mask-${id}`
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||||
|
onChange && onChange(theme === 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={toggle} variant="ghost" size="icon">
|
||||||
|
<div className="flex items-center w-5 h-5 bg-transparent">
|
||||||
|
<animated.svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
color={theme === 'dark' ? moonColor : sunColor}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
stroke="currentColor"
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
...svgContainerProps,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<mask id={uniqueMaskId}>
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||||
|
<animated.circle
|
||||||
|
// @ts-ignore
|
||||||
|
style={maskedCircleProps}
|
||||||
|
r="9"
|
||||||
|
fill="black"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
<animated.circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
fill={theme === 'dark' ? moonColor : sunColor}
|
||||||
|
// @ts-ignore
|
||||||
|
style={centerCircleProps}
|
||||||
|
mask={`url(#${uniqueMaskId})`}
|
||||||
|
/>
|
||||||
|
<animated.g stroke="currentColor" style={linesProps}>
|
||||||
|
<line x1="12" y1="1" x2="12" y2="3" />
|
||||||
|
<line x1="12" y1="21" x2="12" y2="23" />
|
||||||
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||||
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||||
|
<line x1="1" y1="12" x2="3" y2="12" />
|
||||||
|
<line x1="21" y1="12" x2="23" y2="12" />
|
||||||
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||||
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||||
|
</animated.g>
|
||||||
|
</animated.svg>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
49
src/components/ui/button.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
|
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-10 px-4 py-2',
|
||||||
|
sm: 'h-9 rounded-md px-3',
|
||||||
|
lg: 'h-11 rounded-md px-8',
|
||||||
|
icon: 'h-10 w-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button'
|
||||||
|
return (
|
||||||
|
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
86
src/components/ui/heading.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import slug from 'slug'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
type HeadingProps = HTMLAttributes<HTMLHeadingElement>
|
||||||
|
|
||||||
|
export interface Props extends HeadingProps {
|
||||||
|
children: string
|
||||||
|
/**
|
||||||
|
* Heading level, each level represent the HTML heading level.
|
||||||
|
* @min 1
|
||||||
|
* @max 6
|
||||||
|
*/
|
||||||
|
level: number
|
||||||
|
/**
|
||||||
|
* If `true`, the heading will be associated with a hash link.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
withLink?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const HashLink = ({ text }: { text: string }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href={`#${slug(text)}`}
|
||||||
|
className="group-hover/heading:opacity-100 opacity-0 transition-opacity ease-in-out duration-500 text-foreground/40"
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</Link>
|
||||||
|
<div id={slug(text)} className="relative invisible -top-24" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Heading({ children, level, withLink = true, ...rest }: Props) {
|
||||||
|
const defaultClasses = ['group/heading', 'font-bold', 'flex', 'items-center', 'gap-4', 'mb-2']
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<h1 {...rest} className={cn(defaultClasses, 'text-4xl md:text-6xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<h2 {...rest} className={cn(defaultClasses, 'text-3xl md:text-5xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<h3 {...rest} className={cn(defaultClasses, 'text-2xl md:text-4xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
case 4:
|
||||||
|
return (
|
||||||
|
<h4 {...rest} className={cn(defaultClasses, 'text-xl md:text-3xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h4>
|
||||||
|
)
|
||||||
|
case 5:
|
||||||
|
return (
|
||||||
|
<h5 {...rest} className={cn(defaultClasses, 'text-lg md:text-2xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h5>
|
||||||
|
)
|
||||||
|
case 6:
|
||||||
|
return (
|
||||||
|
<h6 {...rest} className={cn(defaultClasses, 'text-base md:text-xl', rest.className)}>
|
||||||
|
{children}
|
||||||
|
{withLink ? <HashLink text={children} /> : null}
|
||||||
|
</h6>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
116
src/components/ui/sheet.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root
|
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger
|
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close
|
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const sheetVariants = cva(
|
||||||
|
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
|
bottom:
|
||||||
|
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||||
|
right:
|
||||||
|
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SheetContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
))
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||||
|
)
|
||||||
|
SheetHeader.displayName = 'SheetHeader'
|
||||||
|
|
||||||
|
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
SheetFooter.displayName = 'SheetFooter'
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-lg font-semibold text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetPortal,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
7
src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
30
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
19
src/config/site.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export const siteConfig = {
|
||||||
|
name: 'Refansa',
|
||||||
|
email: 'm.refansa.am@gmail.com',
|
||||||
|
tel: '(+62) 812-8543-3284',
|
||||||
|
url: 'https://refansa.my.id',
|
||||||
|
description: 'A humble internet abode.',
|
||||||
|
get links() {
|
||||||
|
return siteLinks
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const siteLinks = {
|
||||||
|
github: 'https://github.com/refansa',
|
||||||
|
email: `mailto:${siteConfig.email}`,
|
||||||
|
tel: `tel:${siteConfig.tel}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SiteConfig = typeof siteConfig
|
||||||
|
export type SiteLinks = typeof siteLinks
|
20
src/contents/posts/the-weird-state-of-web-development.mdx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
siteTitle: 'What the heck is going on with web development?'
|
||||||
|
postTitle: 'The Weird State of Web Development'
|
||||||
|
siteDescription: 'Javascript, the mother of all frameworks.'
|
||||||
|
postDescription: 'A never ending solution to a never ending problem...'
|
||||||
|
publishedOn: '03 August, 2024'
|
||||||
|
updatedOn: '04 August, 2024'
|
||||||
|
isPublished: true
|
||||||
|
tags: ['web-development', 'personal-thought']
|
||||||
|
---
|
||||||
|
|
||||||
|
I still remember the first time I tried to dip my toe into the world of programming. On that time, I still can't wrap my head around some of the general-purpose language
|
||||||
|
like C, C++, Java or the likes (*Yes, I'm just that stupid at the time*) actually works, this was in my early days when I start learning how to code in junior high school.
|
||||||
|
Everytime I tried C, I keep shooting my foot with **`segmentation fault`** errors, so I say to myself,
|
||||||
|
|
||||||
|
> You know what? Let me just learn how to create a website, it look simple enough. It won't be too hard isn't it? (*But boy do I was wrong...*)
|
||||||
|
|
||||||
|
I start off with just HTML, CSS, and a little bit of javascript, it was just a simple personal website. But then I start comparing my work with others.
|
||||||
|
I keep mumbling to myself, how do I do this? How do I do that? It was more complicated than I imagined. I thought browser just support HTML, CSS, and JS, so I did my research. But to my surprise,
|
||||||
|
<TermWord description="There are other things that are supported by browsers, but I'm talking about the main general support of browser DOM rendering.">**THEY DO JUST THAT**<span className="dark:text-red-400 text-red-600">*</span></TermWord>.
|
@ -1,49 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"title": "AmanaTax",
|
|
||||||
"description": "A video course website to learn about taxes.",
|
|
||||||
"imgSrc": "/assets/project-1.png",
|
|
||||||
"alt": "AmanaTax Homepage",
|
|
||||||
"tags": [
|
|
||||||
"internship project",
|
|
||||||
"laravel",
|
|
||||||
"completed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Koperasi",
|
|
||||||
"description": "Cooperatives website that accept online transactions.",
|
|
||||||
"codeSrc": "https://github.com/Refansa/koperasi",
|
|
||||||
"imgSrc": "/assets/project-2.png",
|
|
||||||
"alt": "Koperasi Homepage",
|
|
||||||
"tags": [
|
|
||||||
"school project",
|
|
||||||
"vue.js",
|
|
||||||
"completed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "RMBG",
|
|
||||||
"description": "Easily remove background from an image with one simple click.",
|
|
||||||
"codeSrc": "https://github.com/Refansa/rmbg",
|
|
||||||
"imgSrc": "/assets/project-3.png",
|
|
||||||
"alt": "RMBG Website",
|
|
||||||
"tags": [
|
|
||||||
"personal project",
|
|
||||||
"react.js",
|
|
||||||
"completed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "This Portfolio",
|
|
||||||
"description": "My own portfolio!",
|
|
||||||
"codeSrc": "https://github.com/Refansa/portfolio",
|
|
||||||
"imgSrc": "/assets/project-4.png",
|
|
||||||
"alt": "Portfolio Homepage",
|
|
||||||
"tags": [
|
|
||||||
"personal project",
|
|
||||||
"react",
|
|
||||||
"in progress"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,35 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { motion, useAnimation } from 'framer-motion'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useInView } from 'react-intersection-observer'
|
|
||||||
|
|
||||||
export default function SlideUpWhenVisible({
|
|
||||||
children,
|
|
||||||
threshold,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
threshold?: number | number[]
|
|
||||||
}) {
|
|
||||||
const controls = useAnimation()
|
|
||||||
const [ref, inView] = useInView({ threshold: threshold ? threshold : 0 })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inView) {
|
|
||||||
controls.start('visible')
|
|
||||||
}
|
|
||||||
}, [controls, inView])
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
ref={ref}
|
|
||||||
animate={controls}
|
|
||||||
initial='hidden'
|
|
||||||
transition={{ duration: 0.4 }}
|
|
||||||
variants={{
|
|
||||||
visible: { opacity: 1, y: 0 },
|
|
||||||
hidden: { opacity: 0, y: 20 },
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
.grayscale {
|
|
||||||
filter: grayscale(1);
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
import {
|
|
||||||
Anchor,
|
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
Image,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import classes from './About.module.css'
|
|
||||||
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
|
|
||||||
|
|
||||||
function About() {
|
|
||||||
return (
|
|
||||||
<SlideUpWhenVisible>
|
|
||||||
<Box my='xl'>
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{ base: 1, sm: 2 }}
|
|
||||||
spacing={{ base: 'sm', md: 'md', lg: 'lg' }}>
|
|
||||||
<Stack>
|
|
||||||
<Title>A bit About Me</Title>
|
|
||||||
<Stack gap='md'>
|
|
||||||
<Text>
|
|
||||||
Hello 👋 Refansa here. Muhammad Refansa Ali Muzky is my full
|
|
||||||
name. I'm an 18 years old software developer, just came fresh
|
|
||||||
out of the oven. Constantly seeking new open source projects to
|
|
||||||
contribute to and enjoy working with others to make a meaningful
|
|
||||||
contribution.
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
I thrive on challenging coding projects and love nothing more
|
|
||||||
than diving into complex software development tasks. As a lover
|
|
||||||
of open source, I believe that sharing knowledge and
|
|
||||||
collaborating on projects is essential for the advancement of
|
|
||||||
the tech industry.
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
mt='lg'
|
|
||||||
c='dark.1'
|
|
||||||
fs='italic'>
|
|
||||||
I use Archlinux btw.
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
mt='xl'
|
|
||||||
fw='bold'>
|
|
||||||
Peace from Indonesia 🇮🇩
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<Container visibleFrom='sm'>
|
|
||||||
<Stack
|
|
||||||
justify='center'
|
|
||||||
w={360}>
|
|
||||||
<Image
|
|
||||||
className={classes.grayscale}
|
|
||||||
src='/assets/hand-coding.svg'
|
|
||||||
alt='Coding Illustration'
|
|
||||||
width={360}
|
|
||||||
height='auto'
|
|
||||||
/>
|
|
||||||
<Anchor
|
|
||||||
ta='center'
|
|
||||||
fs='italic'
|
|
||||||
fz='xs'
|
|
||||||
href='https://storyset.com/work'>
|
|
||||||
Work illustrations by Storyset
|
|
||||||
</Anchor>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
</SlideUpWhenVisible>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default About
|
|
@ -1,142 +0,0 @@
|
|||||||
import { Box, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'
|
|
||||||
import {
|
|
||||||
IconBrandCss3,
|
|
||||||
IconBrandHtml5,
|
|
||||||
IconBrandJavascript,
|
|
||||||
IconBrandLaravel,
|
|
||||||
IconBrandMantine,
|
|
||||||
IconBrandMysql,
|
|
||||||
IconBrandNextjs,
|
|
||||||
IconBrandNodejs,
|
|
||||||
IconBrandPhp,
|
|
||||||
IconBrandPrisma,
|
|
||||||
IconBrandPython,
|
|
||||||
IconBrandReact,
|
|
||||||
IconBrandSass,
|
|
||||||
IconBrandSupabase,
|
|
||||||
IconBrandTailwind,
|
|
||||||
IconBrandVue,
|
|
||||||
} from '@tabler/icons-react'
|
|
||||||
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
|
|
||||||
|
|
||||||
interface ItemGridProps {
|
|
||||||
icon: React.ReactNode
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Item = ({ icon, text }: ItemGridProps) => {
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
w={100}
|
|
||||||
h={100}>
|
|
||||||
<Stack
|
|
||||||
p='sm'
|
|
||||||
w='inherit'
|
|
||||||
h='inherit'
|
|
||||||
align='center'
|
|
||||||
justify='center'>
|
|
||||||
{icon}
|
|
||||||
<Text
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
fw='bold'
|
|
||||||
fz='sm'>
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Experiences() {
|
|
||||||
return (
|
|
||||||
<SlideUpWhenVisible>
|
|
||||||
<Box my='xl'>
|
|
||||||
<Stack
|
|
||||||
align='center'
|
|
||||||
ta='center'>
|
|
||||||
<Title>Experiences</Title>
|
|
||||||
<Text
|
|
||||||
fz='lg'
|
|
||||||
ta='center'>
|
|
||||||
I have worked and used these awesome technologies in my projects
|
|
||||||
</Text>
|
|
||||||
<SimpleGrid
|
|
||||||
mt='xl'
|
|
||||||
w={{ base: 240, sm: 480, md: 720, lg: 960 }}
|
|
||||||
cols={{ base: 2, sm: 4, md: 6, lg: 8 }}>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandHtml5 />}
|
|
||||||
text='HTML'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandCss3 />}
|
|
||||||
text='CSS'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandJavascript />}
|
|
||||||
text='Javascript'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandPhp />}
|
|
||||||
text='PHP'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandPython />}
|
|
||||||
text='Python'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandMysql />}
|
|
||||||
text='MySQL'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandLaravel />}
|
|
||||||
text='Laravel'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandNodejs />}
|
|
||||||
text='NodeJS'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandReact />}
|
|
||||||
text='React'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandVue />}
|
|
||||||
text='Vue'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandTailwind />}
|
|
||||||
text='Tailwind'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandSass />}
|
|
||||||
text='Sass'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandMantine />}
|
|
||||||
text='Mantine'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandNextjs />}
|
|
||||||
text='NextJS'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandPrisma />}
|
|
||||||
text='Prisma'
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
icon={<IconBrandSupabase />}
|
|
||||||
text='Supabase'
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
<Text
|
|
||||||
mt='xl'
|
|
||||||
fs='italic'
|
|
||||||
ta='center'>
|
|
||||||
And probably many more...
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</SlideUpWhenVisible>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
.background {
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
background-image: url('/grid.svg');
|
|
||||||
background-repeat: repeat;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.4;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
width: 4rem;
|
|
||||||
height: 0.125rem;
|
|
||||||
background-color: var(--mantine-color-coffee-8);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import { Box, Button, Flex, Group, Text, Title } from '@mantine/core'
|
|
||||||
import { IconBrandGithub, IconMail } from '@tabler/icons-react'
|
|
||||||
import classes from './Introduction.module.css'
|
|
||||||
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
|
|
||||||
|
|
||||||
function Introduction() {
|
|
||||||
return (
|
|
||||||
<SlideUpWhenVisible>
|
|
||||||
<Box
|
|
||||||
mt='xl'
|
|
||||||
mah='100vh'
|
|
||||||
h='100vh'
|
|
||||||
className={classes.background}>
|
|
||||||
<Flex
|
|
||||||
align='center'
|
|
||||||
gap='md'>
|
|
||||||
<Box className={classes.line} />
|
|
||||||
<Text
|
|
||||||
fz='display-lg'
|
|
||||||
fw='bold'
|
|
||||||
c='coffee.6'>
|
|
||||||
Hi There!
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
<Title
|
|
||||||
variant='shadow'
|
|
||||||
fz='display-xl'
|
|
||||||
fw='bold'
|
|
||||||
lh={1.2}>
|
|
||||||
I'm Refansa.
|
|
||||||
</Title>
|
|
||||||
<Text
|
|
||||||
fz='display-lg'
|
|
||||||
fw='bold'
|
|
||||||
lh='xs'>
|
|
||||||
A Passionate, self-taught Software Developer{' '}
|
|
||||||
<Text
|
|
||||||
component='span'
|
|
||||||
display='block'
|
|
||||||
inherit
|
|
||||||
c='dark.1'>
|
|
||||||
A supporter of Open Source Software.
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Group mt={'xl'}>
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
href='https://github.com/Refansa'
|
|
||||||
target='_blank'
|
|
||||||
leftSection={<IconBrandGithub />}
|
|
||||||
size='lg'
|
|
||||||
variant='light'>
|
|
||||||
Github
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
href='mailto:m.refansa.am@gmail.com'
|
|
||||||
leftSection={<IconMail />}
|
|
||||||
size='lg'
|
|
||||||
variant='light'>
|
|
||||||
Email
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
</SlideUpWhenVisible>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Introduction
|
|
@ -1,126 +0,0 @@
|
|||||||
import {
|
|
||||||
AspectRatio,
|
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { IconBrandGithub } from '@tabler/icons-react'
|
|
||||||
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
|
|
||||||
import NextImage from 'next/image'
|
|
||||||
|
|
||||||
export interface ProjectItemProps {
|
|
||||||
alt: string
|
|
||||||
codeSrc?: any
|
|
||||||
description: string
|
|
||||||
imgSrc?: any
|
|
||||||
tags?: string[]
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectItem = ({
|
|
||||||
alt,
|
|
||||||
codeSrc,
|
|
||||||
description,
|
|
||||||
imgSrc,
|
|
||||||
tags,
|
|
||||||
title,
|
|
||||||
}: ProjectItemProps) => {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
shadow='sm'
|
|
||||||
padding='lg'
|
|
||||||
withBorder>
|
|
||||||
<Card.Section>
|
|
||||||
<AspectRatio
|
|
||||||
ratio={1366 / 609}
|
|
||||||
mah={160}
|
|
||||||
my='auto'>
|
|
||||||
<Image
|
|
||||||
component={NextImage}
|
|
||||||
src={imgSrc}
|
|
||||||
alt={alt}
|
|
||||||
width={340}
|
|
||||||
height={160}
|
|
||||||
/>
|
|
||||||
</AspectRatio>
|
|
||||||
</Card.Section>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
fw='bold'
|
|
||||||
size='lg'
|
|
||||||
mt='md'>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{tags ? (
|
|
||||||
<Group
|
|
||||||
gap='xs'
|
|
||||||
my='md'>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={title}
|
|
||||||
radius='xs'
|
|
||||||
variant='light'>
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
) : null}
|
|
||||||
<Text
|
|
||||||
size='sm'
|
|
||||||
c='dimmed'
|
|
||||||
mb='md'>
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
<Group mt='auto'>
|
|
||||||
{codeSrc ? (
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
href={codeSrc}
|
|
||||||
target='_blank'
|
|
||||||
leftSection={<IconBrandGithub />}
|
|
||||||
variant='filled'
|
|
||||||
size='lg'>
|
|
||||||
Source Code
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Projects({
|
|
||||||
projectLists,
|
|
||||||
}: {
|
|
||||||
projectLists: ProjectItemProps[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SlideUpWhenVisible>
|
|
||||||
<Box my='xl'>
|
|
||||||
<Stack align='center'>
|
|
||||||
<Title ta='center'>My Projects</Title>
|
|
||||||
<SimpleGrid
|
|
||||||
mt='xl'
|
|
||||||
cols={{ base: 1, sm: 2, md: 3 }}>
|
|
||||||
{projectLists.map((project) => {
|
|
||||||
return (
|
|
||||||
<ProjectItem
|
|
||||||
key={project.title}
|
|
||||||
{...project}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</SlideUpWhenVisible>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
.button {
|
|
||||||
transition: all linear 150ms;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(calc(0.9 * var(--mantine-scale)));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { Button as Component } from '@mantine/core'
|
|
||||||
import classes from './Button.module.css'
|
|
||||||
|
|
||||||
export const Button = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
className: classes.button,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,7 +0,0 @@
|
|||||||
.card {
|
|
||||||
@mixin dark {
|
|
||||||
&[data-with-border='true'] {
|
|
||||||
border-color: var(--mantine-color-dark-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { Card as Component } from '@mantine/core'
|
|
||||||
import classes from './Card.module.css'
|
|
||||||
|
|
||||||
export const Card = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
className: classes.card,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,15 +0,0 @@
|
|||||||
.title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-8);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Drawer as Component } from '@mantine/core'
|
|
||||||
import classes from './Drawer.module.css'
|
|
||||||
|
|
||||||
export const Drawer = Component.extend({
|
|
||||||
classNames: {
|
|
||||||
title: classes.title,
|
|
||||||
header: classes.header,
|
|
||||||
content: classes.content,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,5 +0,0 @@
|
|||||||
.root {
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-8);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { Paper as Component } from '@mantine/core'
|
|
||||||
import classes from './Paper.module.css'
|
|
||||||
|
|
||||||
export const Paper = Component.extend({
|
|
||||||
classNames: {
|
|
||||||
root: classes.root,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,13 +0,0 @@
|
|||||||
.text {
|
|
||||||
@mixin dark {
|
|
||||||
&[data-variant='curvy'] {
|
|
||||||
text-decoration-style: wavy;
|
|
||||||
text-decoration-color: var(--mantine-color-coffee-4);
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-variant='shadow'] {
|
|
||||||
text-shadow: 0.25rem 0.25rem var(--mantine-color-dark-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Text as Component } from '@mantine/core'
|
|
||||||
|
|
||||||
import classes from './Text.module.css'
|
|
||||||
|
|
||||||
export const Text = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
className: classes.text,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-6);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { TextInput as Component } from '@mantine/core'
|
|
||||||
|
|
||||||
import classes from './TextInput.module.css'
|
|
||||||
|
|
||||||
export const TextInput = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
classNames: {
|
|
||||||
input: classes.input,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-6);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Textarea as Component } from '@mantine/core'
|
|
||||||
|
|
||||||
import classes from './Textarea.module.css'
|
|
||||||
|
|
||||||
export const Textarea = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
classNames: {
|
|
||||||
input: classes.input,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,13 +0,0 @@
|
|||||||
.title {
|
|
||||||
@mixin dark {
|
|
||||||
&[data-variant='curvy'] {
|
|
||||||
text-decoration-style: wavy;
|
|
||||||
text-decoration-color: var(--mantine-color-coffee-4);
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-variant='shadow'] {
|
|
||||||
text-shadow: 0.25rem 0.25rem var(--mantine-color-dark-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Title as Component } from '@mantine/core'
|
|
||||||
|
|
||||||
import classes from './Title.module.css'
|
|
||||||
|
|
||||||
export const Title = Component.extend({
|
|
||||||
defaultProps: {
|
|
||||||
className: classes.title,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,8 +0,0 @@
|
|||||||
export { Button } from './Button'
|
|
||||||
export { Card } from './Card'
|
|
||||||
export { Drawer } from './Drawer'
|
|
||||||
export { Paper } from './Paper'
|
|
||||||
export { Text } from './Text'
|
|
||||||
export { Textarea } from './Textarea'
|
|
||||||
export { TextInput } from './TextInput'
|
|
||||||
export { Title } from './Title'
|
|
77
src/styles/globals.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 33 44% 22%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 33 44% 42%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 10%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary: 33 44% 52%;
|
||||||
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 33 44% 52%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply scroll-smooth;
|
||||||
|
scrollbar-color: hsl(var(--primary)) hsl(var(--secondary));
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground/90;
|
||||||
|
}
|
||||||
|
*::selection {
|
||||||
|
@apply bg-primary/20;
|
||||||
|
}
|
||||||
|
}
|
@ -1,180 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {
|
|
||||||
CSSVariablesResolver,
|
|
||||||
MantineColorsTuple,
|
|
||||||
createTheme,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { fluidDisplay, fluidTypography } from '../utils/typography'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Drawer,
|
|
||||||
Paper,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Textarea,
|
|
||||||
Title,
|
|
||||||
} from './extends'
|
|
||||||
|
|
||||||
const coffee: MantineColorsTuple = [
|
|
||||||
'#fff4e5',
|
|
||||||
'#f4e7d8',
|
|
||||||
'#e5cfb5',
|
|
||||||
'#d4b590',
|
|
||||||
'#c59e6f',
|
|
||||||
'#bd905a',
|
|
||||||
'#ba894f',
|
|
||||||
'#a3763e',
|
|
||||||
'#926835',
|
|
||||||
'#805928',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const theme = createTheme({
|
|
||||||
// Controls focus ring styles. Supports the following options:
|
|
||||||
// - `auto` – focus ring is displayed only when the user navigates with keyboard (default value)
|
|
||||||
// - `always` – focus ring is displayed when the user navigates with keyboard and mouse
|
|
||||||
// - `never` – focus ring is always hidden (not recommended)
|
|
||||||
//
|
|
||||||
focusRing: 'auto',
|
|
||||||
|
|
||||||
// rem units scale, change if you customize font-size of `<html />` element
|
|
||||||
// default value is `1` (for `100%`/`16px` font-size on `<html />`)
|
|
||||||
//
|
|
||||||
scale: 1,
|
|
||||||
|
|
||||||
// Determines whether `font-smoothing` property should be set on the body, `true` by default
|
|
||||||
fontSmoothing: true,
|
|
||||||
|
|
||||||
// White color
|
|
||||||
white: '#ffffff',
|
|
||||||
|
|
||||||
// Black color
|
|
||||||
black: '#000000',
|
|
||||||
|
|
||||||
// Object of colors, key is color name, value is an array of at least 10 strings (colors)
|
|
||||||
colors: {
|
|
||||||
coffee,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Index of theme.colors[color].
|
|
||||||
// Primary shade is used in all components to determine which color from theme.colors[color] should be used.
|
|
||||||
// Can be either a number (0–9) or an object to specify different color shades for light and dark color schemes.
|
|
||||||
// Default value `{ light: 6, dark: 8 }`
|
|
||||||
//
|
|
||||||
// For example,
|
|
||||||
// { primaryShade: 6 } // shade 6 is used both for dark and light color schemes
|
|
||||||
// { primaryShade: { light: 6, dark: 7 } } // different shades for dark and light color schemes
|
|
||||||
//
|
|
||||||
primaryShade: { light: 4, dark: 8 },
|
|
||||||
|
|
||||||
// Key of `theme.colors`, hex/rgb/hsl values are not supported.
|
|
||||||
// Determines which color will be used in all components by default.
|
|
||||||
// Default value – `blue`.
|
|
||||||
//
|
|
||||||
primaryColor: 'coffee',
|
|
||||||
|
|
||||||
// Function to resolve colors based on variant.
|
|
||||||
// Can be used to deeply customize how colors are applied to `Button`, `ActionIcon`, `ThemeIcon`
|
|
||||||
// and other components that use colors from theme.
|
|
||||||
//
|
|
||||||
// variantColorResolver: VariantColorsResolver;
|
|
||||||
|
|
||||||
// font-family used in all components, system fonts by default
|
|
||||||
fontFamily:
|
|
||||||
'Poppins, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji',
|
|
||||||
|
|
||||||
// Monospace font-family, used in code and other similar components, system fonts by default
|
|
||||||
fontFamilyMonospace:
|
|
||||||
'Fira Code, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
|
|
||||||
|
|
||||||
// Controls various styles of h1-h6 elements, used in TypographyStylesProvider and Title components
|
|
||||||
headings: {
|
|
||||||
fontFamily:
|
|
||||||
'Gabarito, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Object of values that are used to set `border-radius` in all components that support it
|
|
||||||
// radius: MantineRadiusValues;
|
|
||||||
|
|
||||||
// Key of `theme.radius` or any valid CSS value. Default `border-radius` used by most components
|
|
||||||
defaultRadius: 0,
|
|
||||||
|
|
||||||
// Object of values that are used to set various CSS properties that control spacing between elements
|
|
||||||
// spacing: MantineSpacingValues;
|
|
||||||
|
|
||||||
// Object of values that are used to control `font-size` property in all components
|
|
||||||
fontSizes: {
|
|
||||||
'fluid-xs': fluidTypography(8, 12, 480, 1440),
|
|
||||||
'fluid-sm': fluidTypography(10, 14, 480, 1440),
|
|
||||||
'fluid-md': fluidTypography(12, 16, 480, 1440),
|
|
||||||
'fluid-lg': fluidTypography(14, 18, 480, 1440),
|
|
||||||
'fluid-xl': fluidTypography(16, 20, 480, 1440),
|
|
||||||
'display-xl': fluidDisplay(80, 144, 768, 1920),
|
|
||||||
'display-lg': fluidDisplay(24, 36, 768, 1920),
|
|
||||||
'display-md': fluidDisplay(16, 24, 768, 1920),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Object of values that are used to control `line-height` property in `Text` component
|
|
||||||
// lineHeights: MantineLineHeightValues;
|
|
||||||
|
|
||||||
// Object of values that are used to control breakpoints in all components,
|
|
||||||
// values are expected to be defined in em
|
|
||||||
//
|
|
||||||
// breakpoints: MantineBreakpointsValues;
|
|
||||||
|
|
||||||
// Object of values that are used to add `box-shadow` styles to components that support `shadow` prop
|
|
||||||
// shadows: MantineShadowsValues;
|
|
||||||
|
|
||||||
// Determines whether user OS settings to reduce motion should be respected, `false` by default
|
|
||||||
// respectReducedMotion: boolean;
|
|
||||||
|
|
||||||
// Determines which cursor type will be used for interactive elements
|
|
||||||
// - `default` – cursor that is used by native HTML elements, for example, `input[type="checkbox"]` has `cursor: default` styles
|
|
||||||
// - `pointer` – sets `cursor: pointer` on interactive elements that do not have these styles by default
|
|
||||||
//
|
|
||||||
cursorType: 'pointer',
|
|
||||||
|
|
||||||
// Default gradient configuration for components that support `variant="gradient"`
|
|
||||||
// defaultGradient: MantineGradient;
|
|
||||||
|
|
||||||
// Class added to the elements that have active styles, for example, `Button` and `ActionIcon`
|
|
||||||
// activeClassName: string;
|
|
||||||
|
|
||||||
// Class added to the elements that have focus styles, for example, `Button` or `ActionIcon`.
|
|
||||||
// Overrides `theme.focusRing` property.
|
|
||||||
//
|
|
||||||
// focusClassName: string;
|
|
||||||
|
|
||||||
// Allows adding `classNames`, `styles` and `defaultProps` to any component
|
|
||||||
components: {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Drawer,
|
|
||||||
Paper,
|
|
||||||
Text,
|
|
||||||
Textarea,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Any other properties that you want to access with the theme objects
|
|
||||||
other: {
|
|
||||||
bodyLight: '#ffffff',
|
|
||||||
textLight: '#000000',
|
|
||||||
bodyDark: '#000000',
|
|
||||||
textDark: '#ffffff',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const resolver: CSSVariablesResolver = (t) => ({
|
|
||||||
variables: {},
|
|
||||||
light: {
|
|
||||||
'--mantine-color-body': t.other.bodyLight,
|
|
||||||
'--mantine-color-text': t.other.textLight,
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
'--mantine-color-body': t.other.bodyDark,
|
|
||||||
'--mantine-color-text': t.other.textDark,
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,58 +0,0 @@
|
|||||||
export const PIXEL_PER_REM = 16
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a number from pixel to rem.
|
|
||||||
*
|
|
||||||
* @param value The value in pixel.
|
|
||||||
*/
|
|
||||||
export const rem = (value: number): number => {
|
|
||||||
return value / PIXEL_PER_REM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string of calculated value used for font size in display typography.
|
|
||||||
*
|
|
||||||
* Formula based from https://github.com/abdulrcs/abdulrahman.id/blob/main/styles/theme.js
|
|
||||||
*
|
|
||||||
* @param minFont Minimal font size in pixel.
|
|
||||||
* @param maxFont Maximum font size in pixel.
|
|
||||||
* @param minVW Minimum font size in pixel.
|
|
||||||
* @param maxVW Maximum font size in pixel.
|
|
||||||
*/
|
|
||||||
export const fluidDisplay = (
|
|
||||||
minFont: number,
|
|
||||||
maxFont: number,
|
|
||||||
minVW: number,
|
|
||||||
maxVW: number,
|
|
||||||
) => {
|
|
||||||
let XX = minVW / 100
|
|
||||||
let YY = (100 * (maxFont - minFont)) / (maxVW - minVW)
|
|
||||||
let ZZ = rem(minFont)
|
|
||||||
return `calc(${ZZ}rem + ((1vw - ${XX}px) * ${YY}))`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string of clamp value used for font size in normal typography.
|
|
||||||
*
|
|
||||||
* Formula based from https://www.aleksandrhovhannisyan.com/blog/fluid-type-scale-with-css-clamp/
|
|
||||||
*
|
|
||||||
* @param minFont Minimal font size in pixel.
|
|
||||||
* @param maxFont Maximum font size in pixel.
|
|
||||||
* @param minVW Minimum font size in pixel.
|
|
||||||
* @param maxVW Maximum font size in pixel.
|
|
||||||
*/
|
|
||||||
export const fluidTypography = (
|
|
||||||
minFont: number,
|
|
||||||
maxFont: number,
|
|
||||||
minVW: number,
|
|
||||||
maxVW: number,
|
|
||||||
): string => {
|
|
||||||
const slope = (maxFont - minFont) / (maxVW - minVW)
|
|
||||||
const intercept = rem(minFont - slope * minVW)
|
|
||||||
const slopeVW = slope * 100
|
|
||||||
|
|
||||||
const minSize = rem(minFont)
|
|
||||||
const maxSize = rem(maxFont)
|
|
||||||
|
|
||||||
return `clamp(${minSize}rem, ${slopeVW}vw + ${intercept}rem, ${maxSize}rem)`
|
|
||||||
}
|
|
81
tailwind.config.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
import TailwindCSSAnimate from 'tailwindcss-animate'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
darkMode: ['class'],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx,mdx}',
|
||||||
|
'./components/**/*.{ts,tsx,mdx}',
|
||||||
|
'./app/**/*.{ts,tsx,mdx}',
|
||||||
|
'./src/**/*.{ts,tsx,mdx}',
|
||||||
|
],
|
||||||
|
prefix: '',
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: { height: '0' },
|
||||||
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
to: { height: '0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [TailwindCSSAnimate],
|
||||||
|
} satisfies Config
|
||||||
|
|
||||||
|
export default config
|
@ -1,19 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
@ -22,15 +16,11 @@
|
|||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|