Compare commits

..

No commits in common. "master" and "v1" have entirely different histories.
master ... v1

100 changed files with 5635 additions and 11452 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
APP_NAME=portfolio
APP_LANG=en_US
APP_URL=https://refansa.vercel.app

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
APP_NAME=
APP_LANG=

View File

@ -1,84 +1,7 @@
{
"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"
],
"extends": ["next/core-web-vitals", "prettier"],
"rules": {
"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/"]
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off"
}
}

View File

@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
m.refansa.am@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

2
.gitignore vendored
View File

@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
@ -24,6 +23,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local

View File

@ -1 +0,0 @@
pnpm lint

53
.prettierignore Normal file
View File

@ -0,0 +1,53 @@
# Using this project's .gitignore file as base.
#
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next
/out
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# configs
package.json
package-lock.json
next.config.js
pnpm-lock.yaml
postcss.config.js
tsconfig.json
yarn.lock
.eslintrc.json
.prettierrc.json
# public
/public
README.md

View File

@ -1,14 +1,14 @@
{
"overrides": [
{
"files": [
"**/*.{ts,tsx,js,mjs}"
],
"options": {
"printWidth": 100,
"semi": false,
"singleQuote": true
}
}
]
}
"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
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"discord.enabled": false
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Refansa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,19 +1,5 @@
> [!NOTE]
> Mirrorred from gitea.tumbleweed.my.id/refansa/refansa.my.id
# Mantine Next Template
# Site - [refansa.my.id](https://refansa.my.id)
Get started with the template by clicking `Use this template` button on the top of the page.
The source code of my frontend website, [refansa.my.id](https://refansa.my.id)
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.
[Documentation](https://mantine.dev/guides/next/)

View File

@ -1,17 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

7
next.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default nextConfig

View File

@ -1,63 +1,40 @@
{
"name": "refansa.my.id",
"version": "0.0.1c",
"name": "portfolio",
"version": "0.1.3",
"private": true,
"homepage": "https://refansa.my.id",
"author": {
"name": "Muhammad Refansa Ali Muzky",
"nickname": "Refansa",
"name": "Refansa",
"email": "m.refansa.am@gmail.com",
"url": "https://github.com/refansa"
},
"description": "A humble internet abode.",
"repository": {
"url": "https://github.com/refansa/refansa.my.id"
"url": "https://github.com/Refansa"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prepare": "husky"
"prettier": "prettier . \"./**/*.{js,jsx,ts,tsx}\" --write"
},
"dependencies": {
"@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"
"@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"
},
"devDependencies": {
"@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"
"@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"
}
}

File diff suppressed because it is too large Load Diff

14
postcss.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

View File

@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

BIN
public/assets/project-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

BIN
public/assets/project-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
public/assets/project-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
public/assets/project-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

1
public/favicon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 937 B

4
public/grid.svg Normal file
View File

@ -0,0 +1,4 @@
<svg id="download" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<path id="Path_3" data-name="Path 3" d="M96,95h4v1H96v4H95V96H86v4H85V96H76v4H75V96H66v4H65V96H56v4H55V96H46v4H45V96H36v4H35V96H26v4H25V96H16v4H15V96H0V95H15V86H0V85H15V76H0V75H15V66H0V65H15V56H0V55H15V46H0V45H15V36H0V35H15V26H0V25H15V16H0V15H15V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96Zm-1,0V86H86v9ZM85,95V86H76v9ZM75,95V86H66v9ZM65,95V86H56v9ZM55,95V86H46v9ZM45,95V86H36v9ZM35,95V86H26v9ZM25,95V86H16v9ZM16,85h9V76H16Zm10,0h9V76H26Zm10,0h9V76H36Zm10,0h9V76H46Zm10,0h9V76H56Zm10,0h9V76H66Zm10,0h9V76H76Zm10,0h9V76H86Zm9-10V66H86v9ZM85,75V66H76v9ZM75,75V66H66v9ZM65,75V66H56v9ZM55,75V66H46v9ZM45,75V66H36v9ZM35,75V66H26v9ZM25,75V66H16v9ZM16,65h9V56H16Zm10,0h9V56H26Zm10,0h9V56H36Zm10,0h9V56H46Zm10,0h9V56H56Zm10,0h9V56H66Zm10,0h9V56H76Zm10,0h9V56H86Zm9-10V46H86v9ZM85,55V46H76v9ZM75,55V46H66v9ZM65,55V46H56v9ZM55,55V46H46v9ZM45,55V46H36v9ZM35,55V46H26v9ZM25,55V46H16v9ZM16,45h9V36H16Zm10,0h9V36H26Zm10,0h9V36H36Zm10,0h9V36H46Zm10,0h9V36H56Zm10,0h9V36H66Zm10,0h9V36H76Zm10,0h9V36H86Zm9-10V26H86v9ZM85,35V26H76v9ZM75,35V26H66v9ZM65,35V26H56v9ZM55,35V26H46v9ZM45,35V26H36v9ZM35,35V26H26v9ZM25,35V26H16v9ZM16,25h9V16H16Zm10,0h9V16H26Zm10,0h9V16H36Zm10,0h9V16H46Zm10,0h9V16H56Zm10,0h9V16H66Zm10,0h9V16H76Zm10,0h9V16H86Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd" opacity="0.5"/>
<path id="Path_4" data-name="Path 4" d="M6,5V0H5V5H0V6H5v94H6V6h94V5Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,6 @@
import { NextResponse } from 'next/server'
import data from '../../../data/projectList.json'
export async function GET() {
return NextResponse.json(data)
}

View File

@ -1,20 +0,0 @@
import { ReactNode } from 'react'
import DefaultLayout from '@/components/layouts/default-layout'
export interface PostLayoutProps {
children: ReactNode
params: {
slug: string
}
}
export default function PostLayout({ children }: PostLayoutProps) {
return (
<DefaultLayout>
<main>
<article className="flex flex-col gap-4 text-xs md:text-base my-6">{children}</article>
</main>
</DefaultLayout>
)
}

View File

@ -1,127 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { components } from '@/components/mdx-components'
const ROOT_PATH = process.cwd()
const BLOG_PATH = path.join(ROOT_PATH, 'src', 'contents', 'posts')
export interface Props {
params: {
slug: string
}
}
type FrontMatterMetadata = {
siteTitle: string
postTitle: string
siteDescription: string
postDescription: string
publishedOn: string
updatedOn: string
isPublished: boolean
tags: string[]
}
export interface PostMetadata {
slug: string
frontMatter: FrontMatterMetadata
content: string
}
/**
* Get the post from file system, matching the provided slug from the route params.
*/
function getPost(slug: string): PostMetadata {
try {
const markdownFile = fs.readFileSync(path.join(BLOG_PATH, slug + '.mdx'), 'utf-8')
const { data: frontMatter, content } = matter(markdownFile)
if (!frontMatter.isPublished) {
throw new Error('Post is currently not published yet!')
}
return {
frontMatter: frontMatter as FrontMatterMetadata,
slug,
content,
}
} catch {
/*
* Catch any possible error (could be no slug exists, post has not been published,
* or something wrong with the fs) above and just render not found.
*/
notFound()
}
}
/**
* Statically generate routes at build time.
*
* See: <https://nextjs.org/docs/app/api-reference/functions/generate-static-params>
*/
export async function generateStaticParams(): Promise<string[]> {
const files = fs.readdirSync(BLOG_PATH)
const slugPaths = files.map((filename) => filename.replace('.mdx', ''))
return slugPaths
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = params
const post = getPost(slug)
return {
title: post.frontMatter.siteTitle,
description: post.frontMatter.siteDescription,
}
}
export default function Post({ params }: Props) {
const { slug } = params
const post = getPost(slug)
return (
<main className="flex flex-col gap-4 my-8">
<div id="post-detail" className="flex flex-col gap-2 mb-2">
<h1 id="post-title" className="text-3xl md:text-4xl font-bold">
{post.frontMatter.postTitle}
</h1>
<span id="post-description" className="text-base md:text-lg">
{post.frontMatter.postDescription}
</span>
<div id="post-tags" className="flex gap-2 mb-4">
{post.frontMatter.tags.map((tag: string, index: number) => {
return (
<span key={index} className="px-2 py-1 rounded-lg bg-primary/30 text-xs">
#{tag}
</span>
)
})}
</div>
<div id="post-publication-date" className="flex gap-2 font-bold text-xs">
<span>PUBLISHED ON: {post.frontMatter.publishedOn}</span>
{post.frontMatter.updatedOn !== post.frontMatter.publishedOn ? (
<>
<span>|</span>
<span>UPDATED ON: {post.frontMatter.updatedOn}</span>
</>
) : null}
</div>
</div>
<hr />
<article id="post-content" className="flex flex-col gap-4">
<MDXRemote source={post.content} components={components} />
</article>
</main>
)
}

View File

@ -1,18 +0,0 @@
import { Metadata } from 'next'
import DefaultLayout from '@/components/layouts/default-layout'
import UnderConstruction from '@/components/blocks/error/under-construction'
export const metadata: Metadata = {
title: 'Blog',
}
export default function Blog() {
return (
<DefaultLayout>
<main className="flex justify-center items-center h-[85vh]">
<UnderConstruction />
</main>
</DefaultLayout>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

3
src/app/global.css Normal file
View File

@ -0,0 +1,3 @@
html {
scroll-behavior: smooth;
}

View File

@ -1,56 +1,69 @@
import '@/styles/globals.css'
import { MantineProvider, ColorSchemeScript } from '@mantine/core'
import { Metadata } from 'next'
import type { Metadata, Viewport } from 'next'
import { ReactNode } from 'react'
import { siteConfig } from '@/config/site'
import { ThemeProvider } from '@/components/providers/theme-provider'
import { TooltipProvider } from '@/components/ui/tooltip'
import './global.css'
import '@mantine/core/styles.css'
import { resolver, theme } from '../styles/theme'
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: 'Muhammad Refansa Ali Muzky',
url: 'https://refansa.my.id',
name: 'Refansa',
url: 'https://github.com/Refansa',
},
],
creator: 'Muhammad Refansa Ali Muzky',
title: {
template: 'Refansa - Software Developer | %s',
default: 'Refansa - Software Developer',
},
description: "Refansa's portfolio website",
viewport: 'width=device-width, initial-scale=1',
colorScheme: 'dark',
}
export const viewport: Viewport = {
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) {
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<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>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TooltipProvider>
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
</TooltipProvider>
</ThemeProvider>
<MantineProvider
defaultColorScheme={'dark'}
forceColorScheme={'dark'}
cssVariablesResolver={resolver}
theme={theme}>
{children}
</MantineProvider>
</body>
</html>
)

View File

@ -1,18 +0,0 @@
import { Metadata } from 'next'
import DefaultLayout from '@/components/layouts/default-layout'
import PageNotFound from '@/components/blocks/error/page-not-found'
export const metadata: Metadata = {
title: 'You seem to be lost...',
}
export default function NotFound() {
return (
<DefaultLayout>
<main className="flex justify-center items-center h-[85vh]">
<PageNotFound />
</main>
</DefaultLayout>
)
}

View File

@ -1,16 +1,34 @@
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'
'use client'
export default function Home() {
import { AppShell, Container, Stack } from '@mantine/core'
import Introduction from '../partials/Introduction'
import About from '../partials/About'
import Navbar from '../components/Navbar'
import Footer from '../components/Footer'
import Experiences from '../partials/Experiences'
export default function HomePage() {
return (
<DefaultLayout>
<main className="flex flex-col gap-24 mb-24">
<IntroductionSection />
<AboutSection />
<ContactSection />
</main>
</DefaultLayout>
<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>
)
}

View File

@ -1,18 +1,17 @@
import { Metadata } from 'next'
import DefaultLayout from '@/components/layouts/default-layout'
import UnderConstruction from '@/components/blocks/error/under-construction'
import Project from './project'
import data from '../../data/projectList.json'
export const metadata: Metadata = {
title: 'Projects',
}
export default function Projects() {
return (
<DefaultLayout>
<main className="flex justify-center items-center h-[85vh]">
<UnderConstruction />
</main>
</DefaultLayout>
)
async function getProjectLists() {
return data
}
export default async function Projects() {
const projectLists = await getProjectLists()
return <Project projectLists={projectLists} />
}

View File

@ -0,0 +1,34 @@
'use client'
import { AppShell, Container, Stack } from '@mantine/core'
import Footer from '../../components/Footer'
import Navbar from '../../components/Navbar'
import Projects, { ProjectItemProps } from '../../partials/Projects'
export default function Project({
projectLists,
}: {
projectLists: ProjectItemProps[]
}) {
return (
<AppShell
withBorder={false}
padding={'xl'}
header={{ height: 72 }}
className={'shell-root'}>
<AppShell.Header>
<Navbar />
</AppShell.Header>
<AppShell.Main>
<Container
size={1920}
px={{ base: '0rem', lg: '4rem', xl: '8rem' }}>
<Stack>
<Projects projectLists={projectLists} />
<Footer />
</Stack>
</Container>
</AppShell.Main>
</AppShell>
)
}

40
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Anchor, Badge, Box, SimpleGrid, Stack, Text } from '@mantine/core'
import pkg from '../../package.json'
const { version } = pkg
export default function Footer() {
return (
<Box mt='xl'>
<SimpleGrid
cols={{ base: 1, sm: 2 }}
h={120}
spacing={{ base: 'sm', md: 'md', lg: 'lg' }}>
<Stack>
<Text fw='bold'>
Email:{' '}
<Anchor href='mailto:m.refansa.am@gmail.com'>
m.refansa.am@gmail.com
</Anchor>
</Text>
<Text fw='bold'>
Tel:{' '}
<Anchor href='tel:+62-812-8543-3284'>(+62) 812-8543-3284</Anchor>
</Text>
</Stack>
<Stack>{/* empty space */}</Stack>
</SimpleGrid>
<Stack align='center'>
<Badge variant='outline'>V. {version}</Badge>
<Text fw='bold'>
Created with by{' '}
<Anchor
fw='bold'
href='https://github.com/Refansa'>
Refansa
</Anchor>
</Text>
</Stack>
</Box>
)
}

View File

@ -0,0 +1,3 @@
.bracket {
color: var(--mantine-color-dark-4);
}

View File

@ -0,0 +1,21 @@
import { Anchor, Title } from '@mantine/core'
import Link from 'next/link'
import classes from './NavIcon.module.css'
function NavIcon() {
return (
<Anchor
component={Link}
href='/'
underline='never'>
<Title ff='Fira Code'>
<span className={classes.bracket}>{'{'}</span>
<span>R</span>
<span className={classes.bracket}>{'}'}</span>
</Title>
</Anchor>
)
}
export default NavIcon

View File

@ -0,0 +1,24 @@
import { Button } from '@mantine/core'
import Link from 'next/link'
import { MouseEventHandler } from 'react'
interface NavLinkProps {
content: string
href: string
onClick?: MouseEventHandler<HTMLElement>
}
function NavLink({ content, href, onClick }: NavLinkProps) {
return (
<Button
component={Link}
variant='subtle'
size='md'
onClick={onClick}
href={href}>
{content}
</Button>
)
}
export default NavLink

View File

@ -0,0 +1,35 @@
import { useDisclosure } from '@mantine/hooks'
import { Box, Burger, Drawer, Stack } from '@mantine/core'
import NavLink from './NavLink'
export default function NavMobile() {
const [opened, { open, close }] = useDisclosure(false)
return (
<Box hiddenFrom='sm'>
<Drawer
opened={opened}
onClose={close}
overlayProps={{ backgroundOpacity: 0.5, blur: 1 }}
position='right'
size='xs'>
<Stack>
<NavLink
content='About'
href='/#about'
onClick={close}
/>
<NavLink
content='Projects'
href='/projects'
/>
</Stack>
</Drawer>
<Burger
opened={opened}
onClick={open}
aria-label='Toggle navigation'
/>
</Box>
)
}

View File

@ -0,0 +1,8 @@
.navigation {
display: flex;
padding-inline: 3vw;
align-items: center;
justify-content: space-between;
height: 100%;
border-bottom: 1px solid var(--mantine-color-dark-7);
}

29
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,29 @@
import { Box, Group } from '@mantine/core'
import classes from './Navbar.module.css'
import NavIcon from './NavIcon'
import NavLink from './NavLink'
import NavMobile from './NavMobile'
function Navbar() {
return (
<Box
component='nav'
className={classes.navigation}>
<NavIcon />
<Group visibleFrom='sm'>
<NavLink
content='About'
href='/'
/>
<NavLink
content='Projects'
href='/projects'
/>
</Group>
<NavMobile />
</Box>
)
}
export default Navbar

View File

@ -1,27 +0,0 @@
import Link from 'next/link'
import { UrlObject } from 'url'
import { HTMLAttributes } from 'react'
export interface Props extends HTMLAttributes<HTMLAnchorElement> {
href: string | UrlObject
/**
* Whether the type of the anchor is for external or internal links.
*
* @default false
*/
isExternal?: boolean
}
export default function Anchor({ href, isExternal = false, children, ...rest }: Props) {
return !isExternal ? (
<Link className="underline hover:text-foreground/80" href={href} {...rest}>
{children}
</Link>
) : (
<a className="underline hover:text-foreground/80" href={href.toString()} {...rest}>
{children}
</a>
)
}

View File

@ -1,21 +0,0 @@
import Link from 'next/link'
import { HomeIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function PageNotFound() {
return (
<div className="flex font-mono gap-4 flex-col items-center tracking-wide">
<span className="text-7xl md:text-9xl">404</span>
<i className="text-xl md:text-2xl">Not Found</i>
<p className="text-center">You are trying to access a page that doesn't exists.</p>
<Button className="font-sans font-bold" variant="secondary">
<Link className="flex gap-2 items-center" href="/">
<HomeIcon />
Go Home
</Link>
</Button>
</div>
)
}

View File

@ -1,21 +0,0 @@
import Link from 'next/link'
import { HomeIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function UnderConstruction() {
return (
<div className="flex font-mono gap-4 flex-col items-center tracking-wide">
<span className="text-7xl md:text-9xl">501</span>
<i className="text-xl md:text-2xl">Not Implemented</i>
<p className="text-center">Sorry! The page is currently under construction.</p>
<Button className="font-sans font-bold" variant="secondary">
<Link className="flex gap-2 items-center" href="/">
<HomeIcon />
Go Home
</Link>
</Button>
</div>
)
}

View File

@ -1,15 +0,0 @@
import Anchor from '@/components/anchor'
import Package from '../../../../package.json'
export default function Footer() {
return (
<footer className="flex flex-col gap-1 items-center mb-8 text-xs md:text-base">
<p className="font-semibold text-center">
Created with by{' '}
<Anchor href={Package.author.url} isExternal>
{Package.author.nickname}
</Anchor>
</p>
</footer>
)
}

View File

@ -1,90 +0,0 @@
'use client'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { usePathname } from 'next/navigation'
import { MenuIcon, XIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import NavigationItem from '@/components/blocks/header/navigation-item'
const Clock = dynamic(() => import('@/components/clock').then((mod) => mod.Clock), {
loading: () => <Skeleton className="md:w-52 w-[100px] h-7" />,
ssr: false,
})
const ThemeSwitch = dynamic(
() => import('@/components/theme-switch').then((mod) => mod.ThemeSwitch),
{
loading: () => <Skeleton className="w-10 h-10" />,
ssr: false,
},
)
export default function HeaderNavigation() {
const pathname = usePathname()
const smoothHeaderScroll = (e: any) => {
if (pathname === '/') {
e.preventDefault()
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}
return (
<nav className="flex h-16 p-2 items-center">
<div className="flex-1">
<Link href="/" className="font-bold text-xl" onClick={smoothHeaderScroll}>
Refansa
</Link>
</div>
<div className="flex-1 flex justify-center">
<Clock />
</div>
<div id="Desktop" className="flex-1 hidden md:flex gap-4 items-center justify-end">
<NavigationItem href="/blog">Blog</NavigationItem>
<NavigationItem href="/projects">Projects</NavigationItem>
<ThemeSwitch />
</div>
<div id="Mobile" className="flex-1 flex md:hidden justify-end">
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<MenuIcon />
</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle className="flex gap-2 justify-between px-2">
<ThemeSwitch starterId={10} />
<SheetClose asChild>
<Button variant="ghost" size="icon">
<XIcon />
</Button>
</SheetClose>
</SheetTitle>
<SheetDescription className="flex flex-col gap-2">
<NavigationItem href="/blog">Blog</NavigationItem>
<NavigationItem href="/projects">Projects</NavigationItem>
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
</div>
</nav>
)
}

View File

@ -1,9 +0,0 @@
import HeaderNavigation from './header-navigation'
export default function Header() {
return (
<header className="sticky top-0 backdrop-blur-xl bg-background/80">
<HeaderNavigation />
</header>
)
}

View File

@ -1,20 +0,0 @@
import Link from 'next/link'
import { ReactNode } from 'react'
import { Button } from '@/components/ui/button'
export type Props = {
children: ReactNode
href: string
}
export default function NavigationItem({ children, href }: Props) {
return (
<Button asChild variant="ghost">
<Link href={href}>
<span className="font-bold">{children}</span>
</Link>
</Button>
)
}

View File

@ -1,40 +0,0 @@
import Anchor from '@/components/anchor'
import TermWord from '@/components/term-word'
import { Heading } from '@/components/ui/heading'
export default function AboutSection() {
return (
<section className="flex flex-col gap-4 tracking-wide leading-relaxed text-xs md:text-base">
<Heading level={3}>A bit about me</Heading>
<p>
I'm a Software Developer from Jakarta, Indonesia 🇮🇩,{' '}
<TermWord description="Nice to meet you!">Senang berkenalan denganmu!</TermWord>
</p>
<p>
This is my humble internet abode, where I sometimes <Anchor href="/blog">blog</Anchor> about
programming, software development, game development, and some 3D modeling in my daily work.
But I mainly do web development, so that's probably what you will commonly see.
</p>
<p>
I love nothing more than diving into complex projects, but that doesn't mean I admire
complexity over simplicity, quite the contrary in fact. It always amaze me how people turn a
complex problems into a simple, digestable format for a simpleton like me to understand.
</p>
<p>
As a supporter of open source, I believe that sharing knowledge and collaborating on
projects is essential for the advancement of technologies.
</p>
<p>
Oh! And before I forget, I always have this urge to say that{' '}
<em className="text-foreground/50">
I use{' '}
<TermWord description="Arch Linux, a lightweight and flexible Linux® distribution that tries to Keep It Simple.">
<em>arch</em>
</TermWord>{' '}
btw
</em>
.
</p>
</section>
)
}

View File

@ -1,20 +0,0 @@
import { siteConfig } from '@/config/site'
import { Heading } from '@/components/ui/heading'
import Anchor from '@/components/anchor'
export default function ContactSection() {
return (
<section className="flex flex-col gap-4 text-xs md:text-base tracking-wide leading-relaxed">
<Heading level={3}>Contact</Heading>
<div className="flex flex-col gap-2">
<span>
Email: <Anchor href={siteConfig.links.email}>{siteConfig.email}</Anchor>
</span>
<span>
Tel: <Anchor href={siteConfig.links.tel}>{siteConfig.tel}</Anchor>
</span>
</div>
</section>
)
}

View File

@ -1,51 +0,0 @@
import { SiGithub } from '@icons-pack/react-simple-icons'
import { Mail } from 'lucide-react'
import { siteConfig } from '@/config/site'
import { Button } from '@/components/ui/button'
import TermWord from '@/components/term-word'
export default function IntroductionSection() {
return (
<section className="flex flex-col py-24 gap-2">
<div className="flex items-center gap-2">
<div className="w-16 h-[2px] bg-primary" />
<span className="md:text-xl font-bold text-primary">Welcome, New & Old Friends!</span>
</div>
<span
className="text-5xl md:text-7xl font-bold"
style={{ textShadow: '3px 3px hsla(var(--primary) / 0.4)' }}
>
I'm Refansa
</span>
<div className="mt-4 flex flex-col">
<span className="text-lg md:text-2xl font-bold">
A Passionate,{' '}
<TermWord description="College is too expensive nowadays.">
<i>self-taught</i>
</TermWord>{' '}
Software Developer
</span>
<span className="text-lg md:text-2xl font-bold text-foreground/50">
And a Patron of Open Source Software.
</span>
</div>
<div className="mt-2 flex gap-4">
<Button className="text-lg font-bold flex gap-2" size="lg" asChild>
<a href={siteConfig.links.github}>
<SiGithub />
Github
</a>
</Button>
<Button className="text-lg font-bold flex gap-2" variant="secondary" size="lg" asChild>
<a href={siteConfig.links.email}>
<Mail />
Email
</a>
</Button>
</div>
</section>
)
}

View File

@ -1,35 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
export function Clock() {
const [time, setTime] = useState(
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
timeZone: 'Asia/Jakarta',
}),
)
useEffect(() => {
const timerInterval = setInterval(() => {
setTime(
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
timeZone: 'Asia/Jakarta',
}),
)
}, 1000)
return () => clearInterval(timerInterval)
}, [])
return (
<div className="font-bold text-lg md:text-xl">
<span>{time.format()}</span>
<span className="md:inline hidden"> - </span>
<span className="md:inline hidden">Jakarta</span>
</div>
)
}

View File

@ -1,18 +0,0 @@
import { ReactNode } from 'react'
import Header from '@/components/blocks/header/header'
import Footer from '@/components/blocks/footer/footer'
export interface Props {
children: ReactNode
}
export default function DefaultLayout({ children }: Props) {
return (
<div className="max-w-screen-lg w-full mx-auto px-6">
<Header />
{children}
<Footer />
</div>
)
}

View File

@ -1,46 +0,0 @@
import { MDXComponents } from 'mdx/types'
import Anchor from '@/components/anchor'
import TermWord from '@/components/term-word'
import { Heading } from '@/components/ui/heading'
export const components: MDXComponents = {
Anchor,
TermWord,
Heading,
p(props) {
return <p className="tracking-wide leading-relaxed">{props.children}</p>
},
h1(props) {
return <Heading level={1}>{props.children as string}</Heading>
},
h2(props) {
return <Heading level={2}>{props.children as string}</Heading>
},
h3(props) {
return <Heading level={3}>{props.children as string}</Heading>
},
h4(props) {
return <Heading level={4}>{props.children as string}</Heading>
},
h5(props) {
return <Heading level={5}>{props.children as string}</Heading>
},
h6(props) {
return <Heading level={6}>{props.children as string}</Heading>
},
code(props) {
return (
<code className="bg-gray-500/20 text-foreground/50 p-1 tracking-normal">
{props.children}
</code>
)
},
blockquote(props) {
return (
<blockquote className="bg-gray-500/20 my-3 px-4 py-6 border-l-8 border-gray-500/50">
{props.children}
</blockquote>
)
},
}

View File

@ -1,9 +0,0 @@
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -1,26 +0,0 @@
import { HTMLAttributes, ReactNode } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
export interface Props extends HTMLAttributes<HTMLSpanElement> {
children: ReactNode
/**
* The description of the term word.
*/
description: string
}
export default function TermWord({ children, description, ...rest }: Props) {
return (
<Tooltip>
<TooltipTrigger>
<span className="underline decoration-dashed underline-offset-2" {...rest}>
{children}
</span>
</TooltipTrigger>
<TooltipContent>
<span className="not-italic font-normal">{description}</span>
</TooltipContent>
</Tooltip>
)
}

View File

@ -1,137 +0,0 @@
'use client'
import { useTheme } from 'next-themes'
import { animated, useSpring } from '@react-spring/web'
import { useEffect, useState, HTMLAttributes } from 'react'
import { Button } from './ui/button'
type SVGProps = Omit<HTMLAttributes<HTMLOrSVGElement>, 'onChange'>
export interface Props extends SVGProps {
onChange?: (checked: boolean) => void
size?: number | string
moonColor?: string
sunColor?: string
starterId?: number
}
export function ThemeSwitch({
onChange,
size = 24,
moonColor = 'white',
sunColor = 'dark',
starterId = 0,
}: Props) {
let REACT_TOGGLE_DARK_MODE_GLOBAL_ID = starterId
const { theme, systemTheme, setTheme } = useTheme()
const [id, setId] = useState(REACT_TOGGLE_DARK_MODE_GLOBAL_ID)
useEffect(() => {
REACT_TOGGLE_DARK_MODE_GLOBAL_ID += 1
setId(REACT_TOGGLE_DARK_MODE_GLOBAL_ID)
}, [setId])
useEffect(() => {
// If the theme is currently using system theme, set the theme according to the system theme value.
if (theme === 'system') {
setTheme(systemTheme as 'dark' | 'light')
}
}, [])
const properties = {
circle: {
r: theme === 'dark' ? 9 : 5,
},
mask: {
cx: theme === 'dark' ? '50%' : '100',
cy: theme === 'dark' ? '23%' : '0%',
},
svg: {
transform: theme === 'dark' ? 'rotate(40deg)' : 'rotate(90deg)',
},
lines: {
opacity: theme === 'dark' ? 0 : 1,
},
config: { mass: 4, tension: 250, friction: 35 },
}
const svgContainerProps = useSpring({
...properties.svg,
config: properties.config,
})
const centerCircleProps = useSpring({
...properties.circle,
config: properties.config,
})
const maskedCircleProps = useSpring({
...properties.mask,
config: properties.config,
})
const linesProps = useSpring({
...properties.lines,
config: properties.config,
})
const uniqueMaskId = `circle-mask-${id}`
const toggle = () => {
setTheme(theme === 'dark' ? 'light' : 'dark')
onChange && onChange(theme === 'dark')
}
return (
<Button onClick={toggle} variant="ghost" size="icon">
<div className="flex items-center w-5 h-5 bg-transparent">
<animated.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
color={theme === 'dark' ? moonColor : sunColor}
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
style={{
cursor: 'pointer',
...svgContainerProps,
}}
>
<mask id={uniqueMaskId}>
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<animated.circle
// @ts-ignore
style={maskedCircleProps}
r="9"
fill="black"
/>
</mask>
<animated.circle
cx="12"
cy="12"
fill={theme === 'dark' ? moonColor : sunColor}
// @ts-ignore
style={centerCircleProps}
mask={`url(#${uniqueMaskId})`}
/>
<animated.g stroke="currentColor" style={linesProps}>
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</animated.g>
</animated.svg>
</div>
</Button>
)
}

View File

@ -1,49 +0,0 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
)
},
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View File

@ -1,86 +0,0 @@
import Link from 'next/link'
import slug from 'slug'
import { cn } from '@/lib/utils'
import { HTMLAttributes } from 'react'
type HeadingProps = HTMLAttributes<HTMLHeadingElement>
export interface Props extends HeadingProps {
children: string
/**
* Heading level, each level represent the HTML heading level.
* @min 1
* @max 6
*/
level: number
/**
* If `true`, the heading will be associated with a hash link.
* @default true
*/
withLink?: boolean
}
const HashLink = ({ text }: { text: string }) => {
return (
<>
<Link
href={`#${slug(text)}`}
className="group-hover/heading:opacity-100 opacity-0 transition-opacity ease-in-out duration-500 text-foreground/40"
>
#
</Link>
<div id={slug(text)} className="relative invisible -top-24" />
</>
)
}
export function Heading({ children, level, withLink = true, ...rest }: Props) {
const defaultClasses = ['group/heading', 'font-bold', 'flex', 'items-center', 'gap-4', 'mb-2']
switch (level) {
case 1:
return (
<h1 {...rest} className={cn(defaultClasses, 'text-4xl md:text-6xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h1>
)
case 2:
return (
<h2 {...rest} className={cn(defaultClasses, 'text-3xl md:text-5xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h2>
)
case 3:
return (
<h3 {...rest} className={cn(defaultClasses, 'text-2xl md:text-4xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h3>
)
case 4:
return (
<h4 {...rest} className={cn(defaultClasses, 'text-xl md:text-3xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h4>
)
case 5:
return (
<h5 {...rest} className={cn(defaultClasses, 'text-lg md:text-2xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h5>
)
case 6:
return (
<h6 {...rest} className={cn(defaultClasses, 'text-base md:text-xl', rest.className)}>
{children}
{withLink ? <HashLink text={children} /> : null}
</h6>
)
}
}

View File

@ -1,116 +0,0 @@
'use client'
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
},
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
)
SheetHeader.displayName = 'SheetHeader'
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
)
SheetFooter.displayName = 'SheetFooter'
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -1,7 +0,0 @@
import { cn } from '@/lib/utils'
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />
}
export { Skeleton }

View File

@ -1,30 +0,0 @@
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -1,19 +0,0 @@
export const siteConfig = {
name: 'Refansa',
email: 'm.refansa.am@gmail.com',
tel: '(+62) 812-8543-3284',
url: 'https://refansa.my.id',
description: 'A humble internet abode.',
get links() {
return siteLinks
},
}
export const siteLinks = {
github: 'https://github.com/refansa',
email: `mailto:${siteConfig.email}`,
tel: `tel:${siteConfig.tel}`,
}
export type SiteConfig = typeof siteConfig
export type SiteLinks = typeof siteLinks

View File

@ -1,20 +0,0 @@
---
siteTitle: 'What the heck is going on with web development?'
postTitle: 'The Weird State of Web Development'
siteDescription: 'Javascript, the mother of all frameworks.'
postDescription: 'A never ending solution to a never ending problem...'
publishedOn: '03 August, 2024'
updatedOn: '04 August, 2024'
isPublished: true
tags: ['web-development', 'personal-thought']
---
I still remember the first time I tried to dip my toe into the world of programming. On that time, I still can't wrap my head around some of the general-purpose language
like C, C++, Java or the likes (*Yes, I'm just that stupid at the time*) actually works, this was in my early days when I start learning how to code in junior high school.
Everytime I tried C, I keep shooting my foot with **`segmentation fault`** errors, so I say to myself,
> You know what? Let me just learn how to create a website, it look simple enough. It won't be too hard isn't it? (*But boy do I was wrong...*)
I start off with just HTML, CSS, and a little bit of javascript, it was just a simple personal website. But then I start comparing my work with others.
I keep mumbling to myself, how do I do this? How do I do that? It was more complicated than I imagined. I thought browser just support HTML, CSS, and JS, so I did my research. But to my surprise,
<TermWord description="There are other things that are supported by browsers, but I'm talking about the main general support of browser DOM rendering.">**THEY DO JUST THAT**<span className="dark:text-red-400 text-red-600">*</span></TermWord>.

49
src/data/projectList.json Normal file
View File

@ -0,0 +1,49 @@
[
{
"title": "AmanaTax",
"description": "A video course website to learn about taxes.",
"imgSrc": "/assets/project-1.png",
"alt": "AmanaTax Homepage",
"tags": [
"internship project",
"laravel",
"completed"
]
},
{
"title": "Koperasi",
"description": "Cooperatives website that accept online transactions.",
"codeSrc": "https://github.com/Refansa/koperasi",
"imgSrc": "/assets/project-2.png",
"alt": "Koperasi Homepage",
"tags": [
"school project",
"vue.js",
"completed"
]
},
{
"title": "RMBG",
"description": "Easily remove background from an image with one simple click.",
"codeSrc": "https://github.com/Refansa/rmbg",
"imgSrc": "/assets/project-3.png",
"alt": "RMBG Website",
"tags": [
"personal project",
"react.js",
"completed"
]
},
{
"title": "This Portfolio",
"description": "My own portfolio!",
"codeSrc": "https://github.com/Refansa/portfolio",
"imgSrc": "/assets/project-4.png",
"alt": "Portfolio Homepage",
"tags": [
"personal project",
"react",
"in progress"
]
}
]

View File

@ -0,0 +1,35 @@
'use client'
import { motion, useAnimation } from 'framer-motion'
import { useEffect } from 'react'
import { useInView } from 'react-intersection-observer'
export default function SlideUpWhenVisible({
children,
threshold,
}: {
children: React.ReactNode
threshold?: number | number[]
}) {
const controls = useAnimation()
const [ref, inView] = useInView({ threshold: threshold ? threshold : 0 })
useEffect(() => {
if (inView) {
controls.start('visible')
}
}, [controls, inView])
return (
<motion.div
ref={ref}
animate={controls}
initial='hidden'
transition={{ duration: 0.4 }}
variants={{
visible: { opacity: 1, y: 0 },
hidden: { opacity: 0, y: 20 },
}}>
{children}
</motion.div>
)
}

View File

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,3 @@
.grayscale {
filter: grayscale(1);
}

77
src/partials/About.tsx Normal file
View File

@ -0,0 +1,77 @@
import {
Anchor,
Box,
Container,
Image,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core'
import classes from './About.module.css'
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
function About() {
return (
<SlideUpWhenVisible>
<Box my='xl'>
<SimpleGrid
cols={{ base: 1, sm: 2 }}
spacing={{ base: 'sm', md: 'md', lg: 'lg' }}>
<Stack>
<Title>A bit About Me</Title>
<Stack gap='md'>
<Text>
Hello 👋 Refansa here. Muhammad Refansa Ali Muzky is my full
name. I'm an 18 years old software developer, just came fresh
out of the oven. Constantly seeking new open source projects to
contribute to and enjoy working with others to make a meaningful
contribution.
</Text>
<Text>
I thrive on challenging coding projects and love nothing more
than diving into complex software development tasks. As a lover
of open source, I believe that sharing knowledge and
collaborating on projects is essential for the advancement of
the tech industry.
</Text>
<Text
mt='lg'
c='dark.1'
fs='italic'>
I use Archlinux btw.
</Text>
<Text
mt='xl'
fw='bold'>
Peace from Indonesia 🇮🇩
</Text>
</Stack>
</Stack>
<Container visibleFrom='sm'>
<Stack
justify='center'
w={360}>
<Image
className={classes.grayscale}
src='/assets/hand-coding.svg'
alt='Coding Illustration'
width={360}
height='auto'
/>
<Anchor
ta='center'
fs='italic'
fz='xs'
href='https://storyset.com/work'>
Work illustrations by Storyset
</Anchor>
</Stack>
</Container>
</SimpleGrid>
</Box>
</SlideUpWhenVisible>
)
}
export default About

View File

@ -0,0 +1,142 @@
import { Box, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'
import {
IconBrandCss3,
IconBrandHtml5,
IconBrandJavascript,
IconBrandLaravel,
IconBrandMantine,
IconBrandMysql,
IconBrandNextjs,
IconBrandNodejs,
IconBrandPhp,
IconBrandPrisma,
IconBrandPython,
IconBrandReact,
IconBrandSass,
IconBrandSupabase,
IconBrandTailwind,
IconBrandVue,
} from '@tabler/icons-react'
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
interface ItemGridProps {
icon: React.ReactNode
text: string
}
const Item = ({ icon, text }: ItemGridProps) => {
return (
<Paper
w={100}
h={100}>
<Stack
p='sm'
w='inherit'
h='inherit'
align='center'
justify='center'>
{icon}
<Text
style={{ userSelect: 'none' }}
fw='bold'
fz='sm'>
{text}
</Text>
</Stack>
</Paper>
)
}
export default function Experiences() {
return (
<SlideUpWhenVisible>
<Box my='xl'>
<Stack
align='center'
ta='center'>
<Title>Experiences</Title>
<Text
fz='lg'
ta='center'>
I have worked and used these awesome technologies in my projects
</Text>
<SimpleGrid
mt='xl'
w={{ base: 240, sm: 480, md: 720, lg: 960 }}
cols={{ base: 2, sm: 4, md: 6, lg: 8 }}>
<Item
icon={<IconBrandHtml5 />}
text='HTML'
/>
<Item
icon={<IconBrandCss3 />}
text='CSS'
/>
<Item
icon={<IconBrandJavascript />}
text='Javascript'
/>
<Item
icon={<IconBrandPhp />}
text='PHP'
/>
<Item
icon={<IconBrandPython />}
text='Python'
/>
<Item
icon={<IconBrandMysql />}
text='MySQL'
/>
<Item
icon={<IconBrandLaravel />}
text='Laravel'
/>
<Item
icon={<IconBrandNodejs />}
text='NodeJS'
/>
<Item
icon={<IconBrandReact />}
text='React'
/>
<Item
icon={<IconBrandVue />}
text='Vue'
/>
<Item
icon={<IconBrandTailwind />}
text='Tailwind'
/>
<Item
icon={<IconBrandSass />}
text='Sass'
/>
<Item
icon={<IconBrandMantine />}
text='Mantine'
/>
<Item
icon={<IconBrandNextjs />}
text='NextJS'
/>
<Item
icon={<IconBrandPrisma />}
text='Prisma'
/>
<Item
icon={<IconBrandSupabase />}
text='Supabase'
/>
</SimpleGrid>
<Text
mt='xl'
fs='italic'
ta='center'>
And probably many more...
</Text>
</Stack>
</Box>
</SlideUpWhenVisible>
)
}

View File

@ -0,0 +1,20 @@
.background {
&::before {
content: '';
background-image: url('/grid.svg');
background-repeat: repeat;
position: absolute;
inset: 0;
opacity: 0.4;
width: 100%;
height: 100%;
pointer-events: none;
}
}
.line {
width: 4rem;
height: 0.125rem;
background-color: var(--mantine-color-coffee-8);
border-radius: 1rem;
}

View File

@ -0,0 +1,69 @@
import { Box, Button, Flex, Group, Text, Title } from '@mantine/core'
import { IconBrandGithub, IconMail } from '@tabler/icons-react'
import classes from './Introduction.module.css'
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
function Introduction() {
return (
<SlideUpWhenVisible>
<Box
mt='xl'
mah='100vh'
h='100vh'
className={classes.background}>
<Flex
align='center'
gap='md'>
<Box className={classes.line} />
<Text
fz='display-lg'
fw='bold'
c='coffee.6'>
Hi There!
</Text>
</Flex>
<Title
variant='shadow'
fz='display-xl'
fw='bold'
lh={1.2}>
I'm Refansa.
</Title>
<Text
fz='display-lg'
fw='bold'
lh='xs'>
A Passionate, self-taught Software Developer{' '}
<Text
component='span'
display='block'
inherit
c='dark.1'>
A supporter of Open Source Software.
</Text>
</Text>
<Group mt={'xl'}>
<Button
component='a'
href='https://github.com/Refansa'
target='_blank'
leftSection={<IconBrandGithub />}
size='lg'
variant='light'>
Github
</Button>
<Button
component='a'
href='mailto:m.refansa.am@gmail.com'
leftSection={<IconMail />}
size='lg'
variant='light'>
Email
</Button>
</Group>
</Box>
</SlideUpWhenVisible>
)
}
export default Introduction

126
src/partials/Projects.tsx Normal file
View File

@ -0,0 +1,126 @@
import {
AspectRatio,
Badge,
Box,
Button,
Card,
Group,
Image,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core'
import { IconBrandGithub } from '@tabler/icons-react'
import SlideUpWhenVisible from '../hooks/slideUpWhenVisible'
import NextImage from 'next/image'
export interface ProjectItemProps {
alt: string
codeSrc?: any
description: string
imgSrc?: any
tags?: string[]
title: string
}
const ProjectItem = ({
alt,
codeSrc,
description,
imgSrc,
tags,
title,
}: ProjectItemProps) => {
return (
<Card
shadow='sm'
padding='lg'
withBorder>
<Card.Section>
<AspectRatio
ratio={1366 / 609}
mah={160}
my='auto'>
<Image
component={NextImage}
src={imgSrc}
alt={alt}
width={340}
height={160}
/>
</AspectRatio>
</Card.Section>
<Text
fw='bold'
size='lg'
mt='md'>
{title}
</Text>
{tags ? (
<Group
gap='xs'
my='md'>
{tags.map((tag) => {
return (
<Badge
key={title}
radius='xs'
variant='light'>
{tag}
</Badge>
)
})}
</Group>
) : null}
<Text
size='sm'
c='dimmed'
mb='md'>
{description}
</Text>
<Group mt='auto'>
{codeSrc ? (
<Button
component='a'
href={codeSrc}
target='_blank'
leftSection={<IconBrandGithub />}
variant='filled'
size='lg'>
Source Code
</Button>
) : null}
</Group>
</Card>
)
}
export default function Projects({
projectLists,
}: {
projectLists: ProjectItemProps[]
}) {
return (
<SlideUpWhenVisible>
<Box my='xl'>
<Stack align='center'>
<Title ta='center'>My Projects</Title>
<SimpleGrid
mt='xl'
cols={{ base: 1, sm: 2, md: 3 }}>
{projectLists.map((project) => {
return (
<ProjectItem
key={project.title}
{...project}
/>
)
})}
</SimpleGrid>
</Stack>
</Box>
</SlideUpWhenVisible>
)
}

View File

@ -0,0 +1,7 @@
.button {
transition: all linear 150ms;
&:active {
transform: scale(calc(0.9 * var(--mantine-scale)));
}
}

View File

@ -0,0 +1,8 @@
import { Button as Component } from '@mantine/core'
import classes from './Button.module.css'
export const Button = Component.extend({
defaultProps: {
className: classes.button,
},
})

View File

@ -0,0 +1,7 @@
.card {
@mixin dark {
&[data-with-border='true'] {
border-color: var(--mantine-color-dark-6);
}
}
}

View File

@ -0,0 +1,8 @@
import { Card as Component } from '@mantine/core'
import classes from './Card.module.css'
export const Card = Component.extend({
defaultProps: {
className: classes.card,
},
})

View File

@ -0,0 +1,15 @@
.title {
font-weight: bold;
}
.content {
@mixin dark {
background-color: var(--mantine-color-dark-8);
}
}
.header {
@mixin dark {
background-color: var(--mantine-color-dark-8);
}
}

View File

@ -0,0 +1,10 @@
import { Drawer as Component } from '@mantine/core'
import classes from './Drawer.module.css'
export const Drawer = Component.extend({
classNames: {
title: classes.title,
header: classes.header,
content: classes.content,
},
})

View File

@ -0,0 +1,5 @@
.root {
@mixin dark {
background-color: var(--mantine-color-dark-8);
}
}

View File

@ -0,0 +1,8 @@
import { Paper as Component } from '@mantine/core'
import classes from './Paper.module.css'
export const Paper = Component.extend({
classNames: {
root: classes.root,
},
})

View File

@ -0,0 +1,13 @@
.text {
@mixin dark {
&[data-variant='curvy'] {
text-decoration-style: wavy;
text-decoration-color: var(--mantine-color-coffee-4);
text-decoration-line: underline;
}
&[data-variant='shadow'] {
text-shadow: 0.25rem 0.25rem var(--mantine-color-dark-4);
}
}
}

View File

@ -0,0 +1,9 @@
import { Text as Component } from '@mantine/core'
import classes from './Text.module.css'
export const Text = Component.extend({
defaultProps: {
className: classes.text,
},
})

View File

@ -0,0 +1,5 @@
.input {
@mixin dark {
background-color: var(--mantine-color-dark-6);
}
}

View File

@ -0,0 +1,11 @@
import { TextInput as Component } from '@mantine/core'
import classes from './TextInput.module.css'
export const TextInput = Component.extend({
defaultProps: {
classNames: {
input: classes.input,
},
},
})

View File

@ -0,0 +1,5 @@
.input {
@mixin dark {
background-color: var(--mantine-color-dark-6);
}
}

View File

@ -0,0 +1,11 @@
import { Textarea as Component } from '@mantine/core'
import classes from './Textarea.module.css'
export const Textarea = Component.extend({
defaultProps: {
classNames: {
input: classes.input,
},
},
})

View File

@ -0,0 +1,13 @@
.title {
@mixin dark {
&[data-variant='curvy'] {
text-decoration-style: wavy;
text-decoration-color: var(--mantine-color-coffee-4);
text-decoration-line: underline;
}
&[data-variant='shadow'] {
text-shadow: 0.25rem 0.25rem var(--mantine-color-dark-4);
}
}
}

View File

@ -0,0 +1,9 @@
import { Title as Component } from '@mantine/core'
import classes from './Title.module.css'
export const Title = Component.extend({
defaultProps: {
className: classes.title,
},
})

View File

@ -0,0 +1,8 @@
export { Button } from './Button'
export { Card } from './Card'
export { Drawer } from './Drawer'
export { Paper } from './Paper'
export { Text } from './Text'
export { Textarea } from './Textarea'
export { TextInput } from './TextInput'
export { Title } from './Title'

View File

@ -1,77 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 33 44% 22%;
--primary-foreground: 355.7 100% 97.3%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 33 44% 42%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 0 0% 95%;
--card: 24 9.8% 10%;
--card-foreground: 0 0% 95%;
--popover: 0 0% 9%;
--popover-foreground: 0 0% 95%;
--primary: 33 44% 52%;
--primary-foreground: 144.9 80.4% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 15%;
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 33 44% 52%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
scrollbar-color: hsl(var(--primary)) hsl(var(--secondary));
scrollbar-width: thin;
}
body {
@apply bg-background text-foreground/90;
}
*::selection {
@apply bg-primary/20;
}
}

180
src/styles/theme.ts Normal file
View File

@ -0,0 +1,180 @@
'use client'
import {
CSSVariablesResolver,
MantineColorsTuple,
createTheme,
} from '@mantine/core'
import { fluidDisplay, fluidTypography } from '../utils/typography'
import {
Button,
Card,
Drawer,
Paper,
Text,
TextInput,
Textarea,
Title,
} from './extends'
const coffee: MantineColorsTuple = [
'#fff4e5',
'#f4e7d8',
'#e5cfb5',
'#d4b590',
'#c59e6f',
'#bd905a',
'#ba894f',
'#a3763e',
'#926835',
'#805928',
]
export const theme = createTheme({
// Controls focus ring styles. Supports the following options:
// - `auto` focus ring is displayed only when the user navigates with keyboard (default value)
// - `always` focus ring is displayed when the user navigates with keyboard and mouse
// - `never` focus ring is always hidden (not recommended)
//
focusRing: 'auto',
// rem units scale, change if you customize font-size of `<html />` element
// default value is `1` (for `100%`/`16px` font-size on `<html />`)
//
scale: 1,
// Determines whether `font-smoothing` property should be set on the body, `true` by default
fontSmoothing: true,
// White color
white: '#ffffff',
// Black color
black: '#000000',
// Object of colors, key is color name, value is an array of at least 10 strings (colors)
colors: {
coffee,
},
// Index of theme.colors[color].
// Primary shade is used in all components to determine which color from theme.colors[color] should be used.
// Can be either a number (09) or an object to specify different color shades for light and dark color schemes.
// Default value `{ light: 6, dark: 8 }`
//
// For example,
// { primaryShade: 6 } // shade 6 is used both for dark and light color schemes
// { primaryShade: { light: 6, dark: 7 } } // different shades for dark and light color schemes
//
primaryShade: { light: 4, dark: 8 },
// Key of `theme.colors`, hex/rgb/hsl values are not supported.
// Determines which color will be used in all components by default.
// Default value `blue`.
//
primaryColor: 'coffee',
// Function to resolve colors based on variant.
// Can be used to deeply customize how colors are applied to `Button`, `ActionIcon`, `ThemeIcon`
// and other components that use colors from theme.
//
// variantColorResolver: VariantColorsResolver;
// font-family used in all components, system fonts by default
fontFamily:
'Poppins, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji',
// Monospace font-family, used in code and other similar components, system fonts by default
fontFamilyMonospace:
'Fira Code, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
// Controls various styles of h1-h6 elements, used in TypographyStylesProvider and Title components
headings: {
fontFamily:
'Gabarito, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji',
},
// Object of values that are used to set `border-radius` in all components that support it
// radius: MantineRadiusValues;
// Key of `theme.radius` or any valid CSS value. Default `border-radius` used by most components
defaultRadius: 0,
// Object of values that are used to set various CSS properties that control spacing between elements
// spacing: MantineSpacingValues;
// Object of values that are used to control `font-size` property in all components
fontSizes: {
'fluid-xs': fluidTypography(8, 12, 480, 1440),
'fluid-sm': fluidTypography(10, 14, 480, 1440),
'fluid-md': fluidTypography(12, 16, 480, 1440),
'fluid-lg': fluidTypography(14, 18, 480, 1440),
'fluid-xl': fluidTypography(16, 20, 480, 1440),
'display-xl': fluidDisplay(80, 144, 768, 1920),
'display-lg': fluidDisplay(24, 36, 768, 1920),
'display-md': fluidDisplay(16, 24, 768, 1920),
},
// Object of values that are used to control `line-height` property in `Text` component
// lineHeights: MantineLineHeightValues;
// Object of values that are used to control breakpoints in all components,
// values are expected to be defined in em
//
// breakpoints: MantineBreakpointsValues;
// Object of values that are used to add `box-shadow` styles to components that support `shadow` prop
// shadows: MantineShadowsValues;
// Determines whether user OS settings to reduce motion should be respected, `false` by default
// respectReducedMotion: boolean;
// Determines which cursor type will be used for interactive elements
// - `default` cursor that is used by native HTML elements, for example, `input[type="checkbox"]` has `cursor: default` styles
// - `pointer` sets `cursor: pointer` on interactive elements that do not have these styles by default
//
cursorType: 'pointer',
// Default gradient configuration for components that support `variant="gradient"`
// defaultGradient: MantineGradient;
// Class added to the elements that have active styles, for example, `Button` and `ActionIcon`
// activeClassName: string;
// Class added to the elements that have focus styles, for example, `Button` or `ActionIcon`.
// Overrides `theme.focusRing` property.
//
// focusClassName: string;
// Allows adding `classNames`, `styles` and `defaultProps` to any component
components: {
Button,
Card,
Drawer,
Paper,
Text,
Textarea,
TextInput,
Title,
},
// Any other properties that you want to access with the theme objects
other: {
bodyLight: '#ffffff',
textLight: '#000000',
bodyDark: '#000000',
textDark: '#ffffff',
},
})
export const resolver: CSSVariablesResolver = (t) => ({
variables: {},
light: {
'--mantine-color-body': t.other.bodyLight,
'--mantine-color-text': t.other.textLight,
},
dark: {
'--mantine-color-body': t.other.bodyDark,
'--mantine-color-text': t.other.textDark,
},
})

58
src/utils/typography.ts Normal file
View File

@ -0,0 +1,58 @@
export const PIXEL_PER_REM = 16
/**
* Returns a number from pixel to rem.
*
* @param value The value in pixel.
*/
export const rem = (value: number): number => {
return value / PIXEL_PER_REM
}
/**
* Returns a string of calculated value used for font size in display typography.
*
* Formula based from https://github.com/abdulrcs/abdulrahman.id/blob/main/styles/theme.js
*
* @param minFont Minimal font size in pixel.
* @param maxFont Maximum font size in pixel.
* @param minVW Minimum font size in pixel.
* @param maxVW Maximum font size in pixel.
*/
export const fluidDisplay = (
minFont: number,
maxFont: number,
minVW: number,
maxVW: number,
) => {
let XX = minVW / 100
let YY = (100 * (maxFont - minFont)) / (maxVW - minVW)
let ZZ = rem(minFont)
return `calc(${ZZ}rem + ((1vw - ${XX}px) * ${YY}))`
}
/**
* Returns a string of clamp value used for font size in normal typography.
*
* Formula based from https://www.aleksandrhovhannisyan.com/blog/fluid-type-scale-with-css-clamp/
*
* @param minFont Minimal font size in pixel.
* @param maxFont Maximum font size in pixel.
* @param minVW Minimum font size in pixel.
* @param maxVW Maximum font size in pixel.
*/
export const fluidTypography = (
minFont: number,
maxFont: number,
minVW: number,
maxVW: number,
): string => {
const slope = (maxFont - minFont) / (maxVW - minVW)
const intercept = rem(minFont - slope * minVW)
const slopeVW = slope * 100
const minSize = rem(minFont)
const maxSize = rem(maxFont)
return `clamp(${minSize}rem, ${slopeVW}vw + ${intercept}rem, ${maxSize}rem)`
}

View File

@ -1,81 +0,0 @@
import type { Config } from 'tailwindcss'
import TailwindCSSAnimate from 'tailwindcss-animate'
const config = {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx,mdx}',
'./components/**/*.{ts,tsx,mdx}',
'./app/**/*.{ts,tsx,mdx}',
'./src/**/*.{ts,tsx,mdx}',
],
prefix: '',
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [TailwindCSSAnimate],
} satisfies Config
export default config

View File

@ -1,13 +1,19 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@ -16,11 +22,15 @@
{
"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"
]
}

2442
yarn.lock Normal file

File diff suppressed because it is too large Load Diff