Compare commits
No commits in common. "master" and "v1" have entirely different histories.
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
APP_NAME=portfolio
|
||||||
|
APP_LANG=en_US
|
||||||
|
APP_URL=https://refansa.vercel.app
|
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
APP_NAME=
|
||||||
|
APP_LANG=
|
@ -1,84 +1,7 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"extends": ["next/core-web-vitals", "prettier"],
|
||||||
"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": {
|
||||||
"import/no-extraneous-dependencies": [
|
"react/no-unescaped-entities": "off",
|
||||||
"error",
|
"@next/next/no-page-custom-font": "off"
|
||||||
{
|
|
||||||
"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
@ -1,128 +0,0 @@
|
|||||||
# 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,7 +4,6 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.yarn/install-state.gz
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@ -24,6 +23,7 @@
|
|||||||
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 +0,0 @@
|
|||||||
pnpm lint
|
|
53
.prettierignore
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# 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 @@
|
|||||||
{
|
{
|
||||||
"overrides": [
|
"printWidth": 80,
|
||||||
{
|
"tabWidth": 2,
|
||||||
"files": [
|
|
||||||
"**/*.{ts,tsx,js,mjs}"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"printWidth": 100,
|
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true
|
"singleQuote": true,
|
||||||
}
|
"quoteProps": "as-needed",
|
||||||
}
|
"jsxSingleQuote": true,
|
||||||
]
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"singleAttributePerLine": true
|
||||||
}
|
}
|
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"discord.enabled": false
|
||||||
|
}
|
21
LICENSE.md
@ -1,21 +0,0 @@
|
|||||||
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,19 +1,5 @@
|
|||||||
> [!NOTE]
|
# Mantine Next Template
|
||||||
> Mirrorred from gitea.tumbleweed.my.id/refansa/refansa.my.id
|
|
||||||
|
|
||||||
# Site - [refansa.my.id](https://refansa.my.id)
|
Get started with the template by clicking `Use this template` button on the top of the page.
|
||||||
|
|
||||||
The source code of my frontend website, [refansa.my.id](https://refansa.my.id)
|
[Documentation](https://mantine.dev/guides/next/)
|
||||||
|
|
||||||
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.
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
7
next.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
@ -1,4 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {}
|
|
||||||
|
|
||||||
export default nextConfig
|
|
71
package.json
@ -1,63 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "refansa.my.id",
|
"name": "portfolio",
|
||||||
"version": "0.0.1c",
|
"version": "0.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://refansa.my.id",
|
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Muhammad Refansa Ali Muzky",
|
"name": "Refansa",
|
||||||
"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",
|
||||||
"prepare": "husky"
|
"prettier": "prettier . \"./**/*.{js,jsx,ts,tsx}\" --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@icons-pack/react-simple-icons": "^9.6.0",
|
"@mantine/core": "7.1.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@mantine/hooks": "7.1.3",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@tabler/icons-react": "^2.39.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"framer-motion": "^10.16.4",
|
||||||
"@react-spring/web": "^9.7.4",
|
"next": "13.4.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"react": "18.2.0",
|
||||||
"clsx": "^2.1.1",
|
"react-dom": "18.2.0",
|
||||||
"gray-matter": "^4.0.3",
|
"react-intersection-observer": "^9.5.2"
|
||||||
"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/mdx": "^2.0.13",
|
"@types/node": "20.2.5",
|
||||||
"@types/node": "^20",
|
"@types/react": "18.2.7",
|
||||||
"@types/react": "^18",
|
"@types/react-dom": "18.2.4",
|
||||||
"@types/react-dom": "^18",
|
"eslint": "8.41.0",
|
||||||
"@types/slug": "^5.0.8",
|
"eslint-config-next": "13.4.4",
|
||||||
"eslint": "^8",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-config-next": "14.2.5",
|
"postcss": "^8.4.31",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"postcss-preset-mantine": "1.8.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"eslint-plugin-github": "^5.0.1",
|
"prettier": "3.0.3",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"typescript": "5.0.4"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
11587
pnpm-lock.yaml
14
postcss.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
1
public/assets/hand-coding.svg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
public/assets/project-1.png
Normal file
After Width: | Height: | Size: 710 KiB |
BIN
public/assets/project-2.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
public/assets/project-3.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
public/assets/project-4.png
Normal file
After Width: | Height: | Size: 91 KiB |
1
public/favicon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 937 B |
4
public/grid.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 1.6 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 629 B |
6
src/app/api/projects/route.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import data from '../../../data/projectList.json'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(data)
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 25 KiB |
3
src/app/global.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
@ -1,56 +1,69 @@
|
|||||||
import '@/styles/globals.css'
|
import { MantineProvider, ColorSchemeScript } from '@mantine/core'
|
||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
import type { Metadata, Viewport } from 'next'
|
import './global.css'
|
||||||
import { ReactNode } from 'react'
|
import '@mantine/core/styles.css'
|
||||||
|
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: 'Muhammad Refansa Ali Muzky',
|
name: 'Refansa',
|
||||||
url: 'https://refansa.my.id',
|
url: 'https://github.com/Refansa',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
creator: 'Muhammad Refansa Ali Muzky',
|
title: {
|
||||||
|
template: 'Refansa - Software Developer | %s',
|
||||||
|
default: 'Refansa - Software Developer',
|
||||||
|
},
|
||||||
|
description: "Refansa's portfolio website",
|
||||||
|
viewport: 'width=device-width, initial-scale=1',
|
||||||
|
colorScheme: 'dark',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export default function RootLayout({
|
||||||
themeColor: [
|
children,
|
||||||
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
}: {
|
||||||
{ media: '(prefers-color-scheme: dark)', color: 'black' },
|
children: React.ReactNode
|
||||||
],
|
}) {
|
||||||
}
|
|
||||||
|
|
||||||
export interface RootLayoutProps {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang={process.env.APP_LANG ?? 'en'}>
|
||||||
<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>
|
||||||
<ThemeProvider
|
<MantineProvider
|
||||||
attribute="class"
|
defaultColorScheme={'dark'}
|
||||||
defaultTheme="system"
|
forceColorScheme={'dark'}
|
||||||
enableSystem
|
cssVariablesResolver={resolver}
|
||||||
disableTransitionOnChange
|
theme={theme}>
|
||||||
>
|
{children}
|
||||||
<TooltipProvider>
|
</MantineProvider>
|
||||||
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
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,16 +1,34 @@
|
|||||||
import DefaultLayout from '@/components/layouts/default-layout'
|
'use client'
|
||||||
import IntroductionSection from '@/components/blocks/home/introduction-section'
|
|
||||||
import AboutSection from '@/components/blocks/home/about-section'
|
|
||||||
import ContactSection from '@/components/blocks/home/contact-section'
|
|
||||||
|
|
||||||
export default function Home() {
|
import { AppShell, Container, Stack } from '@mantine/core'
|
||||||
|
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 (
|
||||||
<DefaultLayout>
|
<AppShell
|
||||||
<main className="flex flex-col gap-24 mb-24">
|
withBorder={false}
|
||||||
<IntroductionSection />
|
padding={'xl'}
|
||||||
<AboutSection />
|
header={{ height: 72 }}
|
||||||
<ContactSection />
|
className={'shell-root'}>
|
||||||
</main>
|
<AppShell.Header>
|
||||||
</DefaultLayout>
|
<Navbar />
|
||||||
|
</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,18 +1,17 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
|
import Project from './project'
|
||||||
import DefaultLayout from '@/components/layouts/default-layout'
|
import data from '../../data/projectList.json'
|
||||||
import UnderConstruction from '@/components/blocks/error/under-construction'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Projects',
|
title: 'Projects',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Projects() {
|
async function getProjectLists() {
|
||||||
return (
|
return data
|
||||||
<DefaultLayout>
|
}
|
||||||
<main className="flex justify-center items-center h-[85vh]">
|
|
||||||
<UnderConstruction />
|
export default async function Projects() {
|
||||||
</main>
|
const projectLists = await getProjectLists()
|
||||||
</DefaultLayout>
|
|
||||||
)
|
return <Project projectLists={projectLists} />
|
||||||
}
|
}
|
||||||
|
34
src/app/projects/project.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
40
src/components/Footer.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
3
src/components/NavIcon.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.bracket {
|
||||||
|
color: var(--mantine-color-dark-4);
|
||||||
|
}
|
21
src/components/NavIcon.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
|
24
src/components/NavLink.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
|
35
src/components/NavMobile.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
8
src/components/Navbar.module.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
padding-inline: 3vw;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 1px solid var(--mantine-color-dark-7);
|
||||||
|
}
|
29
src/components/Navbar.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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
|
@ -1,27 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
'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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import HeaderNavigation from './header-navigation'
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 backdrop-blur-xl bg-background/80">
|
|
||||||
<HeaderNavigation />
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
'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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
'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>
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
'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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
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 }
|
|
@ -1,86 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
'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,
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
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 }
|
|
@ -1,30 +0,0 @@
|
|||||||
'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 }
|
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
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>.
|
|
49
src/data/projectList.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
35
src/hooks/slideUpWhenVisible.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
import { type ClassValue, clsx } from 'clsx'
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs))
|
|
||||||
}
|
|
3
src/partials/About.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.grayscale {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
77
src/partials/About.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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
|
142
src/partials/Experiences.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
20
src/partials/Introduction.module.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.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;
|
||||||
|
}
|
69
src/partials/Introduction.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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
|
126
src/partials/Projects.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
7
src/styles/extends/Button.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.button {
|
||||||
|
transition: all linear 150ms;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(calc(0.9 * var(--mantine-scale)));
|
||||||
|
}
|
||||||
|
}
|
8
src/styles/extends/Button.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Button as Component } from '@mantine/core'
|
||||||
|
import classes from './Button.module.css'
|
||||||
|
|
||||||
|
export const Button = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
className: classes.button,
|
||||||
|
},
|
||||||
|
})
|
7
src/styles/extends/Card.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.card {
|
||||||
|
@mixin dark {
|
||||||
|
&[data-with-border='true'] {
|
||||||
|
border-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/styles/extends/Card.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Card as Component } from '@mantine/core'
|
||||||
|
import classes from './Card.module.css'
|
||||||
|
|
||||||
|
export const Card = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
className: classes.card,
|
||||||
|
},
|
||||||
|
})
|
15
src/styles/extends/Drawer.module.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
10
src/styles/extends/Drawer.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
5
src/styles/extends/Paper.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.root {
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
8
src/styles/extends/Paper.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Paper as Component } from '@mantine/core'
|
||||||
|
import classes from './Paper.module.css'
|
||||||
|
|
||||||
|
export const Paper = Component.extend({
|
||||||
|
classNames: {
|
||||||
|
root: classes.root,
|
||||||
|
},
|
||||||
|
})
|
13
src/styles/extends/Text.module.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/styles/extends/Text.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Text as Component } from '@mantine/core'
|
||||||
|
|
||||||
|
import classes from './Text.module.css'
|
||||||
|
|
||||||
|
export const Text = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
className: classes.text,
|
||||||
|
},
|
||||||
|
})
|
5
src/styles/extends/TextInput.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.input {
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
}
|
11
src/styles/extends/TextInput.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { TextInput as Component } from '@mantine/core'
|
||||||
|
|
||||||
|
import classes from './TextInput.module.css'
|
||||||
|
|
||||||
|
export const TextInput = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
classNames: {
|
||||||
|
input: classes.input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
5
src/styles/extends/Textarea.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.input {
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
}
|
11
src/styles/extends/Textarea.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Textarea as Component } from '@mantine/core'
|
||||||
|
|
||||||
|
import classes from './Textarea.module.css'
|
||||||
|
|
||||||
|
export const Textarea = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
classNames: {
|
||||||
|
input: classes.input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
13
src/styles/extends/Title.module.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/styles/extends/Title.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Title as Component } from '@mantine/core'
|
||||||
|
|
||||||
|
import classes from './Title.module.css'
|
||||||
|
|
||||||
|
export const Title = Component.extend({
|
||||||
|
defaultProps: {
|
||||||
|
className: classes.title,
|
||||||
|
},
|
||||||
|
})
|
8
src/styles/extends/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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'
|
@ -1,77 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
180
src/styles/theme.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
'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,
|
||||||
|
},
|
||||||
|
})
|
58
src/utils/typography.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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)`
|
||||||
|
}
|
@ -1,81 +0,0 @@
|
|||||||
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,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"target": "es5",
|
||||||
|
"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": "bundler",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
@ -16,11 +22,15 @@
|
|||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|