feat: initial release!

This commit is contained in:
Refansa 2024-07-28 22:53:57 +07:00
parent c369cf9d76
commit fbf07f5dff
34 changed files with 6739 additions and 265 deletions

View File

@ -52,7 +52,13 @@
"eslint-comments/no-unused-disable": "off",
"no-useless-concat": "off",
"func-style": "off",
"eslint-comments/no-unlimited-disable": "off"
"eslint-comments/no-unlimited-disable": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
},
"overrides": [
{

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
pnpm lint

View File

@ -1,36 +1,16 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# Site - [refansa.my.id](https://refansa.my.id)
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
First, run the development server:
After you cloned this repo you could easily run by;
```bash
npm install
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Open http://localhost:3000 with your browser to see the result.

View File

@ -1,20 +1,40 @@
{
"name": "refansa.my.id",
"version": "0.0.1",
"version": "0.0.1a",
"private": true,
"homepage": "https://refansa.my.id",
"author": {
"name": "Muhammad Refansa Ali Muzky",
"nickname": "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"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prepare": "husky"
},
"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",
"lucide-react": "^0.408.0",
"next": "14.2.5",
"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"
},
@ -22,6 +42,7 @@
"@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",
@ -30,9 +51,10 @@
"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"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,6 @@ const config = {
plugins: {
tailwindcss: {},
},
};
}
export default config;
export default config

21
src/app/blog/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import { Metadata } from 'next'
import Header from '@/components/blocks/header/header'
import Footer from '@/components/blocks/footer/footer'
import UnderConstruction from '@/components/blocks/error/under-construction'
export const metadata: Metadata = {
title: 'Blog',
}
export default function Blog() {
return (
<div className="max-w-screen-lg w-full mx-auto px-6">
<Header />
<main className="flex justify-center items-center h-[85vh]">
<UnderConstruction />
</main>
<Footer />
</div>
)
}

View File

@ -1,69 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--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: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--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;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,22 +1,56 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import '@/styles/globals.css'
const inter = Inter({ subsets: ['latin'] })
import type { Metadata, Viewport } from 'next'
import { siteConfig } from '@/config/site'
import { ThemeProvider } from '@/components/providers/theme-provider'
import { TooltipProvider } from '@/components/ui/tooltip'
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: {
default: siteConfig.name,
template: `${siteConfig.name} | %s`,
},
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',
},
],
creator: 'Muhammad Refansa Ali Muzky',
}
export default function RootLayout({
children,
}: Readonly<{
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}
interface RootLayoutProps {
children: React.ReactNode
}>) {
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<html lang="en" suppressHydrationWarning>
<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>
</body>
</html>
)
}

19
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,19 @@
import { Metadata } from 'next'
import Header from '@/components/blocks/header/header'
import PageNotFound from '@/components/blocks/error/page-not-found'
export const metadata: Metadata = {
title: 'You seem to be lost...',
}
export default function NotFound() {
return (
<div className="max-w-screen-lg w-full mx-auto px-6">
<Header />
<main className="flex justify-center items-center h-[85vh]">
<PageNotFound />
</main>
</div>
)
}

View File

@ -1,113 +1,19 @@
import Image from 'next/image'
import Header from '@/components/blocks/header/header'
import Footer from '@/components/blocks/footer/footer'
import AboutSection from '@/components/blocks/home/about-section'
import ContactSection from '@/components/blocks/home/contact-section'
import IntroductionSection from '@/components/blocks/home/introduction-section'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className="mb-3 text-2xl font-semibold">
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
<div className="max-w-screen-lg w-full mx-auto px-6">
<Header />
<main className="flex flex-col gap-24 mb-24">
<IntroductionSection />
<AboutSection />
<ContactSection />
</main>
<Footer />
</div>
)
}

21
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import { Metadata } from 'next'
import Header from '@/components/blocks/header/header'
import Footer from '@/components/blocks/footer/footer'
import UnderConstruction from '@/components/blocks/error/under-construction'
export const metadata: Metadata = {
title: 'Projects',
}
export default function Projects() {
return (
<div className="max-w-screen-lg w-full mx-auto px-6">
<Header />
<main className="flex justify-center items-center h-[85vh]">
<UnderConstruction />
</main>
<Footer />
</div>
)
}

17
src/components/anchor.tsx Normal file
View File

@ -0,0 +1,17 @@
import Link from 'next/link'
import { UrlObject } from 'url'
import { HTMLAttributes } from 'react'
export interface Props extends HTMLAttributes<HTMLAnchorElement> {
href: string | UrlObject
}
export default function Anchor({ href, children, ...rest }: Props) {
return (
<Link className="underline hover:text-foreground/80" href={href} {...rest}>
{children}
</Link>
)
}

View File

@ -0,0 +1,21 @@
import Link from 'next/link'
import { HomeIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function PageNotFound() {
return (
<div className="flex font-mono gap-4 flex-col items-center tracking-wider">
<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

@ -0,0 +1,21 @@
import Link from 'next/link'
import { HomeIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function UnderConstruction() {
return (
<div className="flex font-mono gap-4 flex-col items-center tracking-wider">
<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

@ -0,0 +1,13 @@
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">
<p className="font-semibold text-center">Site Version: {Package.version}</p>
<p className="font-semibold text-center">
Created with by <Anchor href={Package.author.url}>{Package.author.nickname}</Anchor>
</p>
</footer>
)
}

View File

@ -0,0 +1,75 @@
import Link from 'next/link'
import dynamic from 'next/dynamic'
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() {
return (
<nav className="flex h-16 p-2 items-center">
<div className="flex-1">
<Link href={'/'} className="font-bold text-xl">
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

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

View File

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

View File

@ -0,0 +1,40 @@
import Anchor from '@/components/anchor'
import TermWord from '@/components/term-word'
import { Heading } from '@/components/ui/heading'
export default function AboutSection() {
return (
<section className="flex flex-col gap-4 tracking-wider 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

@ -0,0 +1,20 @@
import { siteConfig } from '@/config/site'
import { Heading } from '@/components/ui/heading'
import Anchor from '@/components/anchor'
export default function ContactSection() {
return (
<section className="flex flex-col gap-4 text-xs md:text-base tracking-wider 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

@ -0,0 +1,46 @@
import { SiGithub } from '@icons-pack/react-simple-icons'
import { Mail } from 'lucide-react'
import { siteConfig } from '@/config/site'
import { Button } from '@/components/ui/button'
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, <i>self-taught</i> 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>
)
}

37
src/components/clock.tsx Normal file
View File

@ -0,0 +1,37 @@
'use client'
import { useEffect, useState } from 'react'
export function Clock() {
const [time, setTime] = useState(
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Asia/Jakarta',
}),
)
useEffect(() => {
const timerInterval = setInterval(() => {
setTime(
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: '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

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

View File

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

View File

@ -0,0 +1,130 @@
'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, 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])
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

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

View File

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

116
src/components/ui/sheet.tsx Normal file
View File

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

View File

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

View File

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

19
src/config/site.ts Normal file
View File

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

72
src/styles/globals.css Normal file
View File

@ -0,0 +1,72 @@
@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;
}
body {
@apply bg-background text-foreground;
}
*::selection {
@apply bg-primary/20;
}
}

View File

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