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