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
41
services/nextjs/.gitignore
vendored
Normal 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
@@ -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.
|
||||
BIN
services/nextjs/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
264
services/nextjs/app/globals.css
Normal 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;
|
||||
}
|
||||
34
services/nextjs/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
services/nextjs/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
services/nextjs/components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
185
services/nextjs/components/landing/contact.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
|
||||
53
services/nextjs/components/landing/hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
102
services/nextjs/components/landing/projects.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
17
services/nextjs/components/landing/section-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
93
services/nextjs/components/landing/skills.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
51
services/nextjs/components/ui/Meteo.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
249
services/nextjs/components/ui/Particles.jsx
Normal 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;
|
||||
60
services/nextjs/components/ui/button.tsx
Normal 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 }
|
||||
|
||||
93
services/nextjs/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
|
||||
200
services/nextjs/components/ui/dropdown-menu.tsx
Normal 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,
|
||||
}
|
||||
21
services/nextjs/components/ui/input.tsx
Normal 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 }
|
||||
38
services/nextjs/components/ui/mode-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
29
services/nextjs/components/ui/separator.tsx
Normal 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 }
|
||||
|
||||
146
services/nextjs/components/ui/shooting-stars.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
18
services/nextjs/components/ui/textarea.tsx
Normal 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 }
|
||||
234
services/nextjs/components/ui/threads.tsx
Normal 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;
|
||||
68
services/nextjs/components/widgets/Footer.tsx
Normal 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;
|
||||
|
||||
149
services/nextjs/components/widgets/Header.tsx
Normal 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;
|
||||
18
services/nextjs/eslint.config.mjs
Normal 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;
|
||||
7
services/nextjs/lib/utils.ts
Normal 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))
|
||||
}
|
||||
|
||||
7
services/nextjs/next.config.ts
Normal 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
41
services/nextjs/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
services/nextjs/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
services/nextjs/providers/theme-provider.tsx
Normal 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>;
|
||||
}
|
||||
|
||||
1
services/nextjs/public/file.svg
Normal 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 |
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Black.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-BlackItalic.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Bold.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-BoldFlat.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-BoldItalic.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Book.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-BookItalic.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Italic.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Light.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-LightItalic.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Medium.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Regular.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-Thin.woff2
Normal file
BIN
services/nextjs/public/fonts/brand/SupremeLLTT-ThinItalic.woff2
Normal file
8
services/nextjs/public/fonts/generated-fonts.css
Normal 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;
|
||||
}
|
||||
|
||||
1
services/nextjs/public/globe.svg
Normal 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 |
BIN
services/nextjs/public/jaejadle/home.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
services/nextjs/public/joossam/home.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
services/nextjs/public/joossam/main.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
services/nextjs/public/jotion/home.png
Normal file
|
After Width: | Height: | Size: 466 KiB |
BIN
services/nextjs/public/jovies/home.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
1
services/nextjs/public/next.svg
Normal 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 |
BIN
services/nextjs/public/portfolio/home.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
services/nextjs/public/todoList/home.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
1
services/nextjs/public/vercel.svg
Normal 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 |
1
services/nextjs/public/window.svg
Normal 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 |
41
services/nextjs/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||