feat: initial release!
This commit is contained in:
parent
c369cf9d76
commit
fbf07f5dff
@ -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
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
pnpm lint
|
36
README.md
36
README.md
@ -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.
|
||||
|
28
package.json
28
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5679
pnpm-lock.yaml
5679
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,6 @@ const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
21
src/app/blog/page.tsx
Normal file
21
src/app/blog/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
19
src/app/not-found.tsx
Normal 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>
|
||||
)
|
||||
}
|
122
src/app/page.tsx
122
src/app/page.tsx
@ -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
|
||||
<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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Learn about Next.js in an interactive course with 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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</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
21
src/app/projects/page.tsx
Normal 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
17
src/components/anchor.tsx
Normal 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>
|
||||
)
|
||||
}
|
21
src/components/blocks/error/page-not-found.tsx
Normal file
21
src/components/blocks/error/page-not-found.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { HomeIcon } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function PageNotFound() {
|
||||
return (
|
||||
<div className="flex font-mono gap-4 flex-col items-center tracking-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>
|
||||
)
|
||||
}
|
21
src/components/blocks/error/under-construction.tsx
Normal file
21
src/components/blocks/error/under-construction.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { HomeIcon } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function UnderConstruction() {
|
||||
return (
|
||||
<div className="flex font-mono gap-4 flex-col items-center tracking-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>
|
||||
)
|
||||
}
|
13
src/components/blocks/footer/footer.tsx
Normal file
13
src/components/blocks/footer/footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
75
src/components/blocks/header/header-navigation.tsx
Normal file
75
src/components/blocks/header/header-navigation.tsx
Normal 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>
|
||||
)
|
||||
}
|
9
src/components/blocks/header/header.tsx
Normal file
9
src/components/blocks/header/header.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import HeaderNavigation from './header-navigation'
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 backdrop-blur-xl bg-background/80">
|
||||
<HeaderNavigation />
|
||||
</header>
|
||||
)
|
||||
}
|
20
src/components/blocks/header/navigation-item.tsx
Normal file
20
src/components/blocks/header/navigation-item.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export type Props = {
|
||||
children: ReactNode
|
||||
href: string
|
||||
}
|
||||
|
||||
export default function NavigationItem({ children, href }: Props) {
|
||||
return (
|
||||
<Button asChild variant="ghost">
|
||||
<Link href={href}>
|
||||
<span className="font-bold">{children}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
40
src/components/blocks/home/about-section.tsx
Normal file
40
src/components/blocks/home/about-section.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import Anchor from '@/components/anchor'
|
||||
import TermWord from '@/components/term-word'
|
||||
import { Heading } from '@/components/ui/heading'
|
||||
|
||||
export default function AboutSection() {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 tracking-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>
|
||||
)
|
||||
}
|
20
src/components/blocks/home/contact-section.tsx
Normal file
20
src/components/blocks/home/contact-section.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
|
||||
import { Heading } from '@/components/ui/heading'
|
||||
import Anchor from '@/components/anchor'
|
||||
|
||||
export default function ContactSection() {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 text-xs md:text-base tracking-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>
|
||||
)
|
||||
}
|
46
src/components/blocks/home/introduction-section.tsx
Normal file
46
src/components/blocks/home/introduction-section.tsx
Normal 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
37
src/components/clock.tsx
Normal 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>
|
||||
)
|
||||
}
|
9
src/components/providers/theme-provider.tsx
Normal file
9
src/components/providers/theme-provider.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { type ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
24
src/components/term-word.tsx
Normal file
24
src/components/term-word.tsx
Normal 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>
|
||||
)
|
||||
}
|
130
src/components/theme-switch.tsx
Normal file
130
src/components/theme-switch.tsx
Normal 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>
|
||||
)
|
||||
}
|
49
src/components/ui/button.tsx
Normal file
49
src/components/ui/button.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
86
src/components/ui/heading.tsx
Normal file
86
src/components/ui/heading.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import slug from 'slug'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { HTMLAttributes } from 'react'
|
||||
|
||||
type HeadingProps = HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
export interface Props extends HeadingProps {
|
||||
children: string
|
||||
/**
|
||||
* Heading level, each level represent the HTML heading level.
|
||||
* @min 1
|
||||
* @max 6
|
||||
*/
|
||||
level: number
|
||||
/**
|
||||
* If `true`, the heading will be associated with a hash link.
|
||||
* @default true
|
||||
*/
|
||||
withLink?: boolean
|
||||
}
|
||||
|
||||
const HashLink = ({ text }: { text: string }) => {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href={`#${slug(text)}`}
|
||||
className="group-hover/heading:opacity-100 opacity-0 transition-opacity ease-in-out duration-500 text-foreground/40"
|
||||
>
|
||||
#
|
||||
</Link>
|
||||
<div id={slug(text)} className="relative invisible -top-24" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Heading({ children, level, withLink = true, ...rest }: Props) {
|
||||
const defaultClasses = ['group/heading', 'font-bold', 'flex', 'items-center', 'gap-4', 'mb-2']
|
||||
|
||||
switch (level) {
|
||||
case 1:
|
||||
return (
|
||||
<h1 {...rest} className={cn(defaultClasses, 'text-4xl md:text-6xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h1>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<h2 {...rest} className={cn(defaultClasses, 'text-3xl md:text-5xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h2>
|
||||
)
|
||||
case 3:
|
||||
return (
|
||||
<h3 {...rest} className={cn(defaultClasses, 'text-2xl md:text-4xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h3>
|
||||
)
|
||||
case 4:
|
||||
return (
|
||||
<h4 {...rest} className={cn(defaultClasses, 'text-xl md:text-3xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h4>
|
||||
)
|
||||
case 5:
|
||||
return (
|
||||
<h5 {...rest} className={cn(defaultClasses, 'text-lg md:text-2xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h5>
|
||||
)
|
||||
case 6:
|
||||
return (
|
||||
<h6 {...rest} className={cn(defaultClasses, 'text-base md:text-xl', rest.className)}>
|
||||
{children}
|
||||
{withLink ? <HashLink text={children} /> : null}
|
||||
</h6>
|
||||
)
|
||||
}
|
||||
}
|
116
src/components/ui/sheet.tsx
Normal file
116
src/components/ui/sheet.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
7
src/components/ui/skeleton.tsx
Normal file
7
src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Skeleton }
|
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
19
src/config/site.ts
Normal file
19
src/config/site.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export const siteConfig = {
|
||||
name: 'Refansa',
|
||||
email: 'm.refansa.am@gmail.com',
|
||||
tel: '(+62) 812-8543-3284',
|
||||
url: 'https://refansa.my.id',
|
||||
description: 'A humble internet abode.',
|
||||
get links() {
|
||||
return siteLinks
|
||||
},
|
||||
}
|
||||
|
||||
export const siteLinks = {
|
||||
github: 'https://github.com/refansa',
|
||||
email: `mailto:${siteConfig.email}`,
|
||||
tel: `tel:${siteConfig.tel}`,
|
||||
}
|
||||
|
||||
export type SiteConfig = typeof siteConfig
|
||||
export type SiteLinks = typeof siteLinks
|
72
src/styles/globals.css
Normal file
72
src/styles/globals.css
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user