├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .npmrc ├── .env.example ├── commitlint.config.js ├── public ├── favicon.ico ├── newtab-cursor.png ├── yehez-profile.png ├── images │ └── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png ├── manifest.json ├── yehez_avatar.svg └── yehez_avatar_transparent.svg ├── postcss.config.js ├── src ├── constants │ ├── categoryList.ts │ ├── track.ts │ ├── baseConstants.ts │ ├── Funding.ts │ ├── footerLink.ts │ ├── MenuList.tsx │ ├── contactList.ts │ └── techStacks.tsx ├── pages │ ├── resume.tsx │ ├── _document.tsx │ ├── _app.tsx │ ├── projects.tsx │ ├── articles │ │ ├── index.tsx │ │ └── [slug].tsx │ ├── aboutme.tsx │ └── index.tsx ├── lib │ ├── helpers │ │ ├── clsxm.ts │ │ ├── categoryColor.ts │ │ ├── yehezOgImage.ts │ │ ├── trackEvent.ts │ │ └── formatDate.ts │ └── services │ │ ├── sanity-config.ts │ │ ├── fetcher.ts │ │ └── types.ts ├── hooks │ ├── useLoaded.tsx │ └── useScroll.tsx ├── components │ ├── layouts │ │ ├── Layout.tsx │ │ ├── MetaHead.tsx │ │ ├── FooterComponent.tsx │ │ ├── BottomNav.tsx │ │ └── HeaderComponent.tsx │ ├── BaseImage.tsx │ ├── links │ │ ├── UnderlineLink.tsx │ │ ├── PrimaryLink.tsx │ │ ├── UnstyledLink.tsx │ │ └── ArrowLink.tsx │ ├── ui │ │ ├── ExperienceCard.tsx │ │ ├── ProjectCard.tsx │ │ ├── ArticleCard.tsx │ │ └── FundingModal.tsx │ ├── NextImage.tsx │ ├── forms │ │ └── UnstyledSelect.tsx │ ├── buttons │ │ ├── ButtonLink.tsx │ │ └── Button.tsx │ └── markdown │ │ └── newTheme.tsx ├── context │ └── PreloadContext.tsx ├── __test__ │ └── index.test.tsx └── styles │ └── globals.css ├── next-env.d.ts ├── jest.setup.js ├── .github ├── dependabot.yaml └── workflows │ ├── release-please.yaml │ └── yehezgun.yaml ├── next.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── jest.config.js ├── next-seo.config.js ├── README.md ├── tailwind.config.js ├── package.json ├── biome.json └── CHANGELOG.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | UMAMI_WEB_ID= 2 | UMAMI_SRC= 3 | SANITY_DATASET= 4 | SANITY_PROJECTID= -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/newtab-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/newtab-cursor.png -------------------------------------------------------------------------------- /public/yehez-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/yehez-profile.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /public/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehezkielgunawan/yehezgun-v3/HEAD/public/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/constants/categoryList.ts: -------------------------------------------------------------------------------- 1 | export const categoryList: Array = [ 2 | "All", 3 | "Tutorial", 4 | "Random", 5 | "Explicit", 6 | ]; 7 | -------------------------------------------------------------------------------- /src/constants/track.ts: -------------------------------------------------------------------------------- 1 | export const EVENT_TYPE_LINK = "link"; 2 | export const EVENT_TYPE_BLOG = "blog"; 3 | export const EVENT_TYPE_RESUME = "resume"; 4 | export const EVENT_TYPE_FUNDING = "funding"; 5 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/pages/resume.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | function Resume() { 4 | return ( 5 | 9 | ); 10 | } 11 | 12 | export default Resume; 13 | -------------------------------------------------------------------------------- /src/lib/helpers/clsxm.ts: -------------------------------------------------------------------------------- 1 | import clsx, { ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | /** Merge classes with tailwind-merge with clsx full feature */ 5 | export default function clsxm(...classes: ClassValue[]) { 6 | return twMerge(clsx(...classes)); 7 | } 8 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | import "@testing-library/jest-dom/extend-expect"; 7 | -------------------------------------------------------------------------------- /src/lib/helpers/categoryColor.ts: -------------------------------------------------------------------------------- 1 | type CategoryColorListProps = { 2 | [key: string]: string; 3 | }; 4 | 5 | export const categoryColorList: CategoryColorListProps = { 6 | Tutorial: "bg-cyan-200 dark:bg-cyan-700", 7 | Random: "bg-violet-300 dark:bg-violet-700", 8 | Explicit: "bg-rose-300 dark:bg-rose-700", 9 | }; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: "npm" 5 | # Look for `package.json` and `lock` files in the `root` directory 6 | directory: "/" 7 | # Check the npm registry for updates every day (weekdays) 8 | schedule: 9 | interval: "weekly" 10 | versioning-strategy: increase-if-necessary 11 | -------------------------------------------------------------------------------- /src/constants/baseConstants.ts: -------------------------------------------------------------------------------- 1 | export const CHECK_YOUR_CONNECTION_MESSAGE = 2 | "Gagal load data, cek koneksi internet anda atau matikan adblock anda!"; 3 | export const UMAMI_WEB_ID = process.env.UMAMI_WEB_ID; 4 | export const UMAMI_WEB_SRC = process.env.UMAMI_SRC; 5 | export const SANITY_DATASET = process.env.SANITY_DATASET; 6 | export const SANITY_PROJECTID = process.env.SANITY_PROJECTID; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | eslint: { 6 | dirs: ["src"], 7 | }, 8 | env: { 9 | UMAMI_WEB_ID: process.env.UMAMI_WEB_ID, 10 | UMAMI_SRC: process.env.UMAMI_SRC, 11 | SANITY_DATASET: process.env.SANITY_DATASET, 12 | SANITY_PROJECTID: process.env.SANITY_PROJECTID, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/useLoaded.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { usePreloadState } from "@/context/PreloadContext"; 4 | 5 | export default function useLoaded() { 6 | const isPreLoaded = usePreloadState(); 7 | const [isLoaded, setIsLoaded] = useState(false); 8 | React.useEffect(() => { 9 | if (isPreLoaded) { 10 | setIsLoaded(true); 11 | } else { 12 | setTimeout(() => { 13 | setIsLoaded(true); 14 | }, 200); 15 | } 16 | }, [isPreLoaded]); 17 | return isLoaded; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/helpers/yehezOgImage.ts: -------------------------------------------------------------------------------- 1 | export default function yehezOgImage( 2 | text: string, 3 | isArticle: boolean, 4 | pageDesc?: string 5 | ): string { 6 | if (isArticle) { 7 | return `https://og-v2.yehezgun.com/api/og?title=${encodeURIComponent( 8 | text 9 | )}&desc=An%20article%20post%20by%20Yehezkiel%20Gunawan.&siteName=yehezgun.com`; 10 | } 11 | return `https://og-v2.yehezgun.com/api/og?title=${encodeURIComponent( 12 | text 13 | )}&desc=${encodeURIComponent(pageDesc as string)}&siteName=yehezgun.com`; 14 | } 15 | -------------------------------------------------------------------------------- /src/constants/Funding.ts: -------------------------------------------------------------------------------- 1 | import { GiPayMoney } from "react-icons/gi"; 2 | import { SiGithubsponsors } from "react-icons/si"; 3 | 4 | import { SingleContact } from "./contactList"; 5 | 6 | export const fundingSources: Array = [ 7 | { 8 | name: "Github Sponsor", 9 | icon: SiGithubsponsors, 10 | link_route: "https://github.com/sponsors/yehezkielgunawan", 11 | }, 12 | { 13 | name: "Support Via Mayar (IDR Only)", 14 | icon: GiPayMoney, 15 | link_route: "https://yehezgun.mayar.link/support", 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | .eslintcache 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | -------------------------------------------------------------------------------- /src/lib/helpers/trackEvent.ts: -------------------------------------------------------------------------------- 1 | // Full Docs: https://umami.is/docs/tracker-functions 2 | type TrackEventArgsType = { 3 | eventName: string; 4 | eventData: { [key: string]: string | number }; 5 | url?: string; 6 | webID?: string; 7 | }; 8 | 9 | export const trackEvent = ({ 10 | eventName, 11 | eventData, 12 | url, 13 | webID, 14 | }: TrackEventArgsType) => { 15 | if (window?.umami && typeof window?.umami?.track === "function") { 16 | window?.umami?.track(eventName, { 17 | ...eventData, 18 | url: url || window.location.pathname, 19 | webID: Number(webID), 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "./src", 18 | "paths": { 19 | "@/*": ["*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/layouts/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import FooterComponent from "@/components/layouts/FooterComponent"; 4 | import HeaderComponent from "@/components/layouts/HeaderComponent"; 5 | import { PreloadProvider } from "@/context/PreloadContext"; 6 | 7 | import BottomNav from "./BottomNav"; 8 | 9 | const Layout = ({ children }: { children: React.ReactNode }) => { 10 | return ( 11 | <> 12 | 13 | 14 |
{children}
15 |
16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /src/constants/footerLink.ts: -------------------------------------------------------------------------------- 1 | type FooterLinkProps = { 2 | url: string; 3 | labelName: string; 4 | }; 5 | 6 | export const footerLinks: Array = [ 7 | { 8 | url: "https://github.com/yehezkielgunawan?tab=repositories&q=starter&type=&language=&sort=", 9 | labelName: "Starter Template", 10 | }, 11 | { 12 | url: "https://docs.yehezgun.com", 13 | labelName: "Personal Docs", 14 | }, 15 | { 16 | url: "https://umami-v2.yehezgun.com/share/vD8Bwpq4/yehezgun", 17 | labelName: "Analytics", 18 | }, 19 | { 20 | url: "https://statuspage.freshping.io/60373-YehezGunWebAppStatus", 21 | labelName: "Status", 22 | }, 23 | { 24 | url: "https://yehezkielgunawan.showwcase.com/", 25 | labelName: "Showwcase", 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/constants/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import { AiFillHome } from "react-icons/ai"; 2 | import { FaPencilRuler, FaRocket } from "react-icons/fa"; 3 | import { IconType } from "react-icons/lib"; 4 | import { RiAccountCircleFill } from "react-icons/ri"; 5 | 6 | type MenuListProps = { 7 | menu_name: string; 8 | route: string; 9 | icon: IconType; 10 | }; 11 | 12 | export const menuList: Array = [ 13 | { 14 | menu_name: "Home", 15 | route: "/", 16 | icon: AiFillHome, 17 | }, 18 | { 19 | menu_name: "Projects", 20 | route: "/projects", 21 | icon: FaRocket, 22 | }, 23 | { 24 | menu_name: "Articles", 25 | route: "/articles", 26 | icon: FaPencilRuler, 27 | }, 28 | { 29 | menu_name: "About Me", 30 | route: "/aboutme", 31 | icon: RiAccountCircleFill, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | is_bump_minor: 7 | description: "bump-minor-pre-major" 8 | default: false 9 | required: true 10 | type: boolean 11 | is_bump_patch_for_minor_pre_major: 12 | description: "bump-patch-for-minor-pre-major" 13 | default: false 14 | required: true 15 | type: boolean 16 | 17 | jobs: 18 | release-please: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: google-github-actions/release-please-action@v3 22 | with: 23 | release-type: node 24 | package-name: release-please-action 25 | bump-minor-pre-major: ${{ inputs.is_bump_minor }} 26 | bump-patch-for-minor-pre-major: ${{ inputs.is_bump_patch_for_minor_pre_major }} 27 | -------------------------------------------------------------------------------- /src/components/BaseImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import clsxm from "@/lib/helpers/clsxm"; 4 | 5 | type BaseImageProps = { 6 | className?: string; 7 | } & React.ComponentPropsWithRef<"img">; 8 | 9 | const BaseImage = React.forwardRef( 10 | ({ className, ...rest }, ref) => { 11 | const [status, setStatus] = useState(false); 12 | return ( 13 | {rest.alt} setStatus(true)} 22 | loading="lazy" 23 | decoding="async" 24 | {...rest} 25 | /> 26 | ); 27 | } 28 | ); 29 | 30 | export default BaseImage; 31 | -------------------------------------------------------------------------------- /src/hooks/useScroll.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | const useScroll = () => { 4 | const [y, setY] = useState(0); 5 | const [scrollDirection, setScrollDirection] = useState< 6 | "Up" | "Down" | "Not Moved Yet" 7 | >("Not Moved Yet"); 8 | 9 | const handleNavigation = useCallback(() => { 10 | if (y > window.scrollY) { 11 | setScrollDirection("Up"); 12 | } else if (y < window.scrollY) { 13 | setScrollDirection("Down"); 14 | } 15 | setY(window.scrollY); 16 | }, [y]); 17 | 18 | useEffect(() => { 19 | window.addEventListener("scroll", handleNavigation); 20 | return () => { 21 | window.removeEventListener("scroll", handleNavigation); 22 | }; 23 | }, [handleNavigation]); 24 | 25 | return { scrollDirection, scrollPosition: y }; 26 | }; 27 | 28 | export default useScroll; 29 | -------------------------------------------------------------------------------- /src/components/links/UnderlineLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import clsxm from "@/lib/helpers/clsxm"; 4 | 5 | import UnstyledLink, { UnstyledLinkProps } from "./UnstyledLink"; 6 | 7 | const UnderlineLink = React.forwardRef( 8 | ({ children, className, ...rest }, ref) => { 9 | return ( 10 | 21 | {children} 22 | 23 | ); 24 | } 25 | ); 26 | 27 | UnderlineLink.displayName = "UnderlineLink"; 28 | 29 | export default UnderlineLink; 30 | -------------------------------------------------------------------------------- /src/components/links/PrimaryLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import clsxm from "@/lib/helpers/clsxm"; 4 | 5 | import UnstyledLink, { UnstyledLinkProps } from "./UnstyledLink"; 6 | 7 | const PrimaryLink = React.forwardRef( 8 | ({ className, children, ...rest }, ref) => { 9 | return ( 10 | 20 | {children} 21 | 22 | ); 23 | } 24 | ); 25 | 26 | PrimaryLink.displayName = "PrimaryLink"; 27 | 28 | export default PrimaryLink; 29 | -------------------------------------------------------------------------------- /src/lib/helpers/formatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = ( 2 | date: string | Date, 3 | time: boolean = true, 4 | lang: string = "en-EN" 5 | ): string => { 6 | const convertedDate = new Date(date); 7 | const options: Intl.DateTimeFormatOptions = { 8 | day: "numeric", 9 | month: "long", 10 | year: "numeric", 11 | }; 12 | const optionsWithoutTime: Intl.DateTimeFormatOptions = { 13 | day: "numeric", 14 | month: "long", 15 | year: "numeric", 16 | }; 17 | return convertedDate.toLocaleDateString( 18 | lang, 19 | time ? options : optionsWithoutTime 20 | ); 21 | }; 22 | 23 | export const formatDateMonth = (date: string | Date) => { 24 | const convertedDate = new Date(date); 25 | const formattedDate: Intl.DateTimeFormatOptions = { 26 | month: "short", 27 | year: "numeric", 28 | }; 29 | return convertedDate.toLocaleDateString("en-EN", formattedDate); 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext, 7 | } from "next/document"; 8 | 9 | import { UMAMI_WEB_ID, UMAMI_WEB_SRC } from "@/constants/baseConstants"; 10 | 11 | export default class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const initialProps = await Document.getInitialProps(ctx); 14 | return { ...initialProps }; 15 | } 16 | 17 | render() { 18 | return ( 19 | 20 | 21 |