REFACTOR(repo): simplify project structure
- Move services/nextjs/ to nextjs/ - Move Dockerfile.prod to Dockerfile at root - Remove deploy/ folder (K8s manifests moved to K3S-HOME/web-apps) - Remove .gitea/ workflows - Update GitHub Actions for new structure - Remove develop branch triggers
41
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
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.
|
||||
54
nextjs/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
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";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Portfolio - Full Stack Developer",
|
||||
description: "Creating beautiful and functional web experiences with modern technologies",
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
storageKey="portfolio-theme"
|
||||
>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
31
nextjs/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Hero from '@/components/landing/hero';
|
||||
import About from '@/components/landing/about';
|
||||
import Skills from '@/components/landing/skills';
|
||||
import Projects from '@/components/landing/projects';
|
||||
import Contact from '@/components/landing/contact';
|
||||
import GrafanaPage from '@/components/landing/grafana';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div id="hero">
|
||||
<Hero />
|
||||
</div>
|
||||
<div id="about">
|
||||
<About />
|
||||
</div>
|
||||
<div id="skills">
|
||||
<Skills />
|
||||
</div>
|
||||
<div id="projects">
|
||||
<Projects />
|
||||
</div>
|
||||
<div id="monitoring">
|
||||
<GrafanaPage />
|
||||
</div>
|
||||
<div id="contact">
|
||||
<Contact />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
nextjs/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 152 KiB |
285
nextjs/app/globals.css
Normal file
@@ -0,0 +1,285 @@
|
||||
@import "tailwindcss";
|
||||
@import '../public/fonts/generated-fonts.css';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--breakpoint-smalltablet: 600px;
|
||||
--breakpoint-tablet: 990px;
|
||||
--breakpoint-desktop: 1200px;
|
||||
--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 smalltablet (@media (width >= 600px));
|
||||
@custom-variant tablet (@media (width >= 990px));
|
||||
@custom-variant desktop (@media (width >= 1200px));
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
@apply flex flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet:py-20 mt-[70px];
|
||||
}
|
||||
|
||||
.theme-background {
|
||||
@apply bg-gray-100 dark:bg-neutral-800;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
24
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"
|
||||
}
|
||||
}
|
||||
90
nextjs/components/landing/about.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import SectionHeader from '@/components/landing/section-header';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { User, Calendar, MapPin, Mail, GraduationCap, LucideIcon, Github } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface InfoItemProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function InfoItem({ icon: Icon, label, value }: InfoItemProps) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 smalltablet:gap-4">
|
||||
<div className="flex items-center justify-center min-w-8 min-h-8 smalltablet:min-w-10 smalltablet:min-h-10 rounded-full theme-background">
|
||||
<Icon className="w-4 h-4 smalltablet:w-5 smalltablet:h-5" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm smalltablet:text-base font-semibold">{label}</span>
|
||||
<span className="text-xs smalltablet:text-sm text-muted-foreground">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function About() {
|
||||
const t = useTranslations('about');
|
||||
|
||||
const PERSONAL_INFO = [
|
||||
{ icon: User, label: t('name'), value: 'MINJO KIM' },
|
||||
{ icon: GraduationCap, label: t('education'), value: 'Yonsei University (Computer Science)' },
|
||||
{ icon: Calendar, label: t('birthday'), value: '1997.01.17' },
|
||||
{ icon: MapPin, label: t('location'), value: 'Seoul, Korea' },
|
||||
{ icon: Github, label: t('github'), value: 'Mayne0213' },
|
||||
{ icon: Mail, label: t('email'), value: 'minjo.dev@gmail.com' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-muted">
|
||||
<main className="flex flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet:py-20">
|
||||
<SectionHeader
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
/>
|
||||
|
||||
<section className="grid grid-cols-1 tablet:grid-cols-2 gap-6 smalltablet:gap-7 tablet:gap-8 max-w-7xl w-full">
|
||||
{/* Personal Info */}
|
||||
<div className="flex flex-col gap-3 smalltablet:gap-4 h-full">
|
||||
<Card className="p-4 smalltablet:p-5 tablet:p-6 flex-1 gap-3 smalltablet:gap-4">
|
||||
<h3 className="text-xl smalltablet:text-2xl font-bold">{t('personalInfo')}</h3>
|
||||
<Separator/>
|
||||
<div className="grid grid-cols-1 smalltablet:grid-cols-2 gap-4 smalltablet:gap-5 tablet:gap-6 items-center h-full">
|
||||
{PERSONAL_INFO.map((info, index) => (
|
||||
<InfoItem
|
||||
key={index}
|
||||
icon={info.icon}
|
||||
label={info.label}
|
||||
value={info.value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Who I Am */}
|
||||
<div className="flex flex-col gap-3 smalltablet:gap-4 h-full">
|
||||
<Card className="p-4 smalltablet:p-5 tablet:p-6 flex-1">
|
||||
<div className="flex flex-col gap-3 smalltablet:gap-4">
|
||||
<h3 className="text-xl smalltablet:text-2xl font-bold">{t('whoIAm')}</h3>
|
||||
<Separator/>
|
||||
<p className="text-sm smalltablet:text-base text-muted-foreground tablet:text-justify leading-relaxed">
|
||||
{t('bio1')}
|
||||
</p>
|
||||
<p className="text-sm smalltablet:text-base text-muted-foreground tablet:text-justify leading-relaxed">
|
||||
{t('bio2')}
|
||||
</p>
|
||||
<p className="text-sm smalltablet:text-base text-muted-foreground tablet:text-justify leading-relaxed">
|
||||
{t('bio3')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
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-muted">
|
||||
<main className="flex flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet: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 tablet:grid-cols-2 gap-6 smalltablet:gap-7 tablet:gap-8 max-w-6xl mx-auto w-full">
|
||||
{/* Contact Info & Social Links */}
|
||||
<Card className="p-5 smalltablet:p-6 tablet:p-8 hover:shadow-lg transition-shadow">
|
||||
<h3 className="font-bold text-lg smalltablet:text-xl mb-4 smalltablet:mb-5 tablet:mb-6 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 smalltablet:w-5 smalltablet:h-5 text-primary" />
|
||||
Contact Information
|
||||
</h3>
|
||||
<div className="flex flex-col gap-3 smalltablet: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-3 smalltablet:gap-4 p-3 smalltablet:p-4 rounded-lg hover:bg-accent transition-colors group"
|
||||
>
|
||||
<div className={`flex items-center justify-center w-10 h-10 smalltablet:w-12 smalltablet:h-12 rounded-full bg-muted group-hover:scale-110 transition-transform ${method.color}`}>
|
||||
<method.icon className="w-5 h-5 smalltablet:w-6 smalltablet:h-6" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs smalltablet:text-sm text-muted-foreground">{method.label}</p>
|
||||
<p className="text-sm smalltablet:text-base font-medium truncate">{method.value}</p>
|
||||
</div>
|
||||
<ExternalLink className="w-3.5 h-3.5 smalltablet:w-4 smalltablet:h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Contact Form */}
|
||||
<Card className="p-5 smalltablet:p-6 tablet:p-8 hover:shadow-xl transition-shadow">
|
||||
<h3 className="font-bold text-xl smalltablet:text-2xl mb-1.5 smalltablet:mb-2">Send Me a Message</h3>
|
||||
<p className="text-sm smalltablet:text-base text-muted-foreground mb-6 smalltablet: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-4 smalltablet:gap-5 tablet:gap-6">
|
||||
<div className="grid tablet:grid-cols-2 gap-4 smalltablet:gap-5 tablet:gap-6">
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2">
|
||||
<label htmlFor="name" className="text-xs smalltablet: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-10 smalltablet:h-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2">
|
||||
<label htmlFor="email" className="text-xs smalltablet: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-10 smalltablet:h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2">
|
||||
<label htmlFor="subject" className="text-xs smalltablet: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-10 smalltablet:h-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2">
|
||||
<label htmlFor="message" className="text-xs smalltablet: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={6}
|
||||
className="resize-none smalltablet:rows-8"
|
||||
/>
|
||||
</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-3.5 h-3.5 smalltablet:w-4 smalltablet:h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
253
nextjs/components/landing/grafana.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import SectionHeader from "@/components/landing/section-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Section = "overview" | "resources" | "kubernetes" | "network";
|
||||
|
||||
const sections: { id: Section; label: string; panels: number }[] = [
|
||||
{ id: "overview", label: "Overview", panels: 8 },
|
||||
{ id: "resources", label: "Resources", panels: 8 },
|
||||
{ id: "kubernetes", label: "Kubernetes", panels: 4 },
|
||||
{ id: "network", label: "Network", panels: 6 },
|
||||
];
|
||||
|
||||
// Loading Skeleton 컴포넌트
|
||||
const LoadingSkeleton = () => (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 dark:bg-gray-800 animate-pulse">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-8 h-8 border-4 border-gray-300 dark:border-gray-600 border-t-gray-600 dark:border-t-gray-300 rounded-full animate-spin" />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Loading dashboard...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function GrafanaPage() {
|
||||
const [activeSection, setActiveSection] = useState<Section>("overview");
|
||||
const { theme } = useTheme();
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
// Grafana iframe URL 생성 함수
|
||||
const getGrafanaUrl = (panelId: number, themeType: "dark" | "light") => {
|
||||
const baseUrl = "https://grafana0213.kro.kr/d-solo/k8s_views_global/kubernetes-views-global";
|
||||
return `${baseUrl}?orgId=1&refresh=30s&theme=${themeType}&panelId=${panelId}`;
|
||||
};
|
||||
|
||||
// iframe 래퍼 컴포넌트
|
||||
const GrafanaPanel = ({ panelId }: { panelId: number }) => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{/* {(!loaded || isLoading) && <LoadingSkeleton />} */}
|
||||
{theme === "dark" ? (
|
||||
<iframe
|
||||
key={`${panelId}-dark-${theme}`}
|
||||
src={getGrafanaUrl(panelId, "dark")}
|
||||
className="w-full h-full border"
|
||||
style={{ display: loaded ? 'block' : 'none' }}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
) : (
|
||||
<iframe
|
||||
key={`${panelId}-light-${theme}`}
|
||||
src={getGrafanaUrl(panelId, "light")}
|
||||
className="w-full h-full"
|
||||
style={{ display: loaded ? 'block' : 'none' }}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet:py-20" ref={sectionRef}>
|
||||
<SectionHeader
|
||||
title="Monitoring"
|
||||
description="Real-time Kubernetes cluster monitoring with Grafana dashboards"
|
||||
/>
|
||||
|
||||
{/* 섹션 선택 버튼 */}
|
||||
<div className="flex gap-1.5 smalltablet:gap-2 flex-wrap justify-center">
|
||||
{sections.map((section) => (
|
||||
<Button
|
||||
key={section.id}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
variant={activeSection === section.id ? "default" : "outline"}
|
||||
className="transition-all text-[10px] smalltablet:text-xs tablet:text-sm px-2 smalltablet:px-3 py-1 smalltablet:py-2 h-auto"
|
||||
>
|
||||
{section.label}
|
||||
<span className="ml-1 smalltablet:ml-1.5 tablet:ml-2 text-[9px] smalltablet:text-xs opacity-60">
|
||||
({section.panels})
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Overview 섹션 */}
|
||||
{activeSection === "overview" && (
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full h-full">
|
||||
{/* 왼쪽 2개 열 (CPU, RAM) */}
|
||||
<div className="flex flex-col smalltablet:flex-row gap-1.5 smalltablet:gap-2 w-full desktop:flex-1">
|
||||
{/* Global CPU Usage 열 */}
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2 w-full smalltablet:flex-1">
|
||||
{/* Global CPU Usage (id:77) - w:6, h:8 */}
|
||||
<div className="w-full aspect-5/4 theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={77} />
|
||||
</div>
|
||||
{/* CPU Usage (id:37) - w:6, h:4 */}
|
||||
<div className="w-full aspect-5/2 theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={37} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global RAM Usage 열 */}
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2 w-full smalltablet:flex-1">
|
||||
{/* Global RAM Usage (id:78) - w:6, h:8 */}
|
||||
<div className="w-full aspect-5/4 theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={78} />
|
||||
</div>
|
||||
{/* RAM Usage (id:39) - w:6, h:4 */}
|
||||
<div className="w-full aspect-5/2 theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={39} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽 넓은 열 (Kubernetes Resource Count) */}
|
||||
{/* Kubernetes Resource Count (id:52) - w:10, h:12 */}
|
||||
<div className="w-full aspect-5/7 smalltablet:aspect-video desktop:flex-1 desktop:max-w-[40%] theme-background rounded-md smalltablet:rounded-lg overflow-hidden flex items-stretch">
|
||||
<GrafanaPanel panelId={52} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resources 섹션 */}
|
||||
{activeSection === "resources" && (
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2 w-full">
|
||||
{/* Row 1 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* Cluster CPU Utilization (id:72) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={72} />
|
||||
</div>
|
||||
{/* Cluster Memory Utilization (id:55) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={55} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* CPU Utilization by namespace (id:46) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={46} />
|
||||
</div>
|
||||
{/* Memory Utilization by namespace (id:50) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* CPU Utilization by instance (id:54) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={54} />
|
||||
</div>
|
||||
{/* Memory Utilization by instance (id:73) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={73} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* CPU Throttled seconds by namespace (id:82) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={82} />
|
||||
</div>
|
||||
{/* CPU Core Throttled by instance (id:83) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={83} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kubernetes 섹션 */}
|
||||
{activeSection === "kubernetes" && (
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2 w-full">
|
||||
{/* Row 1 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* Kubernetes Pods QoS classes (id:84) - w:12, h:9 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={84} />
|
||||
</div>
|
||||
{/* Kubernetes Pods Status Reason (id:85) - w:12, h:9 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={85} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* OOM Events by namespace (id:87) - w:12, h:9 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={87} />
|
||||
</div>
|
||||
{/* Container Restarts by namespace (id:88) - w:12, h:9 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={88} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network 섹션 */}
|
||||
{activeSection === "network" && (
|
||||
<div className="flex flex-col gap-1.5 smalltablet:gap-2 w-full">
|
||||
{/* Row 1 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* Global Network Utilization by device (id:44) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={44} />
|
||||
</div>
|
||||
{/* Network Saturation - Packets dropped (id:53) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={53} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* Network Received by namespace (id:79) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={79} />
|
||||
</div>
|
||||
{/* Total Network Received by instance (id:80) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={80} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="flex flex-col desktop:flex-row gap-1.5 smalltablet:gap-2 justify-center w-full">
|
||||
{/* Network Received (without loopback) by instance (id:56) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={56} />
|
||||
</div>
|
||||
{/* Network Received (loopback only) by instance (id:81) - w:12, h:8 */}
|
||||
<div className="w-full desktop:flex-1 aspect-5/4 smalltablet:aspect-video theme-background rounded-md smalltablet:rounded-lg overflow-hidden">
|
||||
<GrafanaPanel panelId={81} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
35
nextjs/components/landing/hero.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Particles from '@/components/ui/Particles';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Hero() {
|
||||
const t = useTranslations('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-8 smalltablet:gap-10 tablet:gap-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-8 smalltablet:gap-10 tablet:gap-12 px-4 smalltablet:px-5">
|
||||
<h2 className="font-bold text-4xl smalltablet:text-5xl tablet:text-6xl desktop: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">
|
||||
{t('title')}
|
||||
</h2>
|
||||
<div className="max-w-sm smalltablet:max-w-xl tablet:max-w-2xl desktop:max-w-3xl">
|
||||
<p className="text-lg smalltablet:text-xl tablet:text-2xl desktop:text-3xl text-center leading-[150%] smalltablet:leading-[160%]">
|
||||
{t('description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
188
nextjs/components/landing/projects.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
import { Separator } from '../ui/separator';
|
||||
import { Github, ExternalLink, FileText } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import SectionHeader from './section-header';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import joossamHome from '@/public/joossam/home.png';
|
||||
import jotionHome from '@/public/jotion/home.png';
|
||||
import joossamMain from '@/public/joossam/main.png';
|
||||
import jaejadleHome from '@/public/jaejadle/home.png';
|
||||
import portfolioHome from '@/public/portfolio/home.png';
|
||||
import todoListHome from '@/public/todoList/home.png';
|
||||
import joviesHome from '@/public/jovies/home.png';
|
||||
import docusaurusIcon from '@/public/icons/docusaurus.svg';
|
||||
|
||||
interface ProjectCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
imageSrc: StaticImageData;
|
||||
liveUrl?: string;
|
||||
devUrl?: string;
|
||||
githubUrl?: string;
|
||||
docusaurusUrl?: string;
|
||||
jotionUrl?: string;
|
||||
}
|
||||
|
||||
function ProjectCard({ title, description, tags, imageSrc, liveUrl, devUrl, githubUrl, docusaurusUrl, jotionUrl }: 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"
|
||||
placeholder="blur"
|
||||
sizes="(max-width: 600px) 100vw, (max-width: 990px) 100vw, (max-width: 1200px) 50vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 smalltablet:px-5 tablet:px-6 pt-4 smalltablet:pt-5 tablet:pt-6 pb-3 smalltablet:pb-4 flex flex-col gap-2 smalltablet:gap-3">
|
||||
<h3 className="font-semibold text-lg smalltablet:text-xl tablet:text-2xl">{title}</h3>
|
||||
<p className="text-sm smalltablet:text-base text-muted-foreground font-extralight">{description}</p>
|
||||
<div className="flex gap-1.5 smalltablet:gap-2 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<Button key={tag} variant="outline" className="text-[10px] smalltablet:text-xs px-2 smalltablet:px-3 py-0.5 smalltablet:py-1 h-auto">
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 smalltablet:px-5 tablet:px-6">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="px-4 smalltablet:px-5 tablet:px-6 py-3 smalltablet:py-4 flex gap-3 smalltablet:gap-4 flex-wrap">
|
||||
{liveUrl && (
|
||||
<Link href={liveUrl} target="_blank" rel="noopener noreferrer" aria-label="Live Demo">
|
||||
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<ExternalLink className="w-4 h-4 smalltablet:w-5 smalltablet:h-5" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{devUrl && (
|
||||
<Link href={devUrl} target="_blank" rel="noopener noreferrer" aria-label="Dev Site">
|
||||
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<ExternalLink className="w-4 h-4 smalltablet:w-5 smalltablet:h-5" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{githubUrl && (
|
||||
<Link href={githubUrl} target="_blank" rel="noopener noreferrer" aria-label="GitHub">
|
||||
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<Github className="w-4 h-4 smalltablet:w-5 smalltablet:h-5" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{docusaurusUrl && (
|
||||
<Link href={docusaurusUrl} target="_blank" rel="noopener noreferrer" aria-label="Docusaurus">
|
||||
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<Image
|
||||
src={docusaurusIcon}
|
||||
alt="Docusaurus"
|
||||
className="w-4 h-4 smalltablet:w-5 smalltablet:h-5"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{jotionUrl && (
|
||||
<Link href={jotionUrl} target="_blank" rel="noopener noreferrer" aria-label="Jotion">
|
||||
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<FileText className="w-4 h-4 smalltablet:w-5 smalltablet:h-5" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Projects() {
|
||||
const t = useTranslations('projects');
|
||||
|
||||
return (
|
||||
<main className="flex bg-muted flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet:py-20">
|
||||
<SectionHeader
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
/>
|
||||
|
||||
<section className="grid grid-cols-1 desktop:grid-cols-2 gap-4 smalltablet:gap-5 tablet:gap-6 desktop:gap-8 max-w-[1440px] w-full">
|
||||
<ProjectCard
|
||||
title={t("joossam.title")}
|
||||
description={t("joossam.description")}
|
||||
tags={['Next.js', 'TypeScript', 'Prisma', 'MySQL', 'NextAuth.js']}
|
||||
imageSrc={joossamHome}
|
||||
liveUrl="https://joossam.com"
|
||||
devUrl="https://joossam.com"
|
||||
githubUrl="https://github.com/minjo-on/joossam"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("jotion.title")}
|
||||
description={t("jotion.description")}
|
||||
tags={['Next.js', 'React', 'Convex', 'Clerk', 'BlockNote']}
|
||||
imageSrc={jotionHome}
|
||||
githubUrl="https://github.com/minjo-on/jotion"
|
||||
liveUrl="https://jotion.minjo.xyz"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("youniClassic.title")}
|
||||
description={t("youniClassic.description")}
|
||||
tags={['Next.js', 'TypeScript', 'MySQL', 'Prisma']}
|
||||
imageSrc={joossamMain}
|
||||
githubUrl="https://github.com/minjo-on/youniClassic"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("jaejadle.title")}
|
||||
description={t("jaejadle.description")}
|
||||
tags={['Next.js', 'TypeScript', 'Tailwind CSS', 'Shadcn/ui']}
|
||||
imageSrc={jaejadleHome}
|
||||
githubUrl="https://github.com/minjo-on/jaejadle"
|
||||
liveUrl="https://jaejadle.com"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("portfolio.title")}
|
||||
description={t("portfolio.description")}
|
||||
tags={['Next.js', 'TypeScript', 'Docker', 'Kubernetes', 'ArgoCD']}
|
||||
imageSrc={portfolioHome}
|
||||
githubUrl="https://github.com/minjo-on/portfolio"
|
||||
liveUrl="https://minjo.xyz"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("todoList.title")}
|
||||
description={t("todoList.description")}
|
||||
tags={['React', 'TypeScript', 'Vite', 'Tailwind CSS']}
|
||||
imageSrc={todoListHome}
|
||||
githubUrl="https://github.com/minjo-on/todoList"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
<ProjectCard
|
||||
title={t("jovies.title")}
|
||||
description={t("jovies.description")}
|
||||
tags={['Next.js', 'TypeScript', 'TMDB API', 'Tailwind CSS']}
|
||||
imageSrc={joviesHome}
|
||||
githubUrl="https://github.com/minjo-on/jovies"
|
||||
liveUrl="https://jovies.minjo.xyz"
|
||||
docusaurusUrl="#"
|
||||
jotionUrl="#"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
17
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-3 smalltablet:gap-4 max-w-xl smalltablet:max-w-2xl tablet:max-w-3xl text-center px-4">
|
||||
<h2 className="font-bold text-3xl smalltablet:text-4xl tablet:text-5xl desktop: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-base smalltablet:text-lg tablet:text-xl text-muted-foreground max-w-sm smalltablet:max-w-lg tablet:max-w-2xl">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
nextjs/components/landing/skills.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Code, Server, Layout, Globe, Router, TestTube2, LucideIcon, ChevronDown } from 'lucide-react';
|
||||
import SectionHeader from './section-header';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface SkillCardProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
mainSkills: string[];
|
||||
subSkills: string[];
|
||||
}
|
||||
|
||||
function SkillCard({ icon: Icon, title, description, mainSkills, subSkills }: SkillCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="p-4 smalltablet:p-5 w-full h-full gap-3 smalltablet:gap-4 relative">
|
||||
{/* Toggle Button - Top Right Corner */}
|
||||
{subSkills.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="absolute top-4 right-4 smalltablet:top-6 smalltablet:right-6 p-2 h-8 w-8"
|
||||
>
|
||||
<ChevronDown className={`w-16 h-16 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 smalltablet:gap-4">
|
||||
<div className="flex items-center justify-center min-w-10 min-h-10 smalltablet:min-w-12 smalltablet:min-h-12 rounded-full bg-muted w-fit">
|
||||
<Icon className="min-w-6 min-h-6 smalltablet:min-w-8 smalltablet:min-h-8" />
|
||||
</div>
|
||||
<CardTitle>
|
||||
<h3 className="text-base smalltablet:text-lg">{title}</h3>
|
||||
</CardTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 smalltablet:gap-6 justify-between h-full">
|
||||
<p className="text-sm smalltablet:text-base font-extralight">{description}</p>
|
||||
<div className="flex flex-col gap-3 smalltablet:gap-4">
|
||||
<Separator />
|
||||
{/* Main Skills */}
|
||||
<div className="flex gap-2 smalltablet:gap-3 flex-wrap">
|
||||
{mainSkills.map((skill) => (
|
||||
<span key={skill} className="text-xs smalltablet:text-sm font-medium text-gray-800 dark:text-gray-100">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* Sub Skills - Toggleable */}
|
||||
{subSkills.length > 0 && (
|
||||
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${isExpanded ? 'max-h-96' : 'max-h-0'}`}>
|
||||
<Separator className="opacity-50 mb-3 smalltablet:mb-4" />
|
||||
<div className="flex gap-2 smalltablet:gap-3 flex-wrap">
|
||||
{subSkills.map((skill) => (
|
||||
<span key={skill} className="text-xs smalltablet:text-sm font-extralight text-gray-600 dark:text-gray-300 opacity-80">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Skills() {
|
||||
const t = useTranslations('skills');
|
||||
|
||||
return (
|
||||
<main className="flex max-w-7xl mx-auto flex-col items-center justify-center gap-12 smalltablet:gap-14 tablet:gap-16 p-4 smalltablet:p-6 tablet:p-8 py-16 smalltablet:py-18 tablet:py-20">
|
||||
<SectionHeader
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 smalltablet:grid-cols-2 desktop:grid-cols-3 gap-4 smalltablet:gap-5 tablet:gap-6">
|
||||
<SkillCard
|
||||
icon={Code}
|
||||
title={t('frontend.title')}
|
||||
description={t('frontend.description')}
|
||||
mainSkills={['HTML', 'CSS', 'JavaScript', 'TypeScript', 'React', 'Next.js']}
|
||||
subSkills={['Tailwind', 'Zustand', 'FSD', 'Radix UI', 'Shadcn/ui', 'Chart.js', 'React-Query', 'React-Hook-Form', 'Zod']}
|
||||
/>
|
||||
<SkillCard
|
||||
icon={Server}
|
||||
title={t('backend.title')}
|
||||
description={t('backend.description')}
|
||||
mainSkills={['Node.js', 'Prisma ORM', 'REST API', 'MySQL']}
|
||||
subSkills={['NextAuth.js', 'JWT', 'bcrypt', 'MariaDB']}
|
||||
/>
|
||||
<SkillCard
|
||||
icon={Layout}
|
||||
title={t('devops.title')}
|
||||
description={t('devops.description')}
|
||||
mainSkills={['Git', 'Docker', 'K8s', 'Github Actions', 'ArgoCD', 'Nginx ingress']}
|
||||
subSkills={['Kustomize', 'Helm', 'App of Apps Pattern', 'EC2', 'Lightsail', 'Vercel']}
|
||||
/>
|
||||
<SkillCard
|
||||
icon={TestTube2}
|
||||
title={t('monitoring.title')}
|
||||
description={t('monitoring.description')}
|
||||
mainSkills={['Prometheus', 'Grafana']}
|
||||
subSkills={['Alertmanager', 'Node Exporter', 'kube-state-metrics']}
|
||||
/>
|
||||
<SkillCard
|
||||
icon={Router}
|
||||
title={t('testing.title')}
|
||||
description={t('testing.description')}
|
||||
mainSkills={['Jest', 'Cypress', 'Puppeteer', 'Playwright']}
|
||||
subSkills={[]}
|
||||
/>
|
||||
<SkillCard
|
||||
icon={Globe}
|
||||
title={t('hosting.title')}
|
||||
description={t('hosting.description')}
|
||||
mainSkills={['AWS EC2', 'AWS Lightsail', 'Vercel', 'Nginx']}
|
||||
subSkills={['SSL/TLS', 'Domain Management', 'Load Balancing', 'CDN']}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
249
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
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
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
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
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 }
|
||||
58
nextjs/components/ui/language-toggle.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { usePathname, useRouter } from '@/i18n/routing';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'ko', name: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||
];
|
||||
|
||||
export function LanguageToggle() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
const currentLocale = params.locale as string;
|
||||
|
||||
const handleLanguageChange = (locale: string) => {
|
||||
router.replace(pathname, { locale: locale as any });
|
||||
};
|
||||
|
||||
const currentLanguage = LANGUAGES.find(lang => lang.code === currentLocale) || LANGUAGES[0];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="ml-2">
|
||||
<Languages className="h-[1.2rem] w-[1.2rem]" />
|
||||
<span className="sr-only">Toggle language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{LANGUAGES.map((language) => (
|
||||
<DropdownMenuItem
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<span className="mr-2">{language.flag}</span>
|
||||
{language.name}
|
||||
{currentLanguage.code === language.code && (
|
||||
<span className="ml-2">✓</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
38
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
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 }
|
||||
|
||||
18
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 }
|
||||
68
nextjs/components/widgets/Footer.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Github, Mail } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const FOOTER_MENU_ITEMS = [
|
||||
{ name: 'About', path: '#about' },
|
||||
{ name: 'Skills', path: '#skills' },
|
||||
{ name: 'Projects', path: '#projects' },
|
||||
{ name: 'Monitoring', path: '#monitoring' },
|
||||
{ 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-8 mx-4 smalltablet:my-10 smalltablet:mx-6 tablet:m-12 desktop:mx-24 desktop:mt-20">
|
||||
<div className="flex flex-wrap items-center gap-4 smalltablet:gap-6 tablet:gap-8 mb-4 smalltablet:mb-6 px-2 smalltablet:px-4">
|
||||
{FOOTER_MENU_ITEMS.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.path}
|
||||
className="text-sm smalltablet:text-base tablet:text-lg font-light hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex w-full pt-3 smalltablet:pt-4">
|
||||
<div className="w-full flex flex-col justify-between items-center gap-3 smalltablet:gap-4 tablet:flex-row">
|
||||
<p className="text-xs smalltablet:text-sm text-center text-muted-foreground">
|
||||
© {currentYear} All rights reserved
|
||||
</p>
|
||||
<div className="flex justify-center gap-6 smalltablet: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-5 h-5 smalltablet:w-6 smalltablet:h-6" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
|
||||
152
nextjs/components/widgets/Header.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { Link } from '@/i18n/routing';
|
||||
import { ModeToggle } from '@/components/ui/mode-toggle';
|
||||
import { LanguageToggle } from '@/components/ui/language-toggle';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const HEADER_MENU_ITEMS = [
|
||||
{ key: 'about', path: '/#about' },
|
||||
{ key: 'skills', path: '/#skills' },
|
||||
{ key: 'projects', path: '/#projects' },
|
||||
{ key: 'monitoring', path: '/#monitoring' },
|
||||
{ key: 'contact', path: '/#contact' },
|
||||
];
|
||||
|
||||
const HeaderProfile = () => {
|
||||
return (
|
||||
<Link href="/#top" onClick={(e) => handleScrollClick(e, '/#top')} >
|
||||
<h1 className="text-base smalltablet:text-lg desktop:text-lg font-bold">
|
||||
MINJO
|
||||
</h1>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const handleScrollClick = (
|
||||
e: React.MouseEvent<HTMLAnchorElement>,
|
||||
path: string,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
const sectionId = path.replace('/#', '');
|
||||
const element = document.getElementById(sectionId);
|
||||
const navbarHeight = 70;
|
||||
|
||||
if (element) {
|
||||
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
|
||||
const offsetPosition = elementPosition - navbarHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else {
|
||||
// 요소를 찾지 못하면 최상단으로 스크롤
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const HeaderMenuItemsDesktop = () => {
|
||||
const t = useTranslations('header');
|
||||
|
||||
return (
|
||||
<div className="tablet:flex justify-center items-center hidden">
|
||||
{HEADER_MENU_ITEMS.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="px-2 tablet:px-4 desktop:px-6 transition-all"
|
||||
>
|
||||
<Link
|
||||
href={item.path}
|
||||
onClick={(e) => handleScrollClick(e, item.path)}
|
||||
className="font-extralight text-xs tablet:text-sm desktop:text-base duration-100 ease-in hover:border-b-2 hover:border-b-black hover:dark:border-b-white pb-1"
|
||||
>
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderMenuItemsMobile = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const t = useTranslations('header');
|
||||
|
||||
const handleMenuClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string) => {
|
||||
handleScrollClick(e, path);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="w-6 h-6" />
|
||||
) : (
|
||||
<Menu className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="fixed top-[70px] left-0 right-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b animate-in fade-in fade-out transition-all duration-300">
|
||||
<div className="flex flex-col items-center gap-6 py-6">
|
||||
{HEADER_MENU_ITEMS.map((item) => (
|
||||
<Link
|
||||
href={item.path}
|
||||
key={item.key}
|
||||
onClick={(e) => handleMenuClick(e, item.path)}
|
||||
className="text-lg smalltablet:text-xl duration-100 ease-in hover:border-b-2 hover:border-b-brand-primary hover:dark:border-b-white pb-1"
|
||||
>
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<header
|
||||
className="fixed h-[70px] top-0 z-60 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 smalltablet:px-6 desktop:px-8 py-4 max-w-[1920px] mx-auto relative">
|
||||
{/* 모바일: 왼쪽 - 햄버거 아이콘 */}
|
||||
<div className="tablet:hidden flex items-center relative">
|
||||
<HeaderMenuItemsMobile />
|
||||
</div>
|
||||
|
||||
{/* 모바일: 가운데 - 이름, 태블릿 이상: 왼쪽 - 이름 */}
|
||||
<div className="tablet:hidden absolute left-1/2 -translate-x-1/2">
|
||||
<HeaderProfile />
|
||||
</div>
|
||||
<div className="hidden tablet:block">
|
||||
<HeaderProfile />
|
||||
</div>
|
||||
|
||||
{/* 모바일: 오른쪽 - 토글 버튼들, 태블릿 이상: 오른쪽 - 메뉴 + 토글 버튼들 */}
|
||||
<div className="flex items-center relative tablet:hidden">
|
||||
<LanguageToggle />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div className="hidden tablet:flex items-center">
|
||||
<HeaderMenuItemsDesktop />
|
||||
<LanguageToggle />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
18
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;
|
||||
15
nextjs/i18n/request.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from './routing';
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
9
nextjs/i18n/routing.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
import { createNavigation } from 'next-intl/navigation';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['ko', 'en', 'de'],
|
||||
defaultLocale: 'ko'
|
||||
});
|
||||
|
||||
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
|
||||
7
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))
|
||||
}
|
||||
|
||||
117
nextjs/messages/de.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"header": {
|
||||
"home": "Startseite",
|
||||
"about": "Über mich",
|
||||
"skills": "Fähigkeiten",
|
||||
"projects": "Projekte",
|
||||
"monitoring": "Überwachung",
|
||||
"contact": "Kontakt"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Full Stack Entwickler",
|
||||
"description": "Erstellen schöner und funktionaler Web-Erlebnisse mit modernen Technologien"
|
||||
},
|
||||
"about": {
|
||||
"title": "Über mich",
|
||||
"description": "Leidenschaftlicher Full-Stack-Entwickler mit Expertise in der Entwicklung skalierbarer Webanwendungen und Cloud-Infrastruktur",
|
||||
"personalInfo": "Persönliche Informationen",
|
||||
"whoIAm": "Wer ich bin",
|
||||
"name": "Name",
|
||||
"education": "Ausbildung",
|
||||
"birthday": "Geburtstag",
|
||||
"location": "Standort",
|
||||
"github": "Github",
|
||||
"email": "E-Mail",
|
||||
"bio1": "Ich bin Minjo Kim, ein Full-Stack-Entwickler und Bachelor in Informatik an der Seoul National University, Korea. Ich habe Erfahrung in der Webentwicklung und Cloud-Infrastruktur und bin leidenschaftlich daran interessiert, skalierbare und effiziente Systeme zu entwickeln.",
|
||||
"bio2": "Mein akademischer Hintergrund gab mir solide Kenntnisse in Algorithmen, Datenstrukturen, Systemdesign und Datenbanken, die ich anwende, um effiziente und moderne Systeme zu erstellen. Ich habe wirkungsvolle Lösungen mit TypeScript, React, Next.js und Tailwind CSS geliefert.",
|
||||
"bio3": "Ich bin verantwortlich für die Entwicklung von Full-Stack-Anwendungen, die Einrichtung von Kubernetes-Clustern mit ArgoCD für GitOps-Workflows und die Implementierung von Überwachungssystemen mit Prometheus und Grafana. Ich konzentriere mich auf die Erstellung wartbaren Codes und die Verbesserung der Systemleistung und Skalierbarkeit."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Fähigkeiten & Expertise",
|
||||
"description": "Technologien und Tools, mit denen ich arbeite, um erstaunliche Webanwendungen zu erstellen",
|
||||
"frontend": {
|
||||
"title": "Frontend-Entwicklung",
|
||||
"description": "Erstellen responsiver und interaktiver Benutzeroberflächen mit modernen Frameworks und Bibliotheken"
|
||||
},
|
||||
"backend": {
|
||||
"title": "Backend-Entwicklung",
|
||||
"description": "Entwicklung robuster serverseitiger Anwendungen und RESTful APIs"
|
||||
},
|
||||
"devops": {
|
||||
"title": "DevOps & Tools",
|
||||
"description": "Verwaltung von Deployment-Pipelines und Entwicklungsworkflows"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "Überwachung",
|
||||
"description": "Implementierung von Überwachungssystemen mit Prometheus und Grafana"
|
||||
},
|
||||
"testing": {
|
||||
"title": "Testen",
|
||||
"description": "Implementierung von Test-Frameworks und -Tools"
|
||||
},
|
||||
"hosting": {
|
||||
"title": "Web-Hosting",
|
||||
"description": "Bereitstellung und Verwaltung von Webanwendungen auf verschiedenen Hosting-Plattformen"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"title": "Ausgewählte Projekte",
|
||||
"description": "Einige meiner jüngsten Arbeiten und Nebenprojekte",
|
||||
"joossam": {
|
||||
"title": "Joossam English",
|
||||
"description": "Englisch-Lernplattform für koreanische Studenten"
|
||||
},
|
||||
"jotion": {
|
||||
"title": "Jotion",
|
||||
"description": "Notion-ähnliche Notizen-Anwendung"
|
||||
},
|
||||
"youniClassic": {
|
||||
"title": "Youni Classic",
|
||||
"description": "Kirchengemeinde-Verwaltungssystem"
|
||||
},
|
||||
"jaejadle": {
|
||||
"title": "Jaejadle Church",
|
||||
"description": "Kirchen-Website mit Veranstaltungsverwaltung"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Portfolio",
|
||||
"description": "Persönliche Portfolio-Website mit Kubernetes-Bereitstellung"
|
||||
},
|
||||
"todoList": {
|
||||
"title": "[Seminar] Todo List",
|
||||
"description": "Bildungs-Todo-Listen-Anwendung für den Unterricht"
|
||||
},
|
||||
"jovies": {
|
||||
"title": "Jovies",
|
||||
"description": "Film-Entdeckungs- und Tracking-Anwendung"
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "Überwachung",
|
||||
"description": "Echtzeit-Kubernetes-Cluster-Überwachung mit Grafana-Dashboards",
|
||||
"overview": "Übersicht",
|
||||
"resources": "Ressourcen",
|
||||
"kubernetes": "Kubernetes",
|
||||
"network": "Netzwerk"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Kontaktieren Sie mich",
|
||||
"description": "Haben Sie ein Projekt im Kopf oder möchten Sie zusammenarbeiten? Ich würde mich freuen, von Ihnen zu hören. Kontaktieren Sie mich gerne über einen der unten stehenden Kanäle.",
|
||||
"contactInfo": "Kontaktinformationen",
|
||||
"sendMessage": "Senden Sie mir eine Nachricht",
|
||||
"formDescription": "Füllen Sie das untenstehende Formular aus und ich werde mich so schnell wie möglich bei Ihnen melden.",
|
||||
"yourName": "Ihr Name",
|
||||
"yourEmail": "Ihre E-Mail",
|
||||
"subject": "Betreff",
|
||||
"message": "Nachricht",
|
||||
"messagePlaceholder": "Erzählen Sie mir von Ihrem Projekt oder wie ich Ihnen helfen kann...",
|
||||
"send": "Nachricht senden",
|
||||
"email": "E-Mail",
|
||||
"github": "GitHub",
|
||||
"linkedin": "LinkedIn",
|
||||
"phone": "Telefon"
|
||||
},
|
||||
"footer": {
|
||||
"rights": "Alle Rechte vorbehalten"
|
||||
}
|
||||
}
|
||||
117
nextjs/messages/en.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"header": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"skills": "Skills",
|
||||
"projects": "Projects",
|
||||
"monitoring": "Monitoring",
|
||||
"contact": "Contact"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Full Stack Developer",
|
||||
"description": "Creating functional web experiences with modern technologies."
|
||||
},
|
||||
"about": {
|
||||
"title": "About Me",
|
||||
"description": "Passionate full-stack developer with expertise in building scalable web applications and cloud infrastructure",
|
||||
"personalInfo": "Personal Info",
|
||||
"whoIAm": "Who I Am",
|
||||
"name": "Name",
|
||||
"education": "Education",
|
||||
"birthday": "Birthday",
|
||||
"location": "Location",
|
||||
"github": "Github",
|
||||
"email": "Email",
|
||||
"bio1": "I'm Minjo Kim, a Full-Stack Developer and Bachelor in Computer Science at Seoul National University, Korea. I have experience in web development and cloud infrastructure, passionate about building scalable and efficient systems.",
|
||||
"bio2": "My academic background gave me solid knowledge in Algorithms, Data Structures, System Design, and Databases which I apply to create efficient and modern systems. I've delivered impactful solutions using TypeScript, React, Next.js, and Tailwind CSS.",
|
||||
"bio3": "I'm responsible for building full-stack applications, setting up Kubernetes clusters with ArgoCD for GitOps workflows, and implementing monitoring systems with Prometheus and Grafana. I focus on creating maintainable code and improving system performance and scalability."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills & Expertise",
|
||||
"description": "Technologies and tools I work with to build amazing web applications",
|
||||
"frontend": {
|
||||
"title": "Frontend Development",
|
||||
"description": "Building responsive and interactive user interfaces with modern frameworks and libraries"
|
||||
},
|
||||
"backend": {
|
||||
"title": "Backend Development",
|
||||
"description": "Developing robust server-side applications and RESTful APIs"
|
||||
},
|
||||
"devops": {
|
||||
"title": "DevOps & Tools",
|
||||
"description": "Managing deployment pipelines and development workflows"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "Monitoring",
|
||||
"description": "Implementing monitoring systems with Prometheus and Grafana"
|
||||
},
|
||||
"testing": {
|
||||
"title": "Testing",
|
||||
"description": "Implementing testing frameworks and tools"
|
||||
},
|
||||
"hosting": {
|
||||
"title": "Web Hosting",
|
||||
"description": "Deploying and managing web applications on various hosting platforms"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"title": "Featured Projects",
|
||||
"description": "Some of my recent work and side projects",
|
||||
"joossam": {
|
||||
"title": "Joossam English",
|
||||
"description": "English learning platform for Korean students"
|
||||
},
|
||||
"jotion": {
|
||||
"title": "Jotion",
|
||||
"description": "Notion-like note-taking application"
|
||||
},
|
||||
"youniClassic": {
|
||||
"title": "Youni Classic",
|
||||
"description": "Church community management system"
|
||||
},
|
||||
"jaejadle": {
|
||||
"title": "Jaejadle Church",
|
||||
"description": "Church website with event management"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Portfolio",
|
||||
"description": "Personal portfolio website with Kubernetes deployment"
|
||||
},
|
||||
"todoList": {
|
||||
"title": "[Seminar] Todo List",
|
||||
"description": "Educational todo list application for teaching"
|
||||
},
|
||||
"jovies": {
|
||||
"title": "Jovies",
|
||||
"description": "Movie discovery and tracking application"
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "Monitoring",
|
||||
"description": "Real-time Kubernetes cluster monitoring with Grafana dashboards",
|
||||
"overview": "Overview",
|
||||
"resources": "Resources",
|
||||
"kubernetes": "Kubernetes",
|
||||
"network": "Network"
|
||||
},
|
||||
"contact": {
|
||||
"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.",
|
||||
"contactInfo": "Contact Information",
|
||||
"sendMessage": "Send Me a Message",
|
||||
"formDescription": "Fill out the form below and I'll get back to you as soon as possible.",
|
||||
"yourName": "Your Name",
|
||||
"yourEmail": "Your Email",
|
||||
"subject": "Subject",
|
||||
"message": "Message",
|
||||
"messagePlaceholder": "Tell me about your project or how I can help you...",
|
||||
"send": "Send Message",
|
||||
"email": "Email",
|
||||
"github": "GitHub",
|
||||
"linkedin": "LinkedIn",
|
||||
"phone": "Phone"
|
||||
},
|
||||
"footer": {
|
||||
"rights": "All rights reserved"
|
||||
}
|
||||
}
|
||||
117
nextjs/messages/ko.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"header": {
|
||||
"home": "홈",
|
||||
"about": "소개",
|
||||
"skills": "기술",
|
||||
"projects": "프로젝트",
|
||||
"monitoring": "모니터링",
|
||||
"contact": "연락처"
|
||||
},
|
||||
"hero": {
|
||||
"title": "풀스택 개발자",
|
||||
"description": "현대적인 기술로 아름답고 기능적인 웹 경험을 만듭니다"
|
||||
},
|
||||
"about": {
|
||||
"title": "소개",
|
||||
"description": "확장 가능한 웹 애플리케이션과 클라우드 인프라 구축에 전문성을 갖춘 열정적인 풀스택 개발자",
|
||||
"personalInfo": "개인 정보",
|
||||
"whoIAm": "나는 누구인가",
|
||||
"name": "이름",
|
||||
"education": "학력",
|
||||
"birthday": "생년월일",
|
||||
"location": "위치",
|
||||
"github": "깃허브",
|
||||
"email": "이메일",
|
||||
"bio1": "저는 서울대학교 컴퓨터공학과 학사 과정의 풀스택 개발자 김민조입니다. 웹 개발과 클라우드 인프라 경험이 있으며, 확장 가능하고 효율적인 시스템 구축에 열정을 가지고 있습니다.",
|
||||
"bio2": "학업 배경을 통해 알고리즘, 자료구조, 시스템 설계, 데이터베이스에 대한 탄탄한 지식을 쌓았으며, 이를 효율적이고 현대적인 시스템을 만드는 데 적용합니다. TypeScript, React, Next.js, Tailwind CSS를 사용하여 영향력 있는 솔루션을 제공했습니다.",
|
||||
"bio3": "풀스택 애플리케이션 구축, GitOps 워크플로우를 위한 ArgoCD를 사용한 Kubernetes 클러스터 설정, Prometheus와 Grafana를 사용한 모니터링 시스템 구현을 담당하고 있습니다. 유지보수 가능한 코드 작성과 시스템 성능 및 확장성 향상에 집중합니다."
|
||||
},
|
||||
"skills": {
|
||||
"title": "기술 및 전문성",
|
||||
"description": "놀라운 웹 애플리케이션을 구축하기 위해 사용하는 기술과 도구",
|
||||
"frontend": {
|
||||
"title": "프론트엔드 개발",
|
||||
"description": "현대적인 프레임워크와 라이브러리로 반응형 및 인터랙티브 사용자 인터페이스 구축"
|
||||
},
|
||||
"backend": {
|
||||
"title": "백엔드 개발",
|
||||
"description": "견고한 서버 사이드 애플리케이션 및 RESTful API 개발"
|
||||
},
|
||||
"devops": {
|
||||
"title": "데브옵스 및 도구",
|
||||
"description": "배포 파이프라인 및 개발 워크플로우 관리"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "모니터링",
|
||||
"description": "Prometheus와 Grafana를 사용한 모니터링 시스템 구현"
|
||||
},
|
||||
"testing": {
|
||||
"title": "테스팅",
|
||||
"description": "테스팅 프레임워크 및 도구 구현"
|
||||
},
|
||||
"hosting": {
|
||||
"title": "웹 호스팅",
|
||||
"description": "다양한 호스팅 플랫폼에서 웹 애플리케이션 배포 및 관리"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"title": "주요 프로젝트",
|
||||
"description": "최근 작업 및 사이드 프로젝트",
|
||||
"joossam": {
|
||||
"title": "Joossam English",
|
||||
"description": "한국 학생을 위한 영어 학습 플랫폼"
|
||||
},
|
||||
"jotion": {
|
||||
"title": "Jotion",
|
||||
"description": "Notion과 유사한 노트 작성 애플리케이션"
|
||||
},
|
||||
"youniClassic": {
|
||||
"title": "Youni Classic",
|
||||
"description": "교회 커뮤니티 관리 시스템"
|
||||
},
|
||||
"jaejadle": {
|
||||
"title": "Jaejadle Church",
|
||||
"description": "이벤트 관리 기능이 있는 교회 웹사이트"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Portfolio",
|
||||
"description": "Kubernetes 배포를 사용한 개인 포트폴리오 웹사이트"
|
||||
},
|
||||
"todoList": {
|
||||
"title": "[세미나] Todo List",
|
||||
"description": "교육용 할 일 목록 애플리케이션"
|
||||
},
|
||||
"jovies": {
|
||||
"title": "Jovies",
|
||||
"description": "영화 검색 및 추적 애플리케이션"
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "모니터링",
|
||||
"description": "Grafana 대시보드를 사용한 실시간 Kubernetes 클러스터 모니터링",
|
||||
"overview": "개요",
|
||||
"resources": "리소스",
|
||||
"kubernetes": "Kubernetes",
|
||||
"network": "네트워크"
|
||||
},
|
||||
"contact": {
|
||||
"title": "연락하기",
|
||||
"description": "프로젝트가 있거나 협업하고 싶으신가요? 여러분의 의견을 듣고 싶습니다. 아래 채널을 통해 자유롭게 연락해주세요.",
|
||||
"contactInfo": "연락처 정보",
|
||||
"sendMessage": "메시지 보내기",
|
||||
"formDescription": "아래 양식을 작성해 주시면 최대한 빨리 답변드리겠습니다.",
|
||||
"yourName": "이름",
|
||||
"yourEmail": "이메일",
|
||||
"subject": "제목",
|
||||
"message": "메시지",
|
||||
"messagePlaceholder": "프로젝트나 도움이 필요한 사항에 대해 알려주세요...",
|
||||
"send": "메시지 보내기",
|
||||
"email": "이메일",
|
||||
"github": "GitHub",
|
||||
"linkedin": "LinkedIn",
|
||||
"phone": "전화"
|
||||
},
|
||||
"footer": {
|
||||
"rights": "All rights reserved"
|
||||
}
|
||||
}
|
||||
8
nextjs/middleware.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/(ko|en|de)/:path*']
|
||||
};
|
||||
10
nextjs/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
11766
nextjs/package-lock.json
generated
Normal file
46
nextjs/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"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",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"chart.js": "^4.5.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"gsap": "^3.13.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "16.0.1",
|
||||
"next-intl": "^4.5.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "19.2.0",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "19.2.0",
|
||||
"shadcn": "^3.5.0",
|
||||
"sharp": "^0.33.5",
|
||||
"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
nextjs/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
16
nextjs/providers/theme-provider.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
BIN
nextjs/public/fonts/brand/SupremeLLTT-Black.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-BlackItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Bold.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-BoldFlat.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-BoldFlatItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-BoldItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Book.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-BookItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Italic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Light.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-LightItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Medium.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-MediumItalic.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Regular.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-Thin.woff2
Normal file
BIN
nextjs/public/fonts/brand/SupremeLLTT-ThinItalic.woff2
Normal file
8
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;
|
||||
}
|
||||
|
||||
72
nextjs/public/icons/docusaurus.svg
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -19 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<rect fill="#FFFFFF" x="126.030769" y="45.9487179" width="110.276923" height="44.6358974">
|
||||
|
||||
</rect>
|
||||
<path d="M26.2564103,191.671795 C16.5441641,191.671795 8.0830359,186.385067 3.54067692,178.54359 C1.30231795,182.408533 0,186.883938 0,191.671795 C0,206.17321 11.7549949,217.928205 26.2564103,217.928205 L52.5128205,217.928205 L52.5128205,191.671795 L26.2564103,191.671795 Z" fill="#3ECC5F">
|
||||
|
||||
</path>
|
||||
<path d="M144.384656,53.006441 L236.308349,47.2615385 L236.308349,34.1333333 C236.308349,19.6319179 224.552041,7.87692308 210.051938,7.87692308 L91.8980923,7.87692308 L88.616041,2.19241026 C87.1561846,-0.334769231 83.5104821,-0.334769231 82.0519385,2.19241026 L78.7698872,7.87692308 L75.4878359,2.19241026 C74.0279795,-0.334769231 70.3822769,-0.334769231 68.9237333,2.19241026 L65.6416821,7.87692308 L62.3596308,2.19241026 C60.8997744,-0.334769231 57.2540718,-0.334769231 55.7955282,2.19241026 L52.5134769,7.87692308 C52.4845949,7.87692308 52.4570256,7.88086154 52.4281436,7.88086154 L46.990441,2.44447179 C44.928,0.382030769 41.4070154,1.3246359 40.6508308,4.14326154 L38.8548923,10.8438974 L32.0374154,9.01645128 C29.2187897,8.26157949 26.6404103,10.839959 27.3965949,13.6585846 L29.2227282,20.4760615 L22.5234051,22.2706872 C19.7047795,23.0268718 18.7608615,26.5491692 20.8233026,28.6116103 L26.2596923,34.0493128 C26.2596923,34.0768821 26.2570667,34.1044513 26.2570667,34.1333333 L20.571241,37.4153846 C18.0453744,38.8739282 18.0453744,42.5209436 20.571241,43.9794872 L26.2570667,47.2615385 L20.571241,50.5435897 C18.0453744,52.0021333 18.0453744,55.6491487 20.571241,57.1076923 L26.2570667,60.3897436 L20.571241,63.6717949 C18.0453744,65.1303385 18.0453744,68.7773538 20.571241,70.2358974 L26.2570667,73.5179487 L20.571241,76.8 C18.0453744,78.2585436 18.0453744,81.905559 20.571241,83.3641026 L26.2570667,86.6461538 L20.571241,89.9282051 C18.0453744,91.3867487 18.0453744,95.0337641 20.571241,96.4923077 L26.2570667,99.774359 L20.571241,103.05641 C18.0453744,104.514954 18.0453744,108.161969 20.571241,109.620513 L26.2570667,112.902564 L20.571241,116.184615 C18.0453744,117.643159 18.0453744,121.290174 20.571241,122.748718 L26.2570667,126.030769 L20.571241,129.312821 C18.0453744,130.771364 18.0453744,134.418379 20.571241,135.876923 L26.2570667,139.158974 L20.571241,142.441026 C18.0453744,143.899569 18.0453744,147.546585 20.571241,149.005128 L26.2570667,152.287179 L20.571241,155.569231 C18.0453744,157.027774 18.0453744,160.67479 20.571241,162.133333 L26.2570667,165.415385 L20.571241,168.697436 C18.0453744,170.155979 18.0453744,173.802995 20.571241,175.261538 L26.2570667,178.54359 L20.571241,181.825641 C18.0453744,183.284185 18.0453744,186.9312 20.571241,188.389744 L26.2570667,191.671795 C26.2570667,206.17321 38.0120615,217.928205 52.5134769,217.928205 L210.051938,217.928205 C224.552041,217.928205 236.308349,206.17321 236.308349,191.671795 L236.308349,86.6461538 L144.384656,80.9012513 C137.019733,80.4404513 131.282708,74.3332103 131.282708,66.9538462 C131.282708,59.5744821 137.019733,53.467241 144.384656,53.006441" fill="#3ECC5F">
|
||||
|
||||
</path>
|
||||
<polygon fill="#3ECC5F" points="183.794872 217.928205 223.179487 217.928205 223.179487 165.415385 183.794872 165.415385">
|
||||
|
||||
</polygon>
|
||||
<path d="M249.435897,185.107692 C249.14839,185.107692 248.87401,185.156267 248.597005,185.191713 C248.547118,184.99479 248.499856,184.796554 248.444718,184.599631 C250.815672,183.609764 252.481641,181.272944 252.481641,178.54359 C252.481641,174.917579 249.543549,171.979487 245.917538,171.979487 C244.423549,171.979487 243.062154,172.499364 241.958072,173.339569 C241.812349,173.191221 241.666626,173.044185 241.518277,172.898462 C242.341415,171.800944 242.845538,170.45399 242.845538,168.977067 C242.845538,165.351056 239.907446,162.412964 236.281436,162.412964 C233.570462,162.412964 231.244144,164.057928 230.243774,166.405251 C230.049477,166.350113 229.853867,166.304164 229.659569,166.254277 C229.695015,165.977272 229.74359,165.702892 229.74359,165.415385 C229.74359,161.789374 226.805497,158.851282 223.179487,158.851282 C219.553477,158.851282 216.615385,161.789374 216.615385,165.415385 C216.615385,165.702892 216.663959,165.977272 216.699405,166.254277 C216.505108,166.304164 216.309497,166.350113 216.1152,166.405251 C215.114831,164.057928 212.788513,162.412964 210.077538,162.412964 C206.451528,162.412964 203.513436,165.351056 203.513436,168.977067 C203.513436,170.45399 204.017559,171.800944 204.840697,172.898462 C199.960944,177.666626 196.923077,184.31081 196.923077,191.671795 C196.923077,206.17321 208.678072,217.928205 223.179487,217.928205 C235.439918,217.928205 245.707487,209.513026 248.597005,198.151877 C248.87401,198.187323 249.14839,198.235897 249.435897,198.235897 C253.061908,198.235897 256,195.297805 256,191.671795 C256,188.045785 253.061908,185.107692 249.435897,185.107692" fill="#44D860">
|
||||
|
||||
</path>
|
||||
<polygon fill="#3ECC5F" points="196.923077 139.158974 236.307692 139.158974 236.307692 112.902564 196.923077 112.902564">
|
||||
|
||||
</polygon>
|
||||
<path d="M249.435897,129.312821 C251.248903,129.312821 252.717949,127.843774 252.717949,126.030769 C252.717949,124.217764 251.248903,122.748718 249.435897,122.748718 C249.2928,122.748718 249.154954,122.773662 249.017108,122.790728 C248.990851,122.692267 248.968533,122.593805 248.940964,122.495344 C250.125128,122.00041 250.958769,120.830687 250.958769,119.466667 C250.958769,117.653662 249.489723,116.184615 247.676718,116.184615 C246.929723,116.184615 246.248369,116.443241 245.696985,116.864656 C245.624779,116.789826 245.551262,116.716308 245.476431,116.644103 C245.888656,116.096656 246.140718,115.421867 246.140718,114.682749 C246.140718,112.871056 244.671672,111.400697 242.858667,111.400697 C241.502523,111.400697 240.339364,112.223836 239.839179,113.397497 C238.714092,113.083733 237.533867,112.902564 236.307692,112.902564 C229.058297,112.902564 223.179487,118.781374 223.179487,126.030769 C223.179487,133.280164 229.058297,139.158974 236.307692,139.158974 C237.533867,139.158974 238.714092,138.977805 239.839179,138.664041 C240.339364,139.837703 241.502523,140.660841 242.858667,140.660841 C244.671672,140.660841 246.140718,139.190482 246.140718,137.37879 C246.140718,136.639672 245.888656,135.964882 245.476431,135.417436 C245.551262,135.345231 245.624779,135.271713 245.696985,135.196882 C246.248369,135.618297 246.929723,135.876923 247.676718,135.876923 C249.489723,135.876923 250.958769,134.407877 250.958769,132.594872 C250.958769,131.230851 250.125128,130.061128 248.940964,129.566195 C248.968533,129.469046 248.990851,129.369272 249.017108,129.27081 C249.154954,129.287877 249.2928,129.312821 249.435897,129.312821" fill="#44D860">
|
||||
|
||||
</path>
|
||||
<path d="M78.7692308,50.5435897 C76.9562256,50.5435897 75.4871795,49.0745436 75.4871795,47.2615385 C75.4871795,41.8317128 71.0708513,37.4153846 65.6410256,37.4153846 C60.2112,37.4153846 55.7948718,41.8317128 55.7948718,47.2615385 C55.7948718,49.0745436 54.3258256,50.5435897 52.5128205,50.5435897 C50.6998154,50.5435897 49.2307692,49.0745436 49.2307692,47.2615385 C49.2307692,38.2122667 56.5917538,30.8512821 65.6410256,30.8512821 C74.6902974,30.8512821 82.0512821,38.2122667 82.0512821,47.2615385 C82.0512821,49.0745436 80.5822359,50.5435897 78.7692308,50.5435897" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M131.282051,217.928205 L210.051282,217.928205 C224.552697,217.928205 236.307692,206.17321 236.307692,191.671795 L236.307692,99.774359 L157.538462,99.774359 C143.037046,99.774359 131.282051,111.529354 131.282051,126.030769 L131.282051,217.928205 Z" fill="#FFFF50">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,140.471795 L150.948759,140.471795 C150.222769,140.471795 149.635938,139.884964 149.635938,139.158974 C149.635938,138.432985 150.222769,137.846154 150.948759,137.846154 L216.640985,137.846154 C217.366974,137.846154 217.953805,138.432985 217.953805,139.158974 C217.953805,139.884964 217.366974,140.471795 216.640985,140.471795" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,166.728205 L150.948759,166.728205 C150.222769,166.728205 149.635938,166.141374 149.635938,165.415385 C149.635938,164.689395 150.222769,164.102564 150.948759,164.102564 L216.640985,164.102564 C217.366974,164.102564 217.953805,164.689395 217.953805,165.415385 C217.953805,166.141374 217.366974,166.728205 216.640985,166.728205" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,192.984615 L150.948759,192.984615 C150.222769,192.984615 149.635938,192.397785 149.635938,191.671795 C149.635938,190.945805 150.222769,190.358974 150.948759,190.358974 L216.640985,190.358974 C217.366974,190.358974 217.953805,190.945805 217.953805,191.671795 C217.953805,192.397785 217.366974,192.984615 216.640985,192.984615" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,127.587118 L150.948759,127.587118 C150.222769,127.587118 149.635938,126.998974 149.635938,126.274297 C149.635938,125.548308 150.222769,124.961477 150.948759,124.961477 L216.640985,124.961477 C217.366974,124.961477 217.953805,125.548308 217.953805,126.274297 C217.953805,126.998974 217.366974,127.587118 216.640985,127.587118" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,153.6 L150.948759,153.6 C150.222769,153.6 149.635938,153.013169 149.635938,152.287179 C149.635938,151.56119 150.222769,150.974359 150.948759,150.974359 L216.640985,150.974359 C217.366974,150.974359 217.953805,151.56119 217.953805,152.287179 C217.953805,153.013169 217.366974,153.6 216.640985,153.6" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M216.640985,179.85641 L150.948759,179.85641 C150.222769,179.85641 149.635938,179.269579 149.635938,178.54359 C149.635938,177.8176 150.222769,177.230769 150.948759,177.230769 L216.640985,177.230769 C217.366974,177.230769 217.953805,177.8176 217.953805,178.54359 C217.953805,179.269579 217.366974,179.85641 216.640985,179.85641" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M236.307692,58.5666297 C236.291938,58.5666297 236.27881,58.5587528 236.263056,58.5600656 C232.206441,58.6979118 230.287097,62.75584 228.593559,66.3359015 C226.826503,70.0761272 225.459856,72.5100964 223.220185,72.4365785 C220.740267,72.3473067 219.322421,69.5457477 217.820554,66.5800862 C216.095508,63.1759426 214.126277,59.3136246 209.992205,59.4580349 C205.993354,59.5945682 204.067446,63.1260554 202.368656,66.2413785 C200.560903,69.5601887 199.33079,71.5779938 196.958523,71.4847836 C194.428718,71.3928862 193.08439,69.1151426 191.528697,66.478999 C189.794462,63.5435323 187.789785,60.2431015 183.735795,60.3560041 C179.80521,60.4912246 177.874051,63.487081 176.17001,66.1324144 C174.367508,68.9287221 173.104574,70.6327631 170.702113,70.5316759 C168.111918,70.4384656 166.774154,68.5493169 165.226338,66.3608451 C163.488164,63.9019323 161.529436,61.1187528 157.487262,61.2539733 C153.643323,61.3852554 151.712164,63.8389169 150.009436,66.0037579 C148.392041,68.0570092 147.129108,69.682281 144.457518,69.579881 C143.732841,69.550999 143.125005,70.1194503 143.098749,70.84544 C143.071179,71.5688041 143.638318,72.1779528 144.362995,72.2055221 C148.323774,72.3381169 150.329764,69.8411323 152.071877,67.6277169 C153.617067,65.6637374 154.950892,63.9688862 157.576533,63.8796144 C160.105026,63.7719631 161.290503,65.3434092 163.083815,67.8771528 C164.786544,70.2848656 166.719015,73.0155323 170.60759,73.1560041 C174.681272,73.2925374 176.641313,70.2481067 178.376862,67.554199 C179.928615,65.1464862 181.267692,63.0682913 183.825067,62.9803323 C186.178954,62.8923733 187.460267,64.75264 189.266708,67.8128246 C190.969436,70.6970913 192.897969,73.9647015 196.864,74.1091118 C200.966564,74.2508964 202.94761,70.6682092 204.673969,67.4990605 C206.169272,64.7578913 207.580554,62.1676964 210.081477,62.0823631 C212.435364,62.0272246 213.662851,64.1763118 215.478482,67.7668759 C217.174646,71.1185067 219.097928,74.9151836 223.125662,75.0609067 C223.200492,75.0635323 223.27401,75.0648451 223.347528,75.0648451 C227.37001,75.0648451 229.278851,71.0279221 230.968451,67.4583631 C232.463754,64.2944656 233.878974,61.3130503 236.307692,61.1922708 L236.307692,58.5666297 Z" fill="#000000">
|
||||
|
||||
</path>
|
||||
<polygon fill="#3ECC5F" points="105.025641 217.928205 157.538462 217.928205 157.538462 165.415385 105.025641 165.415385">
|
||||
|
||||
</polygon>
|
||||
<path d="M183.794872,185.107692 C183.507364,185.107692 183.232985,185.156267 182.955979,185.191713 C182.906092,184.99479 182.858831,184.796554 182.803692,184.599631 C185.174646,183.609764 186.840615,181.272944 186.840615,178.54359 C186.840615,174.917579 183.902523,171.979487 180.276513,171.979487 C178.782523,171.979487 177.421128,172.499364 176.317046,173.339569 C176.171323,173.191221 176.0256,173.044185 175.877251,172.898462 C176.70039,171.800944 177.204513,170.45399 177.204513,168.977067 C177.204513,165.351056 174.266421,162.412964 170.64041,162.412964 C167.929436,162.412964 165.603118,164.057928 164.602749,166.405251 C164.408451,166.350113 164.212841,166.304164 164.018544,166.254277 C164.05399,165.977272 164.102564,165.702892 164.102564,165.415385 C164.102564,161.789374 161.164472,158.851282 157.538462,158.851282 C153.912451,158.851282 150.974359,161.789374 150.974359,165.415385 C150.974359,165.702892 151.022933,165.977272 151.058379,166.254277 C150.864082,166.304164 150.668472,166.350113 150.474174,166.405251 C149.473805,164.057928 147.147487,162.412964 144.436513,162.412964 C140.810503,162.412964 137.87241,165.351056 137.87241,168.977067 C137.87241,170.45399 138.376533,171.800944 139.199672,172.898462 C134.319918,177.666626 131.282051,184.31081 131.282051,191.671795 C131.282051,206.17321 143.037046,217.928205 157.538462,217.928205 C169.798892,217.928205 180.066462,209.513026 182.955979,198.151877 C183.232985,198.187323 183.507364,198.235897 183.794872,198.235897 C187.420882,198.235897 190.358974,195.297805 190.358974,191.671795 C190.358974,188.045785 187.420882,185.107692 183.794872,185.107692" fill="#44D860">
|
||||
|
||||
</path>
|
||||
<polygon fill="#3ECC5F" points="105.025641 139.158974 157.538462 139.158974 157.538462 112.902564 105.025641 112.902564">
|
||||
|
||||
</polygon>
|
||||
<path d="M170.666667,129.312821 C172.479672,129.312821 173.948718,127.843774 173.948718,126.030769 C173.948718,124.217764 172.479672,122.748718 170.666667,122.748718 C170.523569,122.748718 170.385723,122.773662 170.247877,122.790728 C170.221621,122.692267 170.199303,122.593805 170.171733,122.495344 C171.355897,122.00041 172.189538,120.830687 172.189538,119.466667 C172.189538,117.653662 170.720492,116.184615 168.907487,116.184615 C168.160492,116.184615 167.479138,116.443241 166.927754,116.864656 C166.855549,116.789826 166.782031,116.716308 166.7072,116.644103 C167.119426,116.096656 167.371487,115.421867 167.371487,114.682749 C167.371487,112.871056 165.902441,111.400697 164.089436,111.400697 C162.733292,111.400697 161.570133,112.223836 161.069949,113.397497 C159.944862,113.083733 158.764636,112.902564 157.538462,112.902564 C150.289067,112.902564 144.410256,118.781374 144.410256,126.030769 C144.410256,133.280164 150.289067,139.158974 157.538462,139.158974 C158.764636,139.158974 159.944862,138.977805 161.069949,138.664041 C161.570133,139.837703 162.733292,140.660841 164.089436,140.660841 C165.902441,140.660841 167.371487,139.190482 167.371487,137.37879 C167.371487,136.639672 167.119426,135.964882 166.7072,135.417436 C166.782031,135.345231 166.855549,135.271713 166.927754,135.196882 C167.479138,135.618297 168.160492,135.876923 168.907487,135.876923 C170.720492,135.876923 172.189538,134.407877 172.189538,132.594872 C172.189538,131.230851 171.355897,130.061128 170.171733,129.566195 C170.199303,129.469046 170.221621,129.369272 170.247877,129.27081 C170.385723,129.287877 170.523569,129.312821 170.666667,129.312821" fill="#44D860">
|
||||
|
||||
</path>
|
||||
<path d="M183.794872,32.4923077 C183.584821,32.4923077 183.361641,32.4660513 183.15159,32.4266667 C182.941538,32.3872821 182.730174,32.321641 182.534564,32.2428718 C182.337641,32.1641026 182.153846,32.0590769 181.968738,31.9409231 C181.798072,31.8227692 181.628718,31.678359 181.469867,31.5339487 C181.326769,31.3764103 181.182359,31.2188718 181.064205,31.0350769 C180.946051,30.8512821 180.841026,30.6674872 180.760944,30.4705641 C180.683487,30.273641 180.617846,30.0635897 180.578462,29.8535385 C180.539077,29.6434872 180.512821,29.4203077 180.512821,29.2102564 C180.512821,29.0002051 180.539077,28.7770256 180.578462,28.5669744 C180.617846,28.3569231 180.683487,28.16 180.760944,27.9499487 C180.841026,27.7530256 180.946051,27.5692308 181.064205,27.3854359 C181.182359,27.2147692 181.326769,27.0441026 181.469867,26.8865641 C181.628718,26.7421538 181.798072,26.5977436 181.968738,26.4795897 C182.153846,26.3614359 182.337641,26.2564103 182.534564,26.177641 C182.730174,26.0988718 182.941538,26.0332308 183.15159,25.9938462 C183.571692,25.9019487 184.004923,25.9019487 184.438154,25.9938462 C184.646892,26.0332308 184.858256,26.0988718 185.055179,26.177641 C185.25079,26.2564103 185.435897,26.3614359 185.619692,26.4795897 C185.790359,26.5977436 185.959713,26.7421538 186.118564,26.8865641 C186.262974,27.0441026 186.407385,27.2147692 186.525538,27.3854359 C186.643692,27.5692308 186.748718,27.7530256 186.827487,27.9499487 C186.906256,28.16 186.971897,28.3569231 187.011282,28.5669744 C187.049354,28.7770256 187.076923,29.0002051 187.076923,29.2102564 C187.076923,30.0767179 186.721149,30.9300513 186.118564,31.5339487 C185.959713,31.678359 185.790359,31.8227692 185.619692,31.9409231 C185.435897,32.0590769 185.25079,32.1641026 185.055179,32.2428718 C184.858256,32.321641 184.646892,32.3872821 184.438154,32.4266667 C184.228103,32.4660513 184.004923,32.4923077 183.794872,32.4923077" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M210.051282,30.8512821 C209.184821,30.8512821 208.344615,30.4968205 207.726277,29.8929231 C207.583179,29.7353846 207.438769,29.5647179 207.320615,29.3940513 C207.202462,29.2102564 207.097436,29.0264615 207.017354,28.8295385 C206.939897,28.6326154 206.874256,28.4225641 206.834872,28.2125128 C206.795487,28.0024615 206.769231,27.7792821 206.769231,27.5692308 C206.769231,26.7027692 207.123692,25.8625641 207.726277,25.2455385 C207.885128,25.1011282 208.054482,24.9567179 208.225149,24.8385641 C208.410256,24.7204103 208.594051,24.6153846 208.790974,24.5366154 C208.986585,24.4578462 209.197949,24.3922051 209.408,24.3528205 C209.828103,24.2609231 210.274462,24.2609231 210.694564,24.3528205 C210.903303,24.3922051 211.114667,24.4578462 211.31159,24.5366154 C211.5072,24.6153846 211.692308,24.7204103 211.876103,24.8385641 C212.046769,24.9567179 212.216123,25.1011282 212.374974,25.2455385 C212.977559,25.8625641 213.333333,26.7027692 213.333333,27.5692308 C213.333333,27.7792821 213.305764,28.0024615 213.267692,28.2125128 C213.228308,28.4225641 213.162667,28.6326154 213.083897,28.8295385 C212.992,29.0264615 212.900103,29.2102564 212.781949,29.3940513 C212.663795,29.5647179 212.519385,29.7353846 212.374974,29.8929231 C212.216123,30.0373333 212.046769,30.1817436 211.876103,30.2998974 C211.692308,30.4180513 211.5072,30.5230769 211.31159,30.6018462 C211.114667,30.6806154 210.903303,30.7462564 210.694564,30.785641 C210.484513,30.8250256 210.261333,30.8512821 210.051282,30.8512821" fill="#000000">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
BIN
nextjs/public/jaejadle/home.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
nextjs/public/joossam/home.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
nextjs/public/joossam/main.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
nextjs/public/jotion/home.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
nextjs/public/jovies/home.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
nextjs/public/portfolio/home.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
nextjs/public/todoList/home.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
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
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"
|
||||
]
|
||||
}
|
||||