FIX(app): replace submodule with files

- Remove services/nextjs as git submodule
- Add complete Next.js application source code
- Include package.json and package-lock.json for npm cache
- This fixes the GitHub Actions cache error
This commit is contained in:
2025-11-23 23:43:22 +09:00
parent 95584b666a
commit 913575ecc0
63 changed files with 12295 additions and 1 deletions

Submodule services/nextjs deleted from 527de6b0f3

41
services/nextjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
services/nextjs/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## 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/app/building-your-application/deploying) for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,264 @@
@import "tailwindcss";
@import '../public/fonts/generated-fonts.css';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme {
--breakpoint-tablet: 768px;
--breakpoint-pc: 1024px;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
@custom-variant tablet (@media (width >= 768px));
@custom-variant pc (@media (width >= 1024px));
:root {
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--brand-primary: oklch(0.129 0.042 264.695);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
.border-b-brand-primary {
border-bottom-color: var(--brand-primary);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-meteor: meteor 5s linear infinite;
@keyframes meteor {
0% {
transform: rotate(215deg) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(215deg) translateX(-500px);
opacity: 0;
}
}
--animate-meteor-effect: meteor 5s linear infinite
;
@keyframes meteor {
0% {
transform: rotate(215deg) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(215deg) translateX(-500px);
opacity: 0;
}
}
}
/* Static Stars Background */
.stars {
background-image:
/* Small stars (2px) */
radial-gradient(2px 2px at 20px 30px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 60px 70px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 50px 160px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 130px 80px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 140px 150px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 300px 180px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 320px 320px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 400px 120px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 480px 280px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 620px 340px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 720px 120px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 800px 260px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 920px 180px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1000px 290px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1100px 150px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1200px 320px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1300px 100px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 100px 200px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 250px 80px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 350px 300px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 450px 50px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 580px 200px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 680px 350px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 780px 90px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 880px 220px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 950px 330px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1050px 60px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1150px 240px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1250px 170px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1350px 280px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 80px 340px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 180px 110px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 280px 250px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 380px 370px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 520px 140px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 640px 80px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 760px 310px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 860px 160px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 980px 240px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1080px 350px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1180px 90px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1280px 210px, white, rgba(0,0,0,0)),
radial-gradient(2px 2px at 1380px 330px, white, rgba(0,0,0,0)),
/* Medium stars (3px) */
radial-gradient(3px 3px at 220px 250px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 550px 190px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 850px 350px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 1300px 200px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 150px 380px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 420px 200px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 700px 270px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 900px 100px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 1120px 310px, white, rgba(0,0,0,0)),
radial-gradient(3px 3px at 1340px 150px, white, rgba(0,0,0,0)),
/* Large stars (4px) - fewer for emphasis */
radial-gradient(4px 4px at 500px 100px, white, rgba(0,0,0,0)),
radial-gradient(4px 4px at 900px 300px, white, rgba(0,0,0,0)),
radial-gradient(4px 4px at 200px 150px, white, rgba(0,0,0,0)),
radial-gradient(4px 4px at 1200px 250px, white, rgba(0,0,0,0));
background-repeat: repeat;
background-size: 1400px 400px;
opacity: 0.6;
animation: starsMove 200s linear infinite;
}
@keyframes starsMove {
from {
transform: translateY(0px);
}
to {
transform: translateY(-400px);
}
}
.dark .stars {
opacity: 0.8;
}

View File

@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import "./globals.css";
import Header from "@/components/widgets/Header";
import Footer from "@/components/widgets/Footer";
import { ThemeProvider } from "@/providers/theme-provider";
export const metadata: Metadata = {
title: "Portfolio - Full Stack Developer",
description: "Creating beautiful and functional web experiences with modern technologies",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
storageKey="portfolio-theme"
>
<Header />
{children}
<Footer />
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,15 @@
import Hero from '@/components/landing/hero';
import Skills from '@/components/landing/skills';
import Projects from '@/components/landing/projects';
import Contact from '@/components/landing/contact';
export default function Home() {
return (
<div className="flex flex-col">
<Hero />
<Skills />
<Projects />
<Contact />
</div>
);
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@react-bits": "https://reactbits.dev/r/{name}.json"
}
}

View File

@@ -0,0 +1,185 @@
'use client';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Mail, Github, Linkedin, Phone, MapPin, Send, ExternalLink } from 'lucide-react';
import { useState } from 'react';
import SectionHeader from './section-header';
export default function Contact() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 실제 구현 시 이메일 전송 로직 추가
console.log('Form submitted:', formData);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const contactMethods = [
{
icon: Mail,
label: 'Email',
value: 'your.email@example.com',
link: 'mailto:your.email@example.com',
color: 'text-blue-600 dark:text-blue-400'
},
{
icon: Github,
label: 'GitHub',
value: '@yourusername',
link: 'https://github.com/yourusername',
color: 'text-gray-800 dark:text-gray-200'
},
{
icon: Linkedin,
label: 'LinkedIn',
value: 'Your Name',
link: 'https://linkedin.com/in/yourusername',
color: 'text-blue-700 dark:text-blue-500'
},
{
icon: Phone,
label: 'Phone',
value: '+82 10-1234-5678',
link: 'tel:+821012345678',
color: 'text-green-600 dark:text-green-400'
}
];
return (
<div className="bg-linear-to-b from-gray-50 to-gray-100 dark:from-neutral-900 dark:to-neutral-800">
<main className="flex flex-col items-center justify-center gap-16 p-4 tablet:p-8 py-20">
<SectionHeader
title="Get In Touch"
description="Have a project in mind or want to collaborate? I'd love to hear from you. Feel free to reach out through any of the channels below."
/>
<div className="grid pc:grid-cols-2 gap-8 max-w-6xl mx-auto">
{/* Contact Info & Social Links */}
<Card className="p-8 hover:shadow-lg transition-shadow">
<h3 className="font-bold text-xl mb-6 flex items-center gap-2">
<MapPin className="w-5 h-5 text-primary" />
Contact Information
</h3>
<div className="flex flex-col gap-4">
{contactMethods.map((method, index) => (
<a
key={index}
href={method.link}
target={method.link.startsWith('http') ? '_blank' : undefined}
rel={method.link.startsWith('http') ? 'noopener noreferrer' : undefined}
className="flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors group"
>
<div className={`flex items-center justify-center w-12 h-12 rounded-full bg-gray-100 dark:bg-neutral-800 group-hover:scale-110 transition-transform ${method.color}`}>
<method.icon className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-muted-foreground">{method.label}</p>
<p className="font-medium truncate">{method.value}</p>
</div>
<ExternalLink className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
))}
</div>
</Card>
{/* Contact Form */}
<Card className="p-8 hover:shadow-xl transition-shadow">
<h3 className="font-bold text-2xl mb-2">Send Me a Message</h3>
<p className="text-muted-foreground mb-8">
Fill out the form below and I&apos;ll get back to you as soon as possible.
</p>
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div className="grid tablet:grid-cols-2 gap-6">
<div className="flex flex-col gap-2">
<label htmlFor="name" className="text-sm font-medium">
Your Name <span className="text-red-500">*</span>
</label>
<Input
id="name"
name="name"
placeholder="John Doe"
value={formData.name}
onChange={handleChange}
required
className="h-12"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="email" className="text-sm font-medium">
Your Email <span className="text-red-500">*</span>
</label>
<Input
id="email"
name="email"
type="email"
placeholder="john@example.com"
value={formData.email}
onChange={handleChange}
required
className="h-12"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="subject" className="text-sm font-medium">
Subject <span className="text-red-500">*</span>
</label>
<Input
id="subject"
name="subject"
placeholder="Project Inquiry"
value={formData.subject}
onChange={handleChange}
required
className="h-12"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="message" className="text-sm font-medium">
Message <span className="text-red-500">*</span>
</label>
<Textarea
id="message"
name="message"
placeholder="Tell me about your project or how I can help you..."
value={formData.message}
onChange={handleChange}
required
rows={8}
className="resize-none"
/>
</div>
<Button type="submit" size="lg" className="w-full tablet:w-auto tablet:self-end group">
<span className="flex items-center gap-2">
Send Message
<Send className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</Button>
</form>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { Button } from '@/components/ui/button';
import Particles from '@/components/ui/Particles';
export default function Hero() {
return (
<div className="relative flex items-center justify-center min-h-screen w-full h-full overflow-hidden">
<Particles
particleColors={['#000000']}
particleCount={300}
particleSpread={10}
speed={0.1}
particleBaseSize={100}
moveParticlesOnHover={false}
alphaParticles={true}
className="absolute inset-0 -z-10"
/>
<div className="relative w-full h-full flex items-center justify-center">
<div className="flex flex-col items-center justify-center gap-12 text-center">
<div className="flex flex-col items-center justify-center gap-12 px-5">
<h2 className="font-bold text-5xl tablet:text-7xl text-center bg-linear-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 bg-clip-text text-transparent">
Full Stack Developer
</h2>
<div className="max-w-3xl">
<p className="text-2xl tablet:text-3xl text-center leading-[160%]">
Creating beautiful and functional web experiences with modern technologies
</p>
</div>
</div>
<div className="flex items-center gap-8">
<Button
type="button"
variant="outline"
className="flex items-center gap-2 cursor-pointer"
>
View Projects
</Button>
<Button
type="button"
variant="outline"
className="flex items-center gap-2 cursor-pointer"
>
Contact Me
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
import { Card } from '@/components/ui/card';
import Image from 'next/image';
import { Separator } from '../ui/separator';
import { SquareArrowOutUpRight } from 'lucide-react';
import { Button } from '../ui/button';
import SectionHeader from './section-header';
interface ProjectCardProps {
title: string;
description: string;
tags: string[];
imageSrc: string;
}
function ProjectCard({ title, description, tags, imageSrc }: ProjectCardProps) {
return (
<Card className="overflow-hidden w-full p-0">
<div className="aspect-1440/770 relative bg-white">
<Image
src={imageSrc}
alt={title}
fill
className="object-cover border-b-2 border-gray-200"
/>
</div>
<div className="px-6 pt-6 pb-4 flex flex-col gap-3">
<h3 className="font-semibold text-2xl">{title}</h3>
<p className="text-muted-foreground font-extralight">{description}</p>
<div className="flex gap-2 flex-wrap">
{tags.map((tag) => (
<Button key={tag} variant="outline" className="text-xs px-3 py-1">
{tag}
</Button>
))}
</div>
</div>
<div className="px-6">
<Separator />
</div>
<div className="px-6 py-4">
<SquareArrowOutUpRight className="w-6 h-6 cursor-pointer" />
</div>
</Card>
);
}
export default function Projects() {
return (
<main className="flex flex-col items-center justify-center gap-16 p-4 tablet:p-8 py-20">
<SectionHeader
title="Featured Projects"
description="Some of my recent work and side projects"
/>
<section className="grid grid-cols-1 pc:grid-cols-2 gap-6 pc:gap-8 max-w-[1440px] w-full">
<ProjectCard
title="Joossam English"
description="A full-featured online shopping platform with A full-featured online shopping platform with payment integratioA full-featured online shopping pntegration"
tags={['Next.js', 'Stripe', 'PostgreSQL']}
imageSrc="/joossam/home.png"
/>
<ProjectCard
title="Jotion"
description="Collaborative task management tool with real-time updates"
tags={['React', 'WebSocket', 'MongoDB']}
imageSrc="/jotion/home.png"
/>
<ProjectCard
title="Youni Classic"
description="Analytics dashboard for social media metrics and insights"
tags={['Next.js', 'Chart.js', 'Redis']}
imageSrc="/joossam/main.png"
/>
<ProjectCard
title="Jaejadle Church"
description="Beautiful weather forecasting application with location detection"
tags={['React', 'OpenWeather API', 'Geolocation']}
imageSrc="/jaejadle/home.png"
/>
<ProjectCard
title="Portfolio"
description="Beautiful weather forecasting application with location detection"
tags={['React', 'OpenWeather API', 'Geolocation']}
imageSrc="/portfolio/home.png"
/>
<ProjectCard
title="[Seminar] Todo List"
description="Beautiful weather forecasting application with location detection"
tags={['React', 'OpenWeather API', 'Geolocation']}
imageSrc="/todoList/home.png"
/>
<ProjectCard
title="Jovies"
description="Beautiful weather forecasting application with location detection"
tags={['React', 'OpenWeather API', 'Geolocation']}
imageSrc="/jovies/home.png"
/>
</section>
</main>
);
}

View File

@@ -0,0 +1,17 @@
interface SectionHeaderProps {
title: string;
description: string;
}
export default function SectionHeader({ title, description }: SectionHeaderProps) {
return (
<div className="flex flex-col items-center gap-4 max-w-3xl text-center">
<h2 className="font-bold text-4xl tablet:text-5xl pc:text-6xl bg-linear-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 bg-clip-text text-transparent">
{title}
</h2>
<p className="text-lg tablet:text-xl text-muted-foreground max-w-2xl">
{description}
</p>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import { Card, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Code, Server, Layout, LucideIcon } from 'lucide-react';
import SectionHeader from './section-header';
interface SkillCardProps {
icon: LucideIcon;
title: string;
description: string;
skills: string[];
}
function SkillCard({ icon: Icon, title, description, skills }: SkillCardProps) {
return (
<Card className="p-5 max-w-[400px] h-full gap-4">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-center min-w-12 min-h-12 rounded-full bg-gray-100 dark:bg-neutral-800 w-fit">
<Icon className="min-w-8 min-h-8" />
</div>
<CardTitle>
<h3 className="text-lg">{title}</h3>
</CardTitle>
</div>
<div className="flex flex-col gap-6 justify-between h-full">
<p className="font-extralight">{description}</p>
<div className="flex flex-col gap-4">
<Separator />
<div className="flex gap-4 flex-wrap">
{skills.map((skill) => (
<p key={skill} className="text-sm text-gray-600 dark:text-gray-100 font-extralight">
{skill}
</p>
))}
</div>
</div>
</div>
</Card>
);
}
export default function Skills() {
return (
<div className="bg-gray-100 dark:bg-neutral-800">
<main className="flex flex-col items-center justify-center gap-16 p-4 tablet:p-8 py-20">
<SectionHeader
title="Skills & Expertise"
description="Technologies and tools I work with to build amazing web applications"
/>
<div className="grid grid-cols-1 tablet:grid-cols-2 pc:grid-cols-3 gap-6">
<SkillCard
icon={Code}
title="Frontend Development"
description="Building responsive and interactive user interfaces with modern frameworks and libraries"
skills={['React', 'Next.js', 'TypeScript', 'Tailwind CSS']}
/>
<SkillCard
icon={Server}
title="Backend Development"
description="Developing robust server-side applications and RESTful APIs"
skills={['Node.js', 'Express', 'PostgreSQL', 'MongoDB']}
/>
<SkillCard
icon={Layout}
title="DevOps & Tools"
description="Managing deployment pipelines and development workflows"
skills={['Git', 'Docker', 'AWS', 'CI/CD']}
/>
<SkillCard
icon={Layout}
title="DevOps & Tools"
description="Managing deployment pipelines and development workflows"
skills={['Git', 'Docker', 'AWS', 'CI/CD']}
/>
<SkillCard
icon={Layout}
title="DevOps & Tools"
description="Managing deployment pipelines and development workflows"
skills={['Git', 'Docker', 'AWS', 'CI/CD']}
/>
<SkillCard
icon={Layout}
title="DevOps & Tools"
description="Managing deployment pipelines and development workflows"
skills={['Git', 'Docker', 'AWS', 'CI/CD']}
/>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,51 @@
"use client";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface MeteorsProps {
number?: number;
minDelay?: number;
maxDelay?: number;
minDuration?: number;
maxDuration?: number;
angle?: number;
className?: string;
}
export const Meteors = ({
number = 20,
minDelay = 0.2,
maxDelay = 1.2,
minDuration = 2,
maxDuration = 10,
angle = 215,
className,
}: MeteorsProps) => {
const [meteorStyles] = useState<Array<React.CSSProperties>>(() => {
return [...new Array(number)].map(() => ({
"--angle": -angle + "deg",
top: "-5%",
left: `calc(0% + ${Math.floor(Math.random() * (typeof window !== "undefined" ? window.innerWidth : 1000))}px)`,
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
animationDuration:
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
"s",
}));
});
return (
<div className="absolute inset-0 overflow-hidden">
{[...meteorStyles].map((style, idx) => (
// Meteor Head
<span
key={idx}
style={{ ...style }}
className={cn(
"pointer-events-none absolute size-1 rotate-(--angle) animate-meteor rounded-full bg-white shadow-[0_0_10px_2px_rgba(255,255,255,0.5)]",
className,
)}
>
{/* Meteor Tail */}
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-[100px] -translate-y-1/2 bg-linear-to-r from-white to-transparent" />
</span>
))}
</div>
);
};

View File

@@ -0,0 +1,249 @@
"use client";
import { useEffect, useRef, useState } from 'react';
import { useTheme } from 'next-themes';
import { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';
const hexToRgb = hex => {
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex
.split('')
.map(c => c + c)
.join('');
}
const int = parseInt(hex, 16);
const r = ((int >> 16) & 255) / 255;
const g = ((int >> 8) & 255) / 255;
const b = (int & 255) / 255;
return [r, g, b];
};
const vertex = /* glsl */ `
attribute vec3 position;
attribute vec4 random;
attribute vec3 color;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float uTime;
uniform float uSpread;
uniform float uBaseSize;
uniform float uSizeRandomness;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vRandom = random;
vColor = color;
vec3 pos = position * uSpread;
pos.z *= 10.0;
vec4 mPos = modelMatrix * vec4(pos, 1.0);
float t = uTime;
mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);
mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);
mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);
vec4 mvPos = viewMatrix * mPos;
if (uSizeRandomness == 0.0) {
gl_PointSize = uBaseSize;
} else {
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
}
gl_Position = projectionMatrix * mvPos;
}
`;
const fragment = /* glsl */ `
precision highp float;
uniform float uTime;
uniform float uAlphaParticles;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vec2 uv = gl_PointCoord.xy;
float d = length(uv - vec2(0.5));
if(uAlphaParticles < 0.5) {
if(d > 0.5) {
discard;
}
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);
} else {
float circle = smoothstep(0.5, 0.4, d) * 0.8;
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
}
}
`;
const Particles = ({
particleCount = 200,
particleSpread = 10,
speed = 0.1,
particleColors,
moveParticlesOnHover = false,
particleHoverFactor = 1,
alphaParticles = false,
particleBaseSize = 100,
sizeRandomness = 1,
cameraDistance = 20,
disableRotation = false,
className
}) => {
const containerRef = useRef(null);
const mouseRef = useRef({ x: 0, y: 0 });
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
if (!mounted) return;
const renderer = new Renderer({ depth: false, alpha: true });
const gl = renderer.gl;
container.appendChild(gl.canvas);
gl.clearColor(0, 0, 0, 0);
const camera = new Camera(gl, { fov: 15 });
camera.position.set(0, 0, cameraDistance);
const resize = () => {
const width = container.clientWidth;
const height = container.clientHeight;
renderer.setSize(width, height);
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
};
window.addEventListener('resize', resize, false);
resize();
const handleMouseMove = e => {
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
mouseRef.current = { x, y };
};
if (moveParticlesOnHover) {
container.addEventListener('mousemove', handleMouseMove);
}
const count = particleCount;
const positions = new Float32Array(count * 3);
const randoms = new Float32Array(count * 4);
const colors = new Float32Array(count * 3);
const isDark = resolvedTheme === 'dark';
const defaultLightColors = ['#000000'];
const palette = isDark
? ['#FFFFFF']
: (particleColors && particleColors.length > 0 ? particleColors : defaultLightColors);
for (let i = 0; i < count; i++) {
let x, y, z, len;
do {
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
len = x * x + y * y + z * z;
} while (len > 1 || len === 0);
const r = Math.cbrt(Math.random());
positions.set([x * r, y * r, z * r], i * 3);
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
colors.set(col, i * 3);
}
const geometry = new Geometry(gl, {
position: { size: 3, data: positions },
random: { size: 4, data: randoms },
color: { size: 3, data: colors }
});
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uSpread: { value: particleSpread },
uBaseSize: { value: particleBaseSize },
uSizeRandomness: { value: sizeRandomness },
uAlphaParticles: { value: alphaParticles ? 1 : 0 }
},
transparent: true,
depthTest: false
});
const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });
let animationFrameId;
let lastTime = performance.now();
let elapsed = 0;
const update = t => {
animationFrameId = requestAnimationFrame(update);
const delta = t - lastTime;
lastTime = t;
elapsed += delta * speed;
program.uniforms.uTime.value = elapsed * 0.001;
if (moveParticlesOnHover) {
particles.position.x = -mouseRef.current.x * particleHoverFactor;
particles.position.y = -mouseRef.current.y * particleHoverFactor;
} else {
particles.position.x = 0;
particles.position.y = 0;
}
if (!disableRotation) {
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;
particles.rotation.z += 0.01 * speed;
}
renderer.render({ scene: particles, camera });
};
animationFrameId = requestAnimationFrame(update);
return () => {
window.removeEventListener('resize', resize);
if (moveParticlesOnHover) {
container.removeEventListener('mousemove', handleMouseMove);
}
cancelAnimationFrame(animationFrameId);
if (container.contains(gl.canvas)) {
container.removeChild(gl.canvas);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
particleCount,
particleSpread,
speed,
moveParticlesOnHover,
particleHoverFactor,
alphaParticles,
particleBaseSize,
sizeRandomness,
cameraDistance,
disableRotation,
resolvedTheme,
mounted
]);
return <div ref={containerRef} className={className} />;
};
export default Particles;

View File

@@ -0,0 +1,60 @@
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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,93 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col rounded-xl pb-6 shadow-sm border",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,38 @@
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "./button"
export function ModeToggle() {
const [mounted, setMounted] = React.useState(false)
const { theme, setTheme } = useTheme()
// useEffect only runs on the client, so now we can safely show the UI
React.useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark")
}
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@@ -0,0 +1,146 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState, useRef } from "react";
interface ShootingStar {
id: number;
x: number;
y: number;
angle: number;
scale: number;
speed: number;
distance: number;
}
interface ShootingStarsProps {
minSpeed?: number;
maxSpeed?: number;
minDelay?: number;
maxDelay?: number;
starColor?: string;
trailColor?: string;
starWidth?: number;
starHeight?: number;
className?: string;
}
const getRandomStartPoint = () => {
const side = Math.floor(Math.random() * 4);
const offset = Math.random() * window.innerWidth;
switch (side) {
case 0:
return { x: offset, y: 0, angle: 45 };
case 1:
return { x: window.innerWidth, y: offset, angle: 135 };
case 2:
return { x: offset, y: window.innerHeight, angle: 225 };
case 3:
return { x: 0, y: offset, angle: 315 };
default:
return { x: 0, y: 0, angle: 45 };
}
};
export const ShootingStars: React.FC<ShootingStarsProps> = ({
minSpeed = 10,
maxSpeed = 30,
minDelay = 1200,
maxDelay = 4200,
starColor = "#9E00FF",
trailColor = "#2EB9DF",
starWidth = 10,
starHeight = 1,
className,
}) => {
const [star, setStar] = useState<ShootingStar | null>(null);
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
const createStar = () => {
const { x, y, angle } = getRandomStartPoint();
const newStar: ShootingStar = {
id: Date.now(),
x,
y,
angle,
scale: 1,
speed: Math.random() * (maxSpeed - minSpeed) + minSpeed,
distance: 0,
};
setStar(newStar);
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
setTimeout(createStar, randomDelay);
};
createStar();
return () => {};
}, [minSpeed, maxSpeed, minDelay, maxDelay]);
useEffect(() => {
const moveStar = () => {
if (star) {
setStar((prevStar) => {
if (!prevStar) return null;
const newX =
prevStar.x +
prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
const newY =
prevStar.y +
prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
const newDistance = prevStar.distance + prevStar.speed;
const newScale = 1 + newDistance / 100;
if (
newX < -20 ||
newX > window.innerWidth + 20 ||
newY < -20 ||
newY > window.innerHeight + 20
) {
return null;
}
return {
...prevStar,
x: newX,
y: newY,
distance: newDistance,
scale: newScale,
};
});
}
};
const animationFrame = requestAnimationFrame(moveStar);
return () => cancelAnimationFrame(animationFrame);
}, [star]);
return (
<svg
ref={svgRef}
className={cn("w-full h-full absolute inset-0", className)}
>
{star && (
<rect
key={star.id}
x={star.x}
y={star.y}
width={starWidth * star.scale}
height={starHeight}
fill="url(#gradient)"
transform={`rotate(${star.angle}, ${
star.x + (starWidth * star.scale) / 2
}, ${star.y + starHeight / 2})`}
/>
)}
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: trailColor, stopOpacity: 0 }} />
<stop
offset="100%"
style={{ stopColor: starColor, stopOpacity: 1 }}
/>
</linearGradient>
</defs>
</svg>
);
};

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,234 @@
"use client";
import React, { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle, Color } from 'ogl';
interface ThreadsProps {
color?: [number, number, number];
amplitude?: number;
distance?: number;
enableMouseInteraction?: boolean;
className?: string;
}
const vertexShader = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform vec3 uColor;
uniform float uAmplitude;
uniform float uDistance;
uniform vec2 uMouse;
#define PI 3.1415926538
const int u_line_count = 40;
const float u_line_width = 7.0;
const float u_line_blur = 10.0;
float Perlin2D(vec2 P) {
vec2 Pi = floor(P);
vec4 Pf_Pfmin1 = P.xyxy - vec4(Pi, Pi + 1.0);
vec4 Pt = vec4(Pi.xy, Pi.xy + 1.0);
Pt = Pt - floor(Pt * (1.0 / 71.0)) * 71.0;
Pt += vec2(26.0, 161.0).xyxy;
Pt *= Pt;
Pt = Pt.xzxz * Pt.yyww;
vec4 hash_x = fract(Pt * (1.0 / 951.135664));
vec4 hash_y = fract(Pt * (1.0 / 642.949883));
vec4 grad_x = hash_x - 0.49999;
vec4 grad_y = hash_y - 0.49999;
vec4 grad_results = inversesqrt(grad_x * grad_x + grad_y * grad_y)
* (grad_x * Pf_Pfmin1.xzxz + grad_y * Pf_Pfmin1.yyww);
grad_results *= 1.4142135623730950;
vec2 blend = Pf_Pfmin1.xy * Pf_Pfmin1.xy * Pf_Pfmin1.xy
* (Pf_Pfmin1.xy * (Pf_Pfmin1.xy * 6.0 - 15.0) + 10.0);
vec4 blend2 = vec4(blend, vec2(1.0 - blend));
return dot(grad_results, blend2.zxzx * blend2.wwyy);
}
float pixel(float count, vec2 resolution) {
return (1.0 / max(resolution.x, resolution.y)) * count;
}
float lineFn(vec2 st, float width, float perc, float offset, vec2 mouse, float time, float amplitude, float distance) {
float split_offset = (perc * 0.4);
float split_point = 0.1 + split_offset;
float amplitude_normal = smoothstep(split_point, 0.7, st.x);
float amplitude_strength = 0.5;
float finalAmplitude = amplitude_normal * amplitude_strength
* amplitude * (1.0 + (mouse.y - 0.5) * 0.2);
float time_scaled = time / 10.0 + (mouse.x - 0.5) * 1.0;
float blur = smoothstep(split_point, split_point + 0.05, st.x) * perc;
float xnoise = mix(
Perlin2D(vec2(time_scaled, st.x + perc) * 2.5),
Perlin2D(vec2(time_scaled, st.x + time_scaled) * 3.5) / 1.5,
st.x * 0.3
);
float y = 0.5 + (perc - 0.5) * distance + xnoise / 2.0 * finalAmplitude;
float line_start = smoothstep(
y + (width / 2.0) + (u_line_blur * pixel(1.0, iResolution.xy) * blur),
y,
st.y
);
float line_end = smoothstep(
y,
y - (width / 2.0) - (u_line_blur * pixel(1.0, iResolution.xy) * blur),
st.y
);
return clamp(
(line_start - line_end) * (1.0 - smoothstep(0.0, 1.0, pow(perc, 0.3))),
0.0,
1.0
);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float line_strength = 1.0;
for (int i = 0; i < u_line_count; i++) {
float p = float(i) / float(u_line_count);
line_strength *= (1.0 - lineFn(
uv,
u_line_width * pixel(1.0, iResolution.xy) * (1.0 - p),
p,
(PI * 1.0) * p,
uMouse,
iTime,
uAmplitude,
uDistance
));
}
float colorVal = 1.0 - line_strength;
fragColor = vec4(uColor * colorVal, colorVal);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`;
const Threads: React.FC<ThreadsProps> = ({
color = [1, 1, 1],
amplitude = 1,
distance = 0,
enableMouseInteraction = false,
className = '',
...rest
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const animationFrameId = useRef<number | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const renderer = new Renderer({ alpha: true });
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
container.appendChild(gl.canvas);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
iTime: { value: 0 },
iResolution: {
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
},
uColor: { value: new Color(...color) },
uAmplitude: { value: amplitude },
uDistance: { value: distance },
uMouse: { value: new Float32Array([0.5, 0.5]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
function resize() {
const { clientWidth, clientHeight } = container;
renderer.setSize(clientWidth, clientHeight);
program.uniforms.iResolution.value.r = clientWidth;
program.uniforms.iResolution.value.g = clientHeight;
program.uniforms.iResolution.value.b = clientWidth / clientHeight;
}
window.addEventListener('resize', resize);
resize();
const currentMouse = new Float32Array([0.5, 0.5]);
const targetMouse = new Float32Array([0.5, 0.5]);
function handleMouseMove(e: MouseEvent) {
const rect = container.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
targetMouse[0] = x;
targetMouse[1] = y;
}
function handleMouseLeave() {
targetMouse[0] = 0.5;
targetMouse[1] = 0.5;
}
if (enableMouseInteraction) {
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseleave', handleMouseLeave);
}
function update(t: number) {
if (enableMouseInteraction) {
const smoothing = 0.05;
currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1]);
program.uniforms.uMouse.value[0] = currentMouse[0];
program.uniforms.uMouse.value[1] = currentMouse[1];
} else {
program.uniforms.uMouse.value[0] = 0.5;
program.uniforms.uMouse.value[1] = 0.5;
}
program.uniforms.iTime.value = t * 0.001;
renderer.render({ scene: mesh });
animationFrameId.current = requestAnimationFrame(update);
}
animationFrameId.current = requestAnimationFrame(update);
return () => {
if (animationFrameId.current !== null) cancelAnimationFrame(animationFrameId.current);
window.removeEventListener('resize', resize);
if (enableMouseInteraction) {
container.removeEventListener('mousemove', handleMouseMove);
container.removeEventListener('mouseleave', handleMouseLeave);
}
if (container.contains(gl.canvas)) container.removeChild(gl.canvas);
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
}, [color, amplitude, distance, enableMouseInteraction]);
return <div ref={containerRef} className={`w-full h-full relative ${className}`} {...rest} />;
};
export default Threads;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Separator } from '@/components/ui/separator';
import { Github, Linkedin, Mail } from 'lucide-react';
import Link from 'next/link';
const FOOTER_MENU_ITEMS = [
{ name: 'About', path: '#about' },
{ name: 'Experience', path: '#experience' },
{ name: 'Projects', path: '#projects' },
{ name: 'Contact', path: '#contact' },
];
const SOCIAL_MEDIA = [
{
name: "GitHub",
icon: Github,
href: "https://github.com",
},
{
name: "Email",
icon: Mail,
href: "mailto:hello@example.com",
},
];
const Footer = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="my-12 mx-6 tablet:m-12 pc:mx-24 pc:mt-20">
<div className="flex items-center gap-8 mb-6 px-4">
{FOOTER_MENU_ITEMS.map((item) => (
<Link
key={item.name}
href={item.path}
className="text-lg font-light hover:opacity-70 transition-opacity"
>
{item.name}
</Link>
))}
</div>
<Separator />
<div className="flex w-full pt-4">
<div className="w-full flex flex-col justify-between items-center gap-4 tablet:flex-row">
<p className="text-sm text-center text-muted-foreground">
© {currentYear} All rights reserved
</p>
<div className="flex justify-center gap-8">
{SOCIAL_MEDIA.map(({ name, icon: Icon, href }) => (
<Link
key={name}
href={href}
target="_blank"
rel="noopener noreferrer"
className="hover:opacity-70 transition-opacity"
>
<Icon className="w-6 h-6" />
</Link>
))}
</div>
</div>
</div>
</footer>
);
}
export default Footer;

View File

@@ -0,0 +1,149 @@
'use client';
import { useState } from 'react';
import { Menu, X } from 'lucide-react';
import Link from 'next/link';
import { ModeToggle } from '@/components/ui/mode-toggle';
const HEADER_MENU_ITEMS = [
{ name: 'About', path: '#about' },
{ name: 'Experience', path: '#experience' },
{ name: 'Projects', path: '#projects' },
{ name: 'Contact', path: '#contact' },
];
interface HeaderProfileProps {
showImage?: boolean;
showName?: boolean;
imageClassName?: string;
hasOrder?: boolean;
}
const HeaderProfile = ({
showImage = true,
showName = true,
imageClassName,
hasOrder = true,
}: HeaderProfileProps) => {
return (
<Link
href="/"
className={hasOrder ? 'order-2 tablet:order-1' : ''}
>
<div className="flex items-center gap-4">
{showImage && (
<div
className={`w-10 h-10 pc:w-14 pc:h-14 rounded-full bg-gray-300 dark:bg-gray-600 ${imageClassName || ''}`}
/>
)}
{showName && (
<h1 className="text-base pc:text-lg transition-colors font-bold">
MINJO KIM
</h1>
)}
</div>
</Link>
);
};
const HeaderMenuItemsDesktop = () => {
return (
<div className="flex justify-center items-center">
{HEADER_MENU_ITEMS.map((item) => (
<div
key={item.name}
className="px-2 pc:px-6 transition-all"
>
<Link
href={item.path}
className="font-extralight text-sm pc:text-base duration-100 ease-in hover:border-b-2 hover:border-b-black hover:dark:border-b-white pb-1"
>
{item.name}
</Link>
</div>
))}
</div>
);
};
const HeaderMenuItemsMobile = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
onClick={() => setIsOpen(true)}
className="dark:invert hover:cursor-pointer"
aria-label="Open menu"
>
<Menu className="w-6 h-6" />
</button>
{isOpen && (
<div className="fixed inset-0 z-50 bg-background/95 backdrop-blur">
<div className="flex flex-col h-full">
<div className="flex justify-end p-4">
<button
onClick={() => setIsOpen(false)}
className="dark:invert hover:cursor-pointer"
aria-label="Close menu"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="flex flex-col items-center justify-between h-full py-16">
<div className="flex flex-col w-full h-full justify-center items-center gap-20">
<HeaderProfile
showImage={false}
hasOrder={false}
/>
<div className="flex flex-col items-center gap-12">
{HEADER_MENU_ITEMS.map(item => (
<div
key={item.name}
className="px-4 pc:px-8 transition-all items-center"
>
<Link
href={item.path}
onClick={() => setIsOpen(false)}
className="text-xl duration-100 ease-in hover:border-b-2 hover:border-b-brand-primary hover:dark:border-b-white pb-1"
>
{item.name}
</Link>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)}
</>
);
};
const Header = () => {
return (
<header
className="fixed h-[70px] top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div className="flex justify-between items-center tracking-wider tablet:tracking-widest font-brand-book px-4 pc:px-8 py-4 max-w-[1920px] mx-auto">
<HeaderProfile
showImage={false}
/>
<div className="order-1 tablet:order-2 flex items-center">
<div className="tablet:hidden">
<HeaderMenuItemsMobile />
</div>
<div className="hidden tablet:block">
<HeaderMenuItemsDesktop />
</div>
<ModeToggle />
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

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

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;

9924
services/nextjs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "portfolio",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gsap": "^3.13.0",
"lucide-react": "^0.552.0",
"next": "16.0.1",
"next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "19.2.0",
"react-dom": "19.2.0",
"shadcn": "^3.5.0",
"tailwind-merge": "^3.3.1",
"three": "^0.181.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1,17 @@
'use client';
import * as React from 'react';
import dynamic from 'next/dynamic';
import { ThemeProviderProps } from 'next-themes';
const NextThemesProvider = dynamic(
() => import('next-themes').then((e) => e.ThemeProvider),
{
ssr: false,
}
);
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1,8 @@
@font-face {
font-family: 'SupremeLLTT-Book';
src: url('/fonts/brand/SupremeLLTT-Book.woff2') format('woff2');
}
.font-brand-book {
font-family: 'SupremeLLTT-Book', sans-serif;
}

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}