├── .npmrc ├── src ├── components │ ├── index.ts │ ├── song │ │ ├── index.ts │ │ └── NowPlaying.tsx │ ├── UI │ │ ├── drawer │ │ │ ├── index.ts │ │ │ ├── DrawerButton.tsx │ │ │ └── DrawerMenu.tsx │ │ ├── inputs │ │ │ ├── index.ts │ │ │ └── Searchbar.tsx │ │ ├── common │ │ │ ├── Header │ │ │ │ ├── index.ts │ │ │ │ ├── Header.tsx │ │ │ │ ├── MobileNav.tsx │ │ │ │ └── ThemeMenu.tsx │ │ │ ├── index.ts │ │ │ ├── EmptyResult.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Nav.tsx │ │ │ ├── SocialHome.tsx │ │ │ ├── Ads.tsx │ │ │ └── Footer.tsx │ │ ├── templates │ │ │ ├── index.ts │ │ │ ├── Hero.tsx │ │ │ └── LayoutPage.tsx │ │ ├── images │ │ │ ├── index.ts │ │ │ ├── WrappedImage.tsx │ │ │ └── CustomImage.tsx │ │ ├── links │ │ │ ├── index.tsx │ │ │ ├── UnstyledLink.tsx │ │ │ └── UnderlineLink.tsx │ │ └── buttons │ │ │ ├── index.ts │ │ │ ├── UnstyledButton.tsx │ │ │ ├── SkipToContent.tsx │ │ │ └── ToTopButton.tsx │ ├── dialog │ │ ├── index.ts │ │ └── DialogResume.tsx │ ├── guestbook │ │ ├── index.ts │ │ ├── Guestbook.tsx │ │ ├── GuestbookItem.tsx │ │ └── GuestbookEditor.tsx │ ├── content │ │ ├── index.ts │ │ ├── portfolio │ │ │ ├── index.ts │ │ │ ├── PortfolioList.tsx │ │ │ ├── PortfolioItem.tsx │ │ │ ├── HeadingPortfolio.tsx │ │ │ └── IconStack.tsx │ │ ├── blog │ │ │ ├── index.ts │ │ │ ├── BlogList.tsx │ │ │ ├── GiscusComment.tsx │ │ │ ├── AuthorSection.tsx │ │ │ ├── BlogItem.tsx │ │ │ └── HeadingContent.tsx │ │ ├── mdx │ │ │ ├── Code.tsx │ │ │ ├── Blockquote.tsx │ │ │ ├── Kbd.tsx │ │ │ ├── index.ts │ │ │ ├── Table.tsx │ │ │ ├── Callout.tsx │ │ │ ├── ContentImage.tsx │ │ │ ├── Tree.tsx │ │ │ ├── Pre.tsx │ │ │ └── Headings.tsx │ │ └── PRButton.tsx │ ├── songlist │ │ ├── TopTracks.tsx │ │ └── Tracks.tsx │ └── CustomSeo.tsx ├── services │ ├── supabase │ │ ├── index.ts │ │ └── client.ts │ ├── directory │ │ ├── index.ts │ │ ├── location.ts │ │ └── readDirectory.ts │ ├── content │ │ ├── index.ts │ │ ├── getContentBySlug.ts │ │ └── getContents.ts │ ├── index.ts │ └── umami │ │ ├── index.ts │ │ ├── instance │ │ ├── umami.ts │ │ ├── index.ts │ │ └── api.ts │ │ ├── getToken.ts │ │ └── pageViews.ts ├── libs │ ├── index.ts │ ├── sorters │ │ ├── index.ts │ │ ├── sortPortfolio.ts │ │ └── sortBlog.ts │ ├── string │ │ ├── index.ts │ │ ├── toLowerCase.ts │ │ └── toUpperCase.ts │ ├── metapage │ │ ├── index.ts │ │ ├── type.ts │ │ ├── ogImage.ts │ │ ├── metaPage.ts │ │ └── metaPageBlog.ts │ ├── intl │ │ ├── index.ts │ │ ├── numberFormat.ts │ │ └── dateFormat.ts │ ├── sing │ │ └── fetcher.ts │ ├── constants │ │ ├── environmentState.ts │ │ ├── social.ts │ │ ├── route.ts │ │ └── certificate.ts │ ├── twclsx.ts │ ├── animation │ │ └── variants.ts │ └── spotify.ts ├── hooks │ ├── guestbook │ │ ├── index.ts │ │ ├── model.ts │ │ ├── useGuestbookUser.tsx │ │ └── useGuestbook.tsx │ ├── index.ts │ ├── utils │ │ ├── index.ts │ │ ├── useTags.tsx │ │ ├── useSearchBlog.tsx │ │ ├── useSearchPortfolio.tsx │ │ └── useSearch.tsx │ └── UI │ │ ├── index.ts │ │ ├── useWindowScrollY.tsx │ │ ├── useTheme.tsx │ │ ├── useDrawer.tsx │ │ ├── useMediaQuery.tsx │ │ └── useClickOutside.tsx ├── stores │ └── index.ts ├── pages │ ├── api │ │ ├── hello.ts │ │ ├── top-tracks.ts │ │ ├── revalidate.ts │ │ ├── umami.ts │ │ ├── now-playing.ts │ │ ├── pageviews │ │ │ └── index.ts │ │ └── og.tsx │ ├── _offline.tsx │ ├── 404.tsx │ ├── toptracks.tsx │ ├── _document.tsx │ ├── _app.tsx │ ├── guestbook.tsx │ ├── portfolio │ │ ├── index.tsx │ │ └── [slug].tsx │ ├── tags.tsx │ ├── certificate.tsx │ └── blog │ │ └── [slug].tsx ├── data │ ├── portfolio │ │ ├── links.mdx │ │ └── hotspotssn.mdx │ └── blog │ │ ├── penyelesaian-soal-lks-itnsa-2023.mdx │ │ ├── port-forwarding-indihome-router-zte-f609.mdx │ │ ├── nmap-scanning-port.mdx │ │ ├── mqtt-use-node-js.mdx │ │ └── ganti-password-zte-f670l.mdx ├── styles │ └── globals.css └── types │ └── index.d.ts ├── public ├── ads.txt ├── favicon.ico ├── icon-192x192.png ├── icon-256x256.png ├── icon-384x384.png ├── icon-512x512.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── fonts │ └── inter-var-latin.woff2 ├── .well-known │ ├── verified-icon.png │ └── brave-rewards-verification.txt ├── fallback-EortYSfSOECOJeuQDaNWo.js ├── browserconfig.xml ├── blur.svg ├── vercel.svg ├── manifest.json └── safari-pinned-tab.svg ├── pnpm-workspace.yaml ├── postcss.config.js ├── renovate.json ├── next-env.d.ts ├── next-sitemap.js ├── .husky └── pre-commit ├── .eslintrc.js ├── .eslintignore ├── .prettierignore ├── .vscode └── extensions.json ├── .prettierrc.js ├── .env.example ├── .gitignore ├── tsconfig.json ├── LICENSE ├── README.md ├── next.config.js ├── tailwind.config.js └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | //.npmrc 2 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomSeo' 2 | -------------------------------------------------------------------------------- /src/components/song/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NowPlaying' 2 | -------------------------------------------------------------------------------- /src/services/supabase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | -------------------------------------------------------------------------------- /src/components/UI/drawer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DrawerButton' 2 | -------------------------------------------------------------------------------- /src/components/UI/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Searchbar' 2 | -------------------------------------------------------------------------------- /src/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DialogResume' 2 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-9254295768355301, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /src/components/UI/common/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header' 2 | -------------------------------------------------------------------------------- /src/libs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './twclsx' 2 | export * from './spotify' 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'services/*' 3 | - 'contracts/*' 4 | - 'packages/*' -------------------------------------------------------------------------------- /src/libs/sorters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sortBlog' 2 | export * from './sortPortfolio' 3 | -------------------------------------------------------------------------------- /src/libs/string/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toLowerCase' 2 | export * from './toUpperCase' 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/UI/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Hero' 2 | export * from './LayoutPage' 3 | -------------------------------------------------------------------------------- /src/services/directory/index.ts: -------------------------------------------------------------------------------- 1 | export * from './readDirectory' 2 | export * from './location' 3 | -------------------------------------------------------------------------------- /src/components/UI/images/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomImage' 2 | export * from './WrappedImage' 3 | -------------------------------------------------------------------------------- /src/components/guestbook/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GuestbookEditor' 2 | export * from './Guestbook' 3 | -------------------------------------------------------------------------------- /src/hooks/guestbook/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useGuestbookUser' 2 | export * from './useGuestbook' 3 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UI' 2 | export * from './utils' 3 | export * from './guestbook' 4 | -------------------------------------------------------------------------------- /src/services/content/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getContentBySlug' 2 | export * from './getContents' 3 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | 3 | export const atomDrawer = atom(false) 4 | -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/icon-256x256.png -------------------------------------------------------------------------------- /public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/icon-384x384.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/icon-512x512.png -------------------------------------------------------------------------------- /src/components/UI/links/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './UnstyledLink' 2 | export * from './UnderlineLink' 3 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directory' 2 | export * from './content' 3 | export * from './umami' 4 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/libs/metapage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metaPage' 2 | export * from './metaPageBlog' 3 | export * from './ogImage' 4 | -------------------------------------------------------------------------------- /src/services/umami/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getToken' 2 | export * from './pageViews' 3 | export * from './instance' 4 | -------------------------------------------------------------------------------- /public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /src/hooks/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTags' 2 | export * from './useSearchBlog' 3 | export * from './useSearchPortfolio' 4 | -------------------------------------------------------------------------------- /public/.well-known/verified-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harleydica/taufikcrisnawan/HEAD/public/.well-known/verified-icon.png -------------------------------------------------------------------------------- /src/components/UI/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UnstyledButton' 2 | export * from './SkipToContent' 3 | export * from './ToTopButton' 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/components/content/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blog' 2 | export * from './mdx' 3 | export * from './portfolio' 4 | export * from './PRButton' 5 | -------------------------------------------------------------------------------- /src/components/content/portfolio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IconStack' 2 | export * from './HeadingPortfolio' 3 | export * from './PortfolioList' 4 | -------------------------------------------------------------------------------- /src/libs/intl/index.ts: -------------------------------------------------------------------------------- 1 | // contains internalization like DateFormat, and NumberFormat 2 | export * from './numberFormat' 3 | export * from './dateFormat' 4 | -------------------------------------------------------------------------------- /src/hooks/guestbook/model.ts: -------------------------------------------------------------------------------- 1 | export type Guestbook = { 2 | message_id: string 3 | user_id: string 4 | message: string 5 | created_at: string 6 | name: string 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/UI/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMediaQuery' 2 | export * from './useClickOutside' 3 | export * from './useDrawer' 4 | export * from './useTheme' 5 | export * from './useWindowScrollY' 6 | -------------------------------------------------------------------------------- /src/libs/string/toLowerCase.ts: -------------------------------------------------------------------------------- 1 | export const toLowerCase = (param: string) => param.toLowerCase() 2 | export const toLowerCaseAll = (...param: string[]) => param.map((value) => value.toLowerCase()) 3 | -------------------------------------------------------------------------------- /src/libs/string/toUpperCase.ts: -------------------------------------------------------------------------------- 1 | export const toUpperCase = (param: string) => param.toUpperCase() 2 | export const toUpperCaseAll = (...param: string[]) => param.map((value) => value.toUpperCase()) 3 | -------------------------------------------------------------------------------- /src/components/content/blog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GiscusComment' 2 | export * from './LabelBlog' 3 | export * from './AuthorSection' 4 | export * from './HeadingContent' 5 | export * from './BlogList' 6 | -------------------------------------------------------------------------------- /src/libs/sing/fetcher.ts: -------------------------------------------------------------------------------- 1 | export default async function fetcher(input: RequestInfo, init?: RequestInit): Promise { 2 | const res = await fetch(input, init) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /public/fallback-EortYSfSOECOJeuQDaNWo.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | 'use strict' 3 | self.fallback = async (e) => 4 | 'document' === e.destination ? caches.match('/_offline', { ignoreSearch: !0 }) : Response.error() 5 | })() 6 | -------------------------------------------------------------------------------- /src/components/UI/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header' 2 | export * from './Nav' 3 | export * from './Footer' 4 | export * from './Spinner' 5 | export * from './EmptyResult' 6 | export * from './SocialHome' 7 | export * from './Ads' 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /public/.well-known/brave-rewards-verification.txt: -------------------------------------------------------------------------------- 1 | This is a Brave Rewards publisher verification file. 2 | 3 | Domain: taufikcrisnawan.dev 4 | Token: a9e5315ee1bd7e9c901d4290ce64fb89459dc3ebebe0f3f490cb4f14fced4c82 5 | 6 | Domain: taufikcrisnawan.my.id 7 | Token: -------------------------------------------------------------------------------- /src/services/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { SUPABASE_ANON_KEY, SUPABASE_URL } from '@/libs/constants/environmentState' 2 | 3 | import { createClient } from '@supabase/supabase-js' 4 | 5 | export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY) 6 | -------------------------------------------------------------------------------- /src/services/directory/location.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | /** Where the content is placed. 4 | * You can change where you want to save your content. 5 | * For example, using `src/content` instead of `src/data` */ 6 | export const LOCATION_DIR = join(process.cwd(), 'src/data') 7 | -------------------------------------------------------------------------------- /src/services/umami/instance/umami.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios' 2 | 3 | const UMAMI_URL = process.env.NEXT_PUBLIC_UMAMI_URL 4 | 5 | const headers = { 'Content-Type': 'application/json' } 6 | 7 | export const UMAMI = Axios.create({ 8 | baseURL: UMAMI_URL, 9 | headers 10 | }) 11 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /next-sitemap.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: 'https://taufikcrisnawan.dev', 4 | generateRobotsTxt: true, 5 | robotsTxtOptions: { 6 | policies: [{ userAgent: '*', allow: '/', userAgent: 'Googlebot', allow: '/ads.txt' }] 7 | }, 8 | sitemapSize: 10000 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 4 | const data = { 5 | name: 'Taufik Crisnawan Santoso', 6 | status: 'Alive' 7 | } 8 | return res.status(200).json(data) 9 | } 10 | 11 | export default handler 12 | -------------------------------------------------------------------------------- /src/services/umami/instance/index.ts: -------------------------------------------------------------------------------- 1 | // server code and client code should be separated, otherwise module not found :) 2 | // dunnow why, but you might want to 3 | // @see this: https://nextjs.org/docs/messages/module-not-found#the-module-youre-trying-to-import-uses-nodejs-specific-modules 4 | export * from './umami' 5 | export * from './api' 6 | -------------------------------------------------------------------------------- /src/services/umami/instance/api.ts: -------------------------------------------------------------------------------- 1 | import { isProd } from '@/libs/constants/environmentState' 2 | 3 | import Axios from 'axios' 4 | 5 | const headers = { 'Content-Type': 'application/json' } 6 | 7 | export const API_CLIENT = Axios.create({ 8 | baseURL: isProd ? 'https://taufikcrisnawan.dev' : 'http://localhost:3000', 9 | headers 10 | }) 11 | -------------------------------------------------------------------------------- /src/libs/sorters/sortPortfolio.ts: -------------------------------------------------------------------------------- 1 | import type { Portfolio } from 'taufikcrisnawan' 2 | 3 | /** 4 | * A function to sort portfolios by latest update. 5 | * @returns a number. 6 | */ 7 | export const getNewestPortfolio = (a: Portfolio, b: Portfolio) => { 8 | return new Date(a.date) < new Date(b.date) ? 1 : new Date(a.date) > new Date(b.date) ? -1 : 0 9 | } 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | echo 'Running Git Hooks' 5 | 6 | echo "🔎 Checking validity of types with TypeScript" 7 | 8 | pnpm type-check || ( 9 | "⛔️ There is a type error in the code, fix it, and try commit again. ⛔️"; 10 | false; 11 | ) 12 | echo "✅ No TypeError found"echo '⌛️ Running lint staged and git commit ⌛️' 13 | 14 | pnpm lint-staged 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').ESLint.ConfigData} */ 2 | module.exports = { 3 | extends: ['plugin:@typescript-eslint/recommended', 'next/core-web-vitals'], 4 | rules: { 5 | '@typescript-eslint/no-unused-vars': 'off', 6 | '@typescript-eslint/no-explicit-any': 'off', 7 | 'prefer-const': 'warn', 8 | 'import/no-duplicates': 'error', 9 | '@typescript-eslint/no-extra-semi': 'off' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/content/mdx/Code.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | export const Code = (props: { children: React.ReactNode }) => { 4 | return ( 5 | 12 | {props.children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/content/mdx/Blockquote.tsx: -------------------------------------------------------------------------------- 1 | export const Blockquote = (props: { children: React.ReactNode }) => { 2 | return ( 3 |
4 | {props.children} 5 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/libs/constants/environmentState.ts: -------------------------------------------------------------------------------- 1 | export const isProd = process.env.NODE_ENV === 'production' 2 | export const isDev = process.env.NODE_ENV === 'development' 3 | export const isTest = process.env.NODE_ENV === 'test' 4 | export const SECRET_KEY = process.env.NEXT_PUBLIC_SECRET 5 | export const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL as string 6 | export const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string 7 | -------------------------------------------------------------------------------- /src/libs/intl/numberFormat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A function to format number to compact display like social media you've ever known 3 | * 4 | * example: your `1000` number will be displayed `1K` 5 | * @param num - the number You want to format as a compact display 6 | * @returns the formatted number 7 | */ 8 | export const numberToCompact = (num: number) => { 9 | return new Intl.NumberFormat('en-US', { notation: 'compact' }).format(num) 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | pnpm-debug.log* 23 | pnpm-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | # vercel 32 | .vercel -------------------------------------------------------------------------------- /public/blur.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | pnpm-debug.log* 23 | pnpm-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | # vercel 32 | .vercel -------------------------------------------------------------------------------- /src/libs/constants/social.ts: -------------------------------------------------------------------------------- 1 | const SOCIAL = [ 2 | { 3 | title: 'Email', 4 | href: 'mailto: mail@taufikcrisnawan.dev?subject=' 5 | }, 6 | { 7 | title: 'LinkedIn', 8 | href: 'https://linkedin.com/in/harleydica/' 9 | }, 10 | { 11 | title: 'GitHub', 12 | href: 'https://github.com/harleydica' 13 | }, 14 | { 15 | title: 'Instagram', 16 | href: 'https://instagram.com/taufikcrisnawan' 17 | } 18 | ] 19 | 20 | export default SOCIAL 21 | -------------------------------------------------------------------------------- /src/components/content/mdx/Kbd.tsx: -------------------------------------------------------------------------------- 1 | import { WithChildren } from 'taufikcrisnawan' 2 | 3 | type KbdProps = WithChildren 4 | 5 | const Kbd = (props: KbdProps) => { 6 | const { children } = props 7 | 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | 15 | export default Kbd 16 | -------------------------------------------------------------------------------- /src/libs/metapage/type.ts: -------------------------------------------------------------------------------- 1 | import type { CustomSeoProps } from '@/components' 2 | 3 | export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL 4 | export const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME 5 | export const TWITER_USERNAME = process.env.NEXT_PUBLIC_TWITTER_USERNAME 6 | 7 | export type MetaPage = { 8 | title: string 9 | description: string 10 | keywords: Array 11 | slug: string 12 | og_image: string 13 | og_image_alt: string 14 | type?: 'website' | 'blog' 15 | } & CustomSeoProps 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "esbenp.prettier-vscode", 5 | "bierner.markdown-preview-github-styles", 6 | "shyykoserhiy.vscode-spotify", 7 | "pkief.material-icon-theme", 8 | "eamodio.gitlens", 9 | "dbaeumer.vscode-eslint", 10 | "formulahendry.auto-rename-tag", 11 | "yzhang.markdown-all-in-one", 12 | "devzstudio.emoji-snippets", 13 | "github.github-vscode-theme" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/components/UI/templates/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | export type HeroProps = { 4 | title: string 5 | description: string 6 | children?: React.ReactNode 7 | className?: string 8 | } 9 | 10 | export const Hero: React.FunctionComponent = (props) => ( 11 |
12 |

{props.title}

13 |

{props.description}

14 | {props.children} 15 |
16 | ) 17 | -------------------------------------------------------------------------------- /src/hooks/utils/useTags.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export const useTags = () => { 4 | const [selectedTags, setSelectedTags] = useState([]) 5 | 6 | const setNewTag = useCallback((tag: string) => { 7 | return () => { 8 | setSelectedTags((prevState) => 9 | prevState.includes(tag) ? prevState.filter((p) => !p.includes(tag)) : [...prevState, tag] 10 | ) 11 | } 12 | }, []) 13 | 14 | return { 15 | selectedTags, 16 | setNewTag 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/libs/twclsx.ts: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { ClassValue } from 'clsx' 3 | import { twMerge } from 'tailwind-merge' 4 | 5 | /** 6 | * It takes a list of class names, and returns a list of class names by using tailwind merge 7 | * to merge tailwind utilities without conflict. 8 | * `e.g` 9 | * ```tsx 10 | * import { twclsx } from '@/libs/twclsx' 11 | * 12 | * 13 | * ``` 14 | */ 15 | export const twclsx = (...args: ClassValue[]) => twMerge(clsx(...args)) 16 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | semi: false, 3 | tabWidth: 2, 4 | printWidth: 120, 5 | singleQuote: true, 6 | jsxSingleQuote: true, 7 | trailingComma: 'none', 8 | arrowParens: 'always', 9 | endOfLine: 'auto', 10 | importOrder: [ 11 | '^@/components(.*)$', 12 | '^@/UI(.*)$', 13 | '^@/services(.*)$', 14 | '^@/libs(.*)$', 15 | '^@/(.*)$', 16 | '^[./]', 17 | '^', 18 | '^@/styles/(.*)$' 19 | ], 20 | importOrderSeparation: true, 21 | importOrderSortSpecifiers: true 22 | } 23 | 24 | module.exports = config 25 | -------------------------------------------------------------------------------- /src/components/UI/buttons/UnstyledButton.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | import { createElement } from 'react' 4 | 5 | export type UnstyledButtonProps = React.DetailedHTMLProps< 6 | React.ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | > 9 | 10 | export const UnstyledButton: React.FunctionComponent = ({ children, className, ...props }) => { 11 | return createElement( 12 | 'button', 13 | { ...props, className: twclsx('inline-flex items-center justify-center', className) }, 14 | children 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # umami 3 | NEXT_PUBLIC_UMAMI_USERNAME=umami username login 4 | NEXT_PUBLIC_UMAMI_PASSWORD=umami password login 5 | NEXT_PUBLIC_UMAMI_URL=umami self-hosted url 6 | 7 | # twitter 8 | NEXT_PUBLIC_TWITTER_USERNAME=@twitter username 9 | 10 | NEXT_PUBLIC_SITE_NAME=site name 11 | NEXT_PUBLIC_SITE_URL=site url including "https://" 12 | 13 | # random string 14 | NEXT_PUBLIC_SECRET= 15 | 16 | # supabase 17 | NEXT_PUBLIC_SUPABASE_URL= 18 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 19 | 20 | # spotify for developer 21 | SPOTIFY_CLIENT_ID= 22 | SPOTIFY_CLIENT_SECRET= 23 | SPOTIFY_REFRESH_TOKEN= -------------------------------------------------------------------------------- /src/components/songlist/TopTracks.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | 3 | import fetcher from '@/libs/sing/fetcher' 4 | import { TopTracks } from 'taufikcrisnawan' 5 | import Track from '@/components/songlist/Tracks' 6 | 7 | export default function Tracks() { 8 | const { data } = useSWR('/api/top-tracks', fetcher) 9 | 10 | if (!data) { 11 | return null 12 | } 13 | 14 | return ( 15 | <> 16 | {data.tracks.map((track, index) => ( 17 | 18 | ))} 19 |

20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/CustomSeo.tsx: -------------------------------------------------------------------------------- 1 | import { NextSeo } from 'next-seo' 2 | import type { NextSeoProps } from 'next-seo' 3 | 4 | export type CustomSeoProps = { 5 | template?: string 6 | } & NextSeoProps 7 | 8 | const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME 9 | 10 | /** 11 | * It takes a NextSeoProps object and returns a ` component`. 12 | */ 13 | export const CustomSeo: React.FunctionComponent = ({ ...props }) => { 14 | const TITLE_TEMPLATE = `%s — ${props.template ?? (SITE_NAME || 'taufikcrisnawan.dev')}` 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /src/components/UI/buttons/SkipToContent.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | export const SkipToContent: React.FunctionComponent = () => { 4 | return ( 5 | 17 | Skip to content 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/UI/images/WrappedImage.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs' 2 | 3 | import type { ImageProps } from 'next/image' 4 | import NextImage from 'next/image' 5 | 6 | type WrappedImageProps = ImageProps & { 7 | alt: string 8 | parentStyle?: string 9 | } 10 | 11 | export const WrappedImage: React.FunctionComponent = ({ parentStyle, ...props }) => { 12 | if (!props.fill) { 13 | return 14 | } 15 | 16 | return ( 17 |
18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/guestbook/Guestbook.tsx: -------------------------------------------------------------------------------- 1 | import { Guestbook as GuestbookType } from '@/hooks/guestbook/model' 2 | 3 | import { GuestbookItem } from './GuestbookItem' 4 | 5 | import { useId } from 'react' 6 | 7 | type P = { 8 | guestbook: GuestbookType[] 9 | } 10 | 11 | export const Guestbook: React.FunctionComponent

= (props) => { 12 | const id = useId() 13 | 14 | if (props.guestbook.length === 0) { 15 | return

No Message...

16 | } 17 | 18 | return ( 19 |
20 | {props.guestbook.map((g) => ( 21 | 22 | ))} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/libs/metapage/ogImage.ts: -------------------------------------------------------------------------------- 1 | import type { genOgImagePayload } from 'taufikcrisnawan' 2 | 3 | export const generateOgImage = (payload: genOgImagePayload) => { 4 | const hyperLogo = { 5 | light: 'hyper-color-logo.svg', 6 | dark: 'hyper-color-logo.svg' 7 | } 8 | 9 | return ( 10 | 'https://og-image.vercel.app/' + 11 | '**' + 12 | (payload?.title ?? '') + 13 | '**' + 14 | '%3Cbr%2F%3E' + 15 | (payload?.subTitle ?? '') + 16 | '.png?theme=' + 17 | (payload.theme ?? 'dark') + 18 | '&md=1&fontSize=100px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2F' + 19 | hyperLogo[payload.theme ?? 'dark'] 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/data/portfolio/links.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'My Social Media' 3 | date: '1/05/2023' 4 | featured: true 5 | summary: 'Display more my social media' 6 | image: 'https://ik.imagekit.io/taufik/portfolio/links/thumbnail.png' 7 | stack: ['TypeScript', 'nextjs', 'react', 'vercel', 'tailwindcss'] 8 | link: { github: 'https://github.com/harleydica/link', live: 'https://links.taufikcrisnawan.dev' } 9 | --- 10 | 11 | Banyak orang menggunakan [linktr.ee](https://linktr.ee), jadi saya punya ide jika saya bisa membuatnya sendiri. Project ini terinspirasi dari [Lee Robinson](https://leerob.io). hehe😆 12 | 13 | ## Tech Stack 14 | 15 | - 👾 NEXT.js 16 | - 🧰 TypeScript 17 | - ⚛️ React 18 | - 💅 Tailwind CSS 19 | - 🔺 Vercel 20 | - 〽️ Analytics 21 | -------------------------------------------------------------------------------- /src/services/umami/getToken.ts: -------------------------------------------------------------------------------- 1 | import { UMAMI } from './instance' 2 | 3 | const USERNAME = process.env.NEXT_PUBLIC_UMAMI_USERNAME 4 | const PASSWORD = process.env.NEXT_PUBLIC_UMAMI_PASSWORD 5 | 6 | /** 7 | * run only on the server. 8 | * It will return the token if the request is successful, otherwise it will return null 9 | * @returns The token or null 10 | */ 11 | export const getToken = async () => { 12 | const body = { username: USERNAME, password: PASSWORD } 13 | try { 14 | const response = await UMAMI.post<{ token: string }>('/api/auth/login', body) 15 | // return null if the status not 200 16 | // return the token 17 | return response.data.token 18 | } catch (error) { 19 | return null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/libs/constants/route.ts: -------------------------------------------------------------------------------- 1 | const APP_ROUTE = [ 2 | { 3 | path: '/', 4 | name: 'Home' 5 | }, 6 | { 7 | path: '/blog', 8 | name: 'Blog' 9 | }, 10 | { 11 | path: '/portfolio', 12 | name: 'Portfolio' 13 | }, 14 | { 15 | path: '/guestbook', 16 | name: 'Guestbook' 17 | } 18 | ] 19 | 20 | export const ADDT_ROUTE = [ 21 | { 22 | path: 'https://obs.tafk.me/', 23 | name: 'Status' 24 | }, 25 | { 26 | path: '/tags', 27 | name: 'Tags' 28 | }, 29 | { 30 | path: '/resume', 31 | name: 'Resume' 32 | }, 33 | { 34 | path: '/certificate', 35 | name: 'Certificate' 36 | }, 37 | { 38 | path: '/toptracks', 39 | name: 'Top Tracks' 40 | } 41 | ] 42 | 43 | export default APP_ROUTE 44 | -------------------------------------------------------------------------------- /src/libs/animation/variants.ts: -------------------------------------------------------------------------------- 1 | import type { Variant, Variants } from 'framer-motion' 2 | 3 | const hidden: Variant = { 4 | y: 15, 5 | opacity: 0 6 | } 7 | 8 | const visible: Variant = { 9 | y: 0, 10 | opacity: 1 11 | } 12 | 13 | const variants = (): Variants => ({ 14 | hidden, 15 | visible: { 16 | ...visible, 17 | transition: { 18 | type: 'tween', 19 | duration: 0.15 20 | } 21 | } 22 | }) 23 | 24 | export const withExit = (func: () => Variants): Variants => { 25 | const v = func() 26 | 27 | return { 28 | ...v, 29 | exit: { 30 | y: 20, 31 | opacity: 0, 32 | transition: { 33 | type: 'tween', 34 | duration: 0.25 35 | } 36 | } 37 | } 38 | } 39 | 40 | export default variants 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | pnpm-debug.log* 25 | pnpm-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | 40 | temp 41 | .eslintcache 42 | 43 | # **/public/workbox-*.js 44 | # **/public/sw.js 45 | # **/public/fallback-*.js 46 | 47 | **/public/*.xml 48 | **/public/robots.txt 49 | .env*.local 50 | -------------------------------------------------------------------------------- /src/libs/intl/dateFormat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a date, format it in a human-readable way 3 | * @param {string | Date} date - The date to format. 4 | * @returns The date in the format of Month Day, Year. 5 | */ 6 | export const dateFormat = (date: string, locales?: string | string[], config?: Intl.DateTimeFormatOptions) => { 7 | return new Intl.DateTimeFormat(locales ?? 'en-GB', config ?? { dateStyle: 'full' }).format(new Date(date)) 8 | } 9 | 10 | /** 11 | * It takes a date string and returns an ISO date string 12 | * @param {string} date - The date string to convert to ISO format. 13 | */ 14 | export const dateStringToISO = ( 15 | /** provide valid date value in string, for example: `05/05/2005` that's going to be **5 May 2005** */ 16 | date: string 17 | ) => new Date(date).toISOString() 18 | -------------------------------------------------------------------------------- /src/services/directory/readDirectory.ts: -------------------------------------------------------------------------------- 1 | import { LOCATION_DIR } from './location' 2 | 3 | import { readdir } from 'fs/promises' 4 | 5 | /** 6 | * It reads the contents of a directory and returns an array of strings that are the names of the files 7 | * in that directory 8 | * @param {string} path - required path to reads, example: `/blog` this will reads all `.mdx` files inside `src/data/blog` 9 | * @returns An array of strings. 10 | */ 11 | export const readDirectory = async (path: string): Promise> => { 12 | /** 13 | * 1. read files inside directory given 14 | * 2. filter the files, exclude all files that doesn't ends with extension .mdx 15 | * 3. return list of file names 16 | */ 17 | return (await readdir(`${LOCATION_DIR}/${path}`)).filter((p) => /\.mdx?$/.test(p)) 18 | } 19 | -------------------------------------------------------------------------------- /src/data/portfolio/hotspotssn.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Simple Login Page MikroTik' 3 | date: '7/15/2023' 4 | featured: true 5 | summary: 'Simple Login Page MikroTik Free WiFi Trial User with HTML & PHP' 6 | image: 'https://ik.imagekit.io/taufik/portfolio/hotspotssn/hotspotssn.png' 7 | stack: ['html', 'php'] 8 | link: { github: 'https://github.com/harleydica/hotspot-ssn.git', live: 'https://www.sintesa.co.id' } 9 | --- 10 | 11 | Internet Service Provider biasanya memiliki layanan Free WiFi sebagai media iklan atau penarik peminat agar membeli produk layanan internet, project kali ini saya membuat sebuah Login Page Hotspot MikroTik untuk media Free WiFi disalah satu ISP Kota Yogyakarta. Login page sederharna dengan menggunakan HTML dan PHP untuk menjalankannya. 12 | 13 | ## Tech Stack 14 | 15 | - 👾 PHP 16 | - 🧰 HTML 17 | -------------------------------------------------------------------------------- /src/components/UI/common/EmptyResult.tsx: -------------------------------------------------------------------------------- 1 | import { WrappedImage } from '@/UI/images' 2 | 3 | import { twclsx } from '@/libs/twclsx' 4 | 5 | export const EmptyResult: React.FunctionComponent = () => { 6 | return ( 7 |
8 | 18 |

19 | Couldn't find what you're looking for, come back later for further content!. 20 |

21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/UI/links/UnstyledLink.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | import type { LinkProps } from 'next/link' 3 | import { createElement, forwardRef } from 'react' 4 | 5 | export type UnstyledLinkProps = { 6 | href: string 7 | title?: string 8 | className?: string 9 | children?: React.ReactNode 10 | } & LinkProps 11 | 12 | export const UnstyledLink = forwardRef(({ href, ...props }, ref) => { 13 | if (href.startsWith('http')) { 14 | return createElement('a', { href, rel: 'noopener noreferrer', target: '_blank', ...props, ref }, props.children) 15 | } 16 | 17 | return ( 18 | 19 | {props.children} 20 | 21 | ) 22 | }) 23 | 24 | UnstyledLink.displayName = 'UnstyledLink' 25 | -------------------------------------------------------------------------------- /src/hooks/UI/useWindowScrollY.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | /** 4 | * react custom hook `useWindowScroll` will run on side effect to observe 5 | * the change of the position of the window, the initial value is `0` as from the top 6 | * This hooks will returns the current scroll position of the window 7 | * @returns The scroll position of the window Y. 8 | */ 9 | export const useWindowScrollY = () => { 10 | const [scrollPos, setScrollPos] = useState(0) 11 | 12 | useEffect(() => { 13 | if (typeof window !== 'undefined') { 14 | const handleScroll = () => setScrollPos(window.scrollY) 15 | window.addEventListener('scroll', handleScroll) 16 | 17 | return () => window.removeEventListener('scroll', handleScroll) 18 | } 19 | }, []) 20 | 21 | return scrollPos 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/api/top-tracks.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server' 2 | import { getTopTracks } from '@/libs/spotify' 3 | 4 | export const config = { 5 | runtime: 'edge' 6 | } 7 | 8 | export default async function handler(req: NextRequest) { 9 | const response = await getTopTracks() 10 | const { items } = await response.json() 11 | 12 | const tracks = items.slice(0, 10).map((track: any) => ({ 13 | artist: track.artists.map((_artist: any) => _artist.name).join(', '), 14 | songUrl: track.external_urls.spotify, 15 | title: track.name 16 | })) 17 | 18 | return new Response(JSON.stringify({ tracks }), { 19 | status: 200, 20 | headers: { 21 | 'content-type': 'application/json', 22 | 'cache-control': 'public, s-maxage=86400, stale-while-revalidate=43200' 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/content/portfolio/PortfolioList.tsx: -------------------------------------------------------------------------------- 1 | import { PortfolioItem } from './PortfolioItem' 2 | 3 | import { Portfolio } from 'taufikcrisnawan' 4 | 5 | type PortfolioListProps = { 6 | title: string 7 | portfolios: Portfolio[] 8 | description: string 9 | } 10 | 11 | export const PortfolioList: React.FunctionComponent = (props) => { 12 | return ( 13 |
14 |

{props.title}

15 |

{props.description}

16 | 17 | {props.portfolios.length > 0 && ( 18 |
19 | {props.portfolios.map((item) => { 20 | return 21 | })} 22 |
23 | )} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/_offline.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutPage } from '@/components/UI/templates' 2 | 3 | import { twclsx } from '@/libs' 4 | 5 | import { NextPage } from 'next' 6 | 7 | const meta = { 8 | title: 'You Are Offline', 9 | description: `It looks like you are offline, please connect to your internet connection and try refreshing this page.` 10 | } 11 | 12 | const OfflinePage: NextPage = () => { 13 | return ( 14 | 15 |
16 |
17 |

503 - Offline

18 |

{meta.description}

19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default OfflinePage 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "downlevelIteration": true, 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "baseUrl": "./src", 19 | "paths": { 20 | "@/*": ["*"], 21 | "@/UI": ["components/UI/"], 22 | "@/UI/*": ["components/UI/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 26 | "exclude": ["node_modules", ".next"], 27 | "moduleResolution": ["node_modules", ".next", "node"] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/songlist/Tracks.tsx: -------------------------------------------------------------------------------- 1 | export default function Track(track: any) { 2 | return ( 3 |
4 |

{track.ranking}

5 |
6 | 12 | {track.title} 13 | 14 |

15 | {track.artist} 16 |

17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/content/mdx/index.ts: -------------------------------------------------------------------------------- 1 | import { UnderlineLink } from '@/UI/links' 2 | 3 | import { Blockquote } from './Blockquote' 4 | import { Code } from './Code' 5 | import { ContentImage } from './ContentImage' 6 | import { HeadingFour, HeadingThree, HeadingTwo } from './Headings' 7 | import { Pre } from './Pre' 8 | import Table from './Table' 9 | 10 | import { MDXRemoteProps } from 'next-mdx-remote' 11 | import Callout from './Callout' 12 | import Tree from './Tree' 13 | import Kbd from './Kbd' 14 | 15 | const MDXComponents = { 16 | pre: Pre, 17 | img: ContentImage, 18 | code: Code, 19 | blockquote: Blockquote, 20 | a: UnderlineLink, 21 | ContentImage, 22 | Callout, 23 | Table, 24 | Tree, 25 | Kbd, 26 | h2: HeadingTwo, 27 | h3: HeadingThree, 28 | h4: HeadingFour 29 | } as MDXRemoteProps['components'] 30 | 31 | export { MDXComponents, Pre, Code, Blockquote, ContentImage } 32 | -------------------------------------------------------------------------------- /src/components/UI/buttons/ToTopButton.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs' 2 | 3 | import type { UnstyledButtonProps } from './UnstyledButton' 4 | import { UnstyledButton } from './UnstyledButton' 5 | 6 | import { useCallback } from 'react' 7 | import { HiArrowUp } from 'react-icons/hi' 8 | 9 | export const ToTopButton: React.FunctionComponent = (props) => { 10 | const toTop = useCallback(() => window.scrollTo({ top: 0, behavior: 'smooth' }), []) 11 | 12 | return ( 13 | 24 | 25 | Back to top 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/content/blog/BlogList.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs' 2 | 3 | import { BlogItem } from './BlogItem' 4 | 5 | import type { Blog } from 'taufikcrisnawan' 6 | 7 | type BlogListProps = { 8 | posts: Blog[] 9 | title: string 10 | description: string 11 | displayViews?: boolean 12 | className?: string 13 | } 14 | 15 | export const BlogList: React.FunctionComponent = ({ displayViews, ...props }) => { 16 | return ( 17 |
18 |

{props.title}

19 |

{props.description}

20 | 21 | {props.posts.length > 0 && ( 22 |
23 | {props.posts.map((post) => { 24 | return 25 | })} 26 |
27 | )} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/content/blog/GiscusComment.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@/hooks' 2 | 3 | import Giscus from '@giscus/react' 4 | import { memo } from 'react' 5 | 6 | export const GiscusComment = memo(() => { 7 | const { theme, systemTheme } = useTheme() 8 | 9 | const gcTheme = theme === 'dark' || (theme === 'system' && systemTheme === 'dark') ? 'dark' : 'light' 10 | 11 | // please change your repoId in giscus.app 12 | return ( 13 |
14 | 27 |
28 | ) 29 | }) 30 | 31 | GiscusComment.displayName = 'GiscusComment' 32 | -------------------------------------------------------------------------------- /src/hooks/guestbook/useGuestbookUser.tsx: -------------------------------------------------------------------------------- 1 | import { supabaseClient } from '@/services/supabase' 2 | 3 | import { User } from '@supabase/supabase-js' 4 | import { atom, useAtom } from 'jotai' 5 | import { useCallback } from 'react' 6 | 7 | export const atomGuestbookUser = atom(null) 8 | 9 | const getGuestbookUser = atom(null as User | null, async (get, set) => { 10 | const { data } = await supabaseClient.auth.getUser() 11 | set(atomGuestbookUser, data.user) 12 | }) 13 | 14 | export const useGuestbookUser = () => { 15 | const [user] = useAtom(atomGuestbookUser) 16 | const [, getUser] = useAtom(getGuestbookUser) 17 | 18 | const signin = useCallback((provider: 'google' | 'github') => { 19 | return async () => 20 | await supabaseClient.auth.signInWithOAuth({ 21 | provider, 22 | options: { redirectTo: 'https://taufikcrisnawan.dev/guestbook' } 23 | }) 24 | }, []) 25 | 26 | return { signin, user, getUser } 27 | } 28 | -------------------------------------------------------------------------------- /src/libs/metapage/metaPage.ts: -------------------------------------------------------------------------------- 1 | import type { CustomSeoProps } from '@/components' 2 | 3 | import { MetaPage, SITE_NAME, SITE_URL, TWITER_USERNAME } from './type' 4 | 5 | export const getMetaPage = (data: MetaPage): CustomSeoProps => { 6 | return { 7 | canonical: SITE_URL + data.slug, 8 | openGraph: { 9 | images: [ 10 | { 11 | url: data.og_image, 12 | alt: data.og_image_alt, 13 | width: 1200, 14 | height: 600 15 | } 16 | ], 17 | site_name: SITE_NAME, 18 | url: SITE_URL + data.slug, 19 | type: data.type ?? 'website' 20 | }, 21 | twitter: { 22 | cardType: 'summary_large_image', 23 | // TODO: Change to your Tiwetter username 24 | site: TWITER_USERNAME, 25 | handle: TWITER_USERNAME 26 | }, 27 | additionalMetaTags: [ 28 | { 29 | name: 'keywords', 30 | content: data.keywords.join(', ') 31 | } 32 | ], 33 | ...data 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/UI/links/UnderlineLink.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | import { UnstyledLink } from './UnstyledLink' 4 | import type { UnstyledLinkProps } from './UnstyledLink' 5 | 6 | export const UnderlineLink: React.FunctionComponent = ({ href, children, className, ...props }) => { 7 | return ( 8 | 22 | {children} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/UI/useTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme as useNextTheme } from 'next-themes' 2 | import { useCallback, useEffect, useState } from 'react' 3 | 4 | export const useTheme = () => { 5 | const { theme, setTheme, systemTheme } = useNextTheme() 6 | const [mounted, setMounted] = useState(false) 7 | const [dropdownIsOpen, setDropdown] = useState(false) 8 | 9 | const toggleDropdown = useCallback(() => setDropdown((prev) => (prev ? false : true)), []) 10 | const closeDropdown = useCallback(() => setDropdown(false), []) 11 | 12 | const changeTheme = useCallback( 13 | (value: string) => { 14 | return () => { 15 | setTheme(value) 16 | closeDropdown() 17 | } 18 | }, 19 | [setTheme, closeDropdown] 20 | ) 21 | 22 | useEffect(() => { 23 | setMounted(true) 24 | }, []) 25 | 26 | return { 27 | mounted, 28 | changeTheme, 29 | theme, 30 | systemTheme, 31 | dropdownIsOpen, 32 | toggleDropdown, 33 | closeDropdown 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/services/content/getContentBySlug.ts: -------------------------------------------------------------------------------- 1 | import { LOCATION_DIR } from '@/services' 2 | 3 | import { readFile } from 'fs/promises' 4 | import matter from 'gray-matter' 5 | import { join } from 'path' 6 | 7 | type GetContentBySlug = { content: string; header: { slug: string } & T } 8 | 9 | export const getContentBySlug = async ( 10 | /** the path to the directory folder, example: `/blog` 11 | * **NOTE!** that the slash on the string is required! 12 | */ 13 | path: string, 14 | /** 15 | * read as file name 16 | */ 17 | slug: string 18 | ): Promise> => { 19 | // read path to file src/data/{path}/[{slug}].mdx 20 | const dir = join(`${LOCATION_DIR}/${path}`, `${slug}.mdx`) 21 | // read file with promise based 22 | const file = await readFile(dir, 'utf8') 23 | 24 | // parse file content with gray matter, extract it's header and content 25 | const { content, data } = matter(file) 26 | 27 | return { 28 | header: { ...(data as T), slug }, 29 | content 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/api/revalidate.ts: -------------------------------------------------------------------------------- 1 | import { SECRET_KEY } from '@/libs/constants/environmentState' 2 | 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | 5 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 6 | if (req.query.secret !== SECRET_KEY) { 7 | return res.status(401).json({ message: 'Invalid token' }) 8 | } 9 | if (!req.query.slug) { 10 | return res.status(400).json({ message: 'Please provide slug to revalidate' }) 11 | } 12 | if (Array.isArray(req.query.slug)) { 13 | return res.status(400).json({ message: 'Slug should be a string' }) 14 | } 15 | 16 | try { 17 | // this should be the actual path not a rewritten path 18 | // e.g. for "/blog/[slug]" this should be "/blog/post-1" 19 | await res.revalidate(req.query.slug) 20 | return res.json({ revalidated: true }) 21 | } catch (err) { 22 | // If there was an error, Next.js will continue 23 | // to show the last successfully generated page 24 | return res.status(500).send('Error revalidating') 25 | } 26 | } 27 | 28 | export default handler 29 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/UI/common/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { twclsx } from '@/libs/twclsx' 2 | 3 | import { ImSpinner2 } from 'react-icons/im' 4 | 5 | const spinnerSize = { 6 | xs: 'w-4 h-4', 7 | sm: 'w-8 h-8', 8 | md: 'w-12 h-12', 9 | lg: 'w-16 h-16', 10 | xl: 'w-24 h-24' 11 | } 12 | 13 | const containerSize = { 14 | fit: 'w-fit h-fit', 15 | full: 'w-full h-full', 16 | fullScreen: 'fixed inset-0 z-50' 17 | } 18 | 19 | type SpinnerProps = { 20 | spinnerSize: keyof typeof spinnerSize 21 | containerSize: keyof typeof containerSize 22 | containerStyle?: string 23 | } 24 | export const Spinner: React.FunctionComponent = (props) => { 25 | return ( 26 |
34 | 35 | Loading... 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/UI/templates/LayoutPage.tsx: -------------------------------------------------------------------------------- 1 | import { CustomSeo, CustomSeoProps } from '@/components/CustomSeo' 2 | 3 | import { Footer } from '@/UI/common' 4 | 5 | import { AdblockDetector } from 'adblock-detector' 6 | import { Ads } from '@/components/UI/common/Ads' 7 | import { useEffect, useState } from 'react' 8 | 9 | import { twclsx } from '@/libs/twclsx' 10 | 11 | type TProps = { 12 | className?: string 13 | children: React.ReactNode 14 | seo: CustomSeoProps 15 | } 16 | 17 | export type LayoutPageProps = TProps 18 | 19 | export const LayoutPage = ({ children, className, ...props }: TProps) => { 20 | const [adblock, setAdblock] = useState(false) 21 | 22 | useEffect(() => { 23 | const detector = new AdblockDetector() 24 | const adblockDetected = detector.detect() 25 | if (adblockDetected) { 26 | setAdblock(true) 27 | } 28 | }, []) 29 | 30 | return ( 31 | <> 32 | 33 | {adblock ? : ''} 34 | 35 |
{children}
36 |