Init NextJS

This commit is contained in:
암냥 2025-09-28 14:52:22 +09:00
commit 7fee80308c
52 changed files with 465 additions and 3542 deletions

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
src/app/globals.css Normal file
View file

@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View file

@ -1,29 +0,0 @@
import "../index.css";
import { BrowserRouter, Routes, Route } from "react-router";
import { Page } from "./page";
import NotFound from "./utils/NotFound";
import { ThemeProvider } from "@/components/theme-provider";
import { useEffect } from "react";
import { useNavigate } from "react-router";
import SUPERCOMMAND from "@/components/SUPERCOMMAND";
function TimelineRedirect() {
const navigate = useNavigate();
useEffect(() => { navigate("/#timeline"); }, [navigate]);
return null;
}
export default function App() {
return (
<ThemeProvider defaultTheme="system">
<SUPERCOMMAND />
<BrowserRouter>
<Routes>
<Route path="/" element={<Page />} />
<Route path="/timeline" element={<TimelineRedirect />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}

34
src/app/layout.tsx Normal file
View file

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View file

@ -1,185 +1,103 @@
import "../link.css";
import { useState, useEffect } from "react";
import Image from "@/profile.avif";
import Timeline from "@/components/TimeLine";
import Contact from "@/components/Contact";
import Projects from "@/components/Projects";
import Seperator from "@/components/Seperator";
import Image from "next/image";
export function Page() {
const [age, setAge] = useState<number>(0);
const [post, setPost] = useState<any>({});
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
useEffect(() => {
const scrollToHash = () => {
const hash = window.location.hash.substring(1);
if (hash) {
const element = document.getElementById(hash);
if (element) {
setTimeout(() => {
element.scrollIntoView({ behavior: "smooth" });
}, 100);
}
}
};
scrollToHash();
}, []);
useEffect(() => {
// 나이 계산
const referenceDate = new Date(2010, 11, 8);
const currentDate = new Date();
let calculatedAge = currentDate.getFullYear() - referenceDate.getFullYear();
if (currentDate < new Date(currentDate.getFullYear(), referenceDate.getMonth(), referenceDate.getDate())) {
calculatedAge -= 1;
}
setAge(calculatedAge);
}, []);
useEffect(() => {
// 블로그 데이터 가져오기
const fetchBlogData = async () => {
try {
const response = await fetch("https://api.imnya.ng/rss");
const data = await response.json();
if (data) {
setPost(data[0] || {});
}
} catch (error) {
console.error("Error fetching posts:", error);
}
};
fetchBlogData();
}, []);
return (
<div className="flex flex-col justify-center font-medium">
<div className="max-w-3xl px-4 mx-auto pt-24 pb-12 leading-8">
<header className="mb-6">
<h1 className="mb-4">
<a
href="mailto:me@imnya.ng"
className="text-5xl font-medium font-serif font-ntype hover:opacity-80 transition-opacity"
>
me@imnya.ng
</a>
</h1>
</header>
<section className="mb-8 space-y-4">
<p className="font-medium">
<span className="font-extrabold"> </span> {" "}
<span className="font-extrabold"></span> {" "}
<span className="font-extrabold"></span>.
</p>
<p className="font-medium">
{" "}
<a
className="link-pink"
target="_blank"
href="https://github.com/team-neko/two_hearts"
title="Chrome New Tab Extension"
rel="noopener noreferrer"
>
Two Hearts
</a>
,{" "}
<a
className="link-amber"
target="_blank"
href="https://www.youtube.com/watch?v=XTdqpdTMZbw&list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD"
title="Effect Playing Contest is ADOFAI Contest"
rel="noopener noreferrer"
>
EPC 2025
</a>
,{" "}
<a
className="link-emerald"
target="_blank"
href="https://github.com/team-neko/dynamic-kawaii"
title="Dark Pink VSCode Theme"
rel="noopener noreferrer"
>
Dynamic Kawaii
</a>{" "}
.
</p>
</section>
<figure className="my-8">
<picture className="block bg-gray-100 rounded-xl aspect-3-2 overflow-hidden image-scale object-shadowed">
<img
src={Image}
className="w-full aspect-3-2 object-cover object-center transition-transform duration-300 hover:scale-105"
alt="프로필 이미지"
/>
</picture>
</figure>
<section className="mb-8 space-y-4">
<p className="font-medium">
{age} , {" "}
<span className="font-extrabold"> </span> ,
<br />
<span className="font-extrabold"> </span> .
<br />
<span className="font-extrabold"> </span> .
</p>
<p className="font-medium">
<span className="font-extrabold"></span> ,
<span className="font-extrabold"></span> .
<br />
4 <span className="font-extrabold">Python</span> ,
<span className="font-extrabold">TypeScript</span> .
<br />
<span className="font-extrabold"> </span> .
</p>
</section>
<section className="my-8">
<div className="flex flex-row items-center justify-center p-6 bg-muted rounded-xl shadow-lg">
<img
loading="lazy"
decoding="async"
src="https://skillicons.dev/icons?i=typescript,js,c,cpp,rust,go,java,kotlin,py,html,css,php,react,remix,nextjs,tailwind,tauri,bun,elysia,mongodb,postgres,sqlite,docker,nginx,github,githubactions,git,arduino,raspberrypi,bots"
className="w-full max-w-3xl rounded-lg object-contain select-none pointer-events-none transition-transform duration-300 hover:scale-105"
alt="기술 스택"
title="기술 스택"
width={800}
/>
</div>
</section>
{post.title && (
<section className="mb-8">
<p className="font-medium">
:{" "}
<a
href={post.link}
className="text-muted-foreground hover:text-foreground transition-colors underline decoration-dashed underline-offset-4"
target="_blank"
rel="noopener noreferrer"
>
{post.title}
</a>
</p>
</section>
)}
<Seperator />
<Projects />
<Seperator />
<Timeline />
<Seperator />
<Contact />
<p>We are in MAGICALWORLD!</p>
</div>
</div>
);
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View file

@ -1,17 +0,0 @@
import { CommitsGrid } from "@/components/commits-grid"
import { Button } from "@/components/ui/button";
import { useNavigate } from "react-router";
export default function NotFound() {
const navigate = useNavigate();
return (
<div className="flex flex-col justify-center items-center h-screen gap-8">
<CommitsGrid text="404" />
<p className="text-2xl">The page you are looking for does not exist.</p>
<Button variant="outline" onClick={() => navigate("/")}>
Go to Home
</Button>
</div>
);
}

View file

@ -1,95 +0,0 @@
import { Github, Instagram, Rss } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import Seperator from "@/components/Seperator";
export default function Contact() {
return (
<div className="w-full h-64 flex items-center justify-center">
<div className="w-full md:w-[50%] p-4 flex items-center justify-center flex-col gap-4">
<span>Discord : <a href="https://api.imnya.ng/discord_invite" className="text-blue-400">@imnya.ng</a></span>
<Seperator />
<div className="flex items-center justify-center gap-4 flex-row">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://github.com/imnyang"
target="_blank"
rel="noreferrer"
className="flex flex-row gap-4"
>
<Github />
</a>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Github
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://x.com/imnya_ng"
target="_blank"
rel="noreferrer"
className="flex flex-row gap-4 text-3xl"
>
𝕏
</a>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">𝕏</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://instagram.com/imnya.ng"
target="_blank"
rel="noreferrer"
className="flex flex-row gap-4"
>
<Instagram />
</a>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Instagram
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://blog.imnya.ng"
target="_blank"
rel="noreferrer"
className="flex flex-row gap-4"
>
<Rss />
</a>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Blog
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-row gap-3 items-center justify-center">
<p>Github</p>
<Button
variant="secondary"
size="sm"
>
<a href="https://github.com/sponsors/imnyang" target="_blank" rel="noreferrer" className="flex items-center justify-center gap-2">
<span>💕</span>
<span>Sponsor</span>
</a>
</Button>
</div>
</div>
</div>
);
}

View file

@ -1,40 +0,0 @@
import ProjectsComponents from "./ProjectsComponents";
const projects = [
{
name: "EPC/broadcast",
url: "https://www.youtube.com/playlist?list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD",
description: "Effect Playing Contest 2025의 방송화면 기능 부분을 맡았습니다.",
techStack: ["Bun", "Elysia", "React"]
},
{
name: "team-neko/two_hearts",
url: "https://chromewebstore.google.com/detail/fhbjjhpphmigcniggnhgoepaodgoobdk?utm_source=item-share-cb",
description: "Two Hearts는 Chrome 확장 프로그램으로, 새탭을 더 간단명료하게 보여줍니다.",
techStack: ["Bun", "Chrome", "TypeScript", "React"]
},
{
name: "team-neko/dynamic-kawaii",
url: "https://github.com/team-neko/dynamic-kawaii",
description: "Dynamic Kawaii는 Visual Studio Code의 몇 안되는 핑크색 다크모드입니다.",
techStack: ["VSCode", "json"]
},
{
name: "imnyang/tsh",
url: "https://github.com/imnyang/tsh",
description: "tsh는 Rust로 작성된 CLI Trash Bin입니다.",
techStack: ["Rust", "trash"]
}
];
export default function Projects() {
return (
<div id="projects" className="mt-8">
<div className="space-y-8">
{projects.map((project, index) => (
<ProjectsComponents key={index} project={project} />
))}
</div>
</div>
)
}

View file

@ -1,38 +0,0 @@
type Project = {
name: string;
url: string;
description: string;
techStack: string[];
};
export default function ProjectsComponents({ project }: { project: Project }) {
return (
<div>
<div>
<h1 className="text-xl mb-2">
<a href={project.url} target="_blank" rel="noopener noreferrer">
{project.name}
</a>
</h1>
<p className="text-sm text-muted-forceground font-light mb-2">
{project.description.split('\n').map((line, idx) => (
<span key={idx}>
{line}
<br />
</span>
))}
</p>
<div>
{project.techStack.map((tech, idx) => (
<span
key={idx}
className="inline-block bg-accent text-accent-foreground text-xs mr-2 px-2.5 py-0.5 rounded select-none"
>
{tech}
</span>
))}
</div>
</div>
</div>
);
}

View file

@ -1,145 +0,0 @@
import React from "react";
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
export default function SUPERCOMMAND() {
const [keySequence, setKeySequence] = React.useState("");
const [showDialog, setShowDialog] = React.useState(false);
const targetSequence = "MAGICALWORLD";
const handleKeyDown = (event: KeyboardEvent) => {
// 영문자만 처리
if (event.key.length === 1 && /[a-zA-Z]/.test(event.key)) {
console.log(keySequence);
console.log(targetSequence);
console.log("Key pressed:", event.key);
setKeySequence(prev => {
const newSequence = prev + event.key;
console.log("New sequence:", newSequence);
// 목표 시퀀스와 정확히 일치하는지 확인 (대문자만)
if (newSequence === targetSequence) {
console.log("Sequence matched!");
setShowDialog(true);
return "";
}
// 목표 시퀀스의 시작 부분과 일치하는지 확인 (대문자만)
if (targetSequence.startsWith(newSequence)) {
return newSequence;
}
// 일치하지 않으면 현재 키부터 다시 시작
const restartSequence = event.key;
if (targetSequence.startsWith(restartSequence)) {
return restartSequence;
}
// 완전히 초기화
return "";
});
} else {
// 특수키나 숫자가 입력되면 시퀀스 초기화
setKeySequence("");
}
};
React.useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent className="flex flex-col gap-0 p-0 sm:max-h-[min(640px,80vh)] sm:max-w-lg [&>button:last-child]:top-3.5">
<DialogHeader className="contents space-y-0 text-left">
<DialogTitle className="border-b px-6 py-4 text-base">
???
</DialogTitle>
<div className="overflow-y-auto">
<DialogDescription asChild>
<div className="px-6 py-4">
<div className="[&_strong]:text-foreground space-y-4 [&_strong]:font-semibold pb-32">
<p style={{ whiteSpace: 'pre-line' }}>{`마법소녀는 세상을 구했는데
?
?
?
?
?
믿
?
?
?
?
?
`}
</p>
</div>
</div>
</DialogDescription>
<DialogFooter className="px-6 py-6 sm:justify-start fixed bottom-0 left-0 w-full bg-background/80 backdrop-blur-sm">
<Button type="button">Okay</Button>
</DialogFooter>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
);
}

View file

@ -1,5 +0,0 @@
export default function Seperator() {
return (
<div className="border-t-1 border-muted rounded-full mt-8" />
)
}

View file

@ -1,237 +0,0 @@
import { Accordion, AccordionContent, AccordionItem } from "@/components/ui/accordion";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { Plus } from "lucide-react";
import { useEffect, useState, useRef } from "react";
const events = [
{
date: "2025-09-13",
description:
"선린인터넷고 소프트웨어 나눔축제 AnA 이수",
category: "Education",
link: "https://ssf.sunrin.io/"
},
{
date: "2025-07-26",
description:
"선린인터넷고 여름방학 중학생 특별교육 우수 이수 (프로그래밍)",
category: "Education",
link: "https://sunrint.sen.hs.kr/"
},
{
date: "2025-02-27",
description:
"화이트햇스쿨 3기 최종합격",
category: "Education",
link: "https://whitehatschool.kr/home/kor/main.do"
},
{
date: "2025-01-19",
description:
"2024 Sunrin LOGCON(TeamLog 주최) 중등부 3위",
category: "Award",
link: "https://teamlog.kr"
},
{
date: "2025-01-12",
description:
"2024 Sunrin Layer7 CTF 중등부 2위",
category: "Award",
link: "https://layer7.kr"
},
{
date: "2025-01-10",
description:
"선린인터넷고 겨울방학 중학생 특별교육 이수 (IT경영학과)",
category: "Education",
link: "https://sunrint.sen.hs.kr/"
},
{
date: "2024-12-14",
description:
"2024 글로벌스타트업학교 K-청소년스타트업 경진대회 우수상 수상",
category: "Award",
link: "https://www.ncf.or.kr/projects/'2024-%EA%B8%80%EB%A1%9C%EB%B2%8C%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85%ED%95%99%EA%B5%90-k-%EC%B2%AD%EC%86%8C%EB%85%84%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85-%EA%B2%BD%EC%A7%84%EB%8C%80%ED%9A%8C'-%EC%B0%B8%EA%B0%80%EC%9E%90-%EB%AA%A8%EC%A7%91",
},
{
date: "2024-12-07",
description: "글로벌 스타트업 학교 팀 1위",
category: "Award",
link: "https://ncf.or.kr",
},
{
date: "2024-12-07",
description: "글로벌 스타트업 학교 개인 최우수상",
category: "Award",
link: "https://ncf.or.kr",
},
{
date: "2024-08-18",
description: "29회 해킹캠프 CTF 1위 (고민중독)",
category: "Award & Conference",
link: "https://ctf.hackingcamp.org/",
},
{
date: "2024-08-01",
description:
"글로벌 스타트업 학교 2기 베트남 해외 연수 데모데이 대상 (1위)",
category: "Award",
link: "http://ncf.or.kr",
},
{
date: "2024-05-16",
description: "글로벌 스타트업 학교 2기 합격",
category: "Education",
link: "http://ncf.or.kr",
},
{
date: "2024-05-11",
description: "LG AI 청소년 캠프 1기 LG 탐색상 수상",
category: "Award",
link: "https://lgaiyouthcamp.or.kr/",
},
{
date: "2024-05-11",
description: "LG AI 청소년 캠프 1기 수료",
category: "Award & Education",
link: "https://lgaiyouthcamp.or.kr/",
},
{
date: "2023-11-14",
description: "인천상정중학교 2023학년도 SW 문제 해결 활동 우수상(2위) 수여",
category: "Award",
},
{
date: "2023-09-02",
description:
"선린인터넷고등학교 제6회 소프트웨어나눔축제 Layer7 부서 과정 이수",
category: "Education",
},
{
date: "2023-07-24",
description: "한국정보기술연구원이 주도하는 사이버 가디언즈 보안캠프 수료",
category: "Education",
},
{
date: "2023-05-15",
description: "한국 코드페어 예선 진출",
category: "Award",
},
{
date: "2022-12-20",
description: "2022 SW영재 창작대회 은상 수상",
category: "Award",
},
{
date: "2022-09-27",
description: "2022 삼성 주니어 SW 창작대회 본선 진출",
category: "Award",
},
{
date: "2022-05-23",
description: "2022학년도 석정초SW영재학급 첫 수업",
category: "Education",
},
{
date: "2022-07-26",
description: "제 14회 맑은하늘 맑은웃음 공모전에서 맑은웃음상 수여",
category: "Award",
},
{
date: "2021-11-14",
description: "Become a ZEPETO Creator 이수",
category: "Education",
},
{
date: "2021-05-19",
description:
"소프트웨어와 전자신문이 주관한 소프트웨어재단 꿈찾기 캠프 이수",
category: "Education",
},
{
date: "2018-01-27",
description:
"제4회 맑은하늘 맑은웃음 어린이 문예공모전에서 위닉스상(2위) 수여",
category: "Award",
},
];
export default function Timeline() {
const [count, setCount] = useState<number>(0);
const [isVisible, setIsVisible] = useState<boolean>(false);
const TimelineRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (TimelineRef.current) {
observer.observe(TimelineRef.current);
}
return () => {
if (TimelineRef.current) {
observer.unobserve(TimelineRef.current);
}
};
}, []);
useEffect(() => {
if (isVisible && count < events.length) {
const timer = setTimeout(() => setCount(count + 1), count === 0 ? 300 : 25);
return () => clearTimeout(timer);
}
}, [isVisible, count]);
return (
<div id="timeline" ref={TimelineRef} className="w-full flex flex-col items-center justify-center mt-8">
<div className="w-full">
<h1 className="text-2xl font-bold mb-4 w-full">🌠 </h1>
<p> {count} !</p>
<br />
<Accordion type="multiple" className="w-full space-y-2">
{Array.from(new Set(events.map(event => new Date(event.date).getFullYear()))).sort((a, b) => b - a).map(year => (
<AccordionItem
value={year.toString()}
key={year}
className="rounded-lg border bg-background px-4 py-1"
>
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger className="flex flex-1 items-center gap-3 py-2 text-left text-[15px] font-semibold leading-6 transition-all [&>svg>path:last-child]:origin-center [&>svg>path:last-child]:transition-all [&>svg>path:last-child]:duration-200 [&>svg]:-order-1 [&[data-state=open]>svg>path:last-child]:rotate-90 [&[data-state=open]>svg>path:last-child]:opacity-0 [&[data-state=open]>svg]:rotate-180">
{year}
<Plus
size={16}
strokeWidth={2}
className="shrink-0 opacity-60 transition-transform duration-200"
aria-hidden="true"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionContent className="pb-2 ps-7 text-foreground overflow-y-auto">
{events.filter(event => new Date(event.date).getFullYear() === year).map((event, index) => (
<div key={index} className="my-2">
<div className="flex flex-row">
<span className="text-md font-semibold fixed-width-number">{new Date(event.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit' })}</span>
<span className="text-md font-semibold fixed-width-number text-muted-foreground">{event.category}</span>
</div>
{event.link ? (
<a href={event.link}>{event.description}</a>
) : (
<span>{event.description}</span>
)}
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
);
}

View file

@ -1,179 +0,0 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils";
import type { CSSProperties } from "react";
export const CommitsGrid = ({ text }: { text: string }) => {
const cleanString = (str: string): string => {
const upperStr = str.toUpperCase();
const withoutAccents = upperStr
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
const allowedChars = Object.keys(letterPatterns);
return withoutAccents
.split("")
.filter((char) => allowedChars.includes(char))
.join("");
};
const generateHighlightedCells = (text: string) => {
const cleanedText = cleanString(text);
const width = Math.max(cleanedText.length * 6, 6) + 1;
let currentPosition = 1; // we start at 1 to leave space for the top border
const highlightedCells: number[] = [];
cleanedText
.toUpperCase()
.split("")
.forEach((char) => {
if (letterPatterns[char]) {
const pattern = letterPatterns[char].map((pos) => {
const row = Math.floor(pos / 50);
const col = pos % 50;
return (row + 1) * width + col + currentPosition;
});
highlightedCells.push(...pattern);
}
currentPosition += 6;
});
return {
cells: highlightedCells,
width,
height: 9, // 7+2 for the top and bottom borders
};
};
const {
cells: highlightedCells,
width: gridWidth,
height: gridHeight,
} = generateHighlightedCells(text);
const getRandomColor = () => {
const commitColors = [
"#48d55d",
"#016d32",
"#0d4429"
];
const randomIndex = Math.floor(Math.random() * commitColors.length);
return commitColors[randomIndex];
};
const getRandomDelay = () => `${(Math.random() * 0.6).toFixed(1)}s`;
const getRandomFlash = () => +(Math.random() < 0.3);
return (
<section
className="w-full max-w-xl bg-card border grid p-1.5 sm:p-3 gap-0.5 sm:gap-1 rounded-[10px] sm:rounded-[15px]"
style={{
gridTemplateColumns: `repeat(${gridWidth}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${gridHeight}, minmax(0, 1fr))`,
}}
>
{Array.from({ length: gridWidth * gridHeight }).map((_, index) => {
const isHighlighted = highlightedCells.includes(index);
const shouldFlash = !isHighlighted && getRandomFlash();
return (
<div
key={index}
className={cn(
`border h-full w-full aspect-square rounded-[4px] sm:rounded-[3px]`,
isHighlighted ? "animate-highlight" : "",
shouldFlash ? "animate-flash" : "",
!isHighlighted && !shouldFlash ? "bg-card" : ""
)}
style={
{
animationDelay: getRandomDelay(),
"--highlight": getRandomColor(),
} as CSSProperties
}
/>
);
})}
</section>
);
};
const letterPatterns: { [key: string]: number[] } = {
A: [
1, 2, 3, 50, 100, 150, 200, 250, 300, 54, 104, 154, 204, 254, 304, 151, 152,
153,
],
B: [
0, 1, 2, 3, 4, 50, 100, 150, 151, 200, 250, 300, 301, 302, 303, 304, 54,
104, 152, 153, 204, 254, 303,
],
C: [0, 1, 2, 3, 4, 50, 100, 150, 200, 250, 300, 301, 302, 303, 304],
D: [
0, 1, 2, 3, 50, 100, 150, 200, 250, 300, 301, 302, 54, 104, 154, 204, 254,
303,
],
E: [0, 1, 2, 3, 4, 50, 100, 150, 200, 250, 300, 301, 302, 303, 304, 151, 152],
F: [0, 1, 2, 3, 4, 50, 100, 150, 200, 250, 300, 151, 152, 153],
G: [
0, 1, 2, 3, 4, 50, 100, 150, 200, 250, 300, 301, 302, 303, 153, 204, 154,
304, 254,
],
H: [
0, 50, 100, 150, 200, 250, 300, 151, 152, 153, 4, 54, 104, 154, 204, 254,
304,
],
I: [0, 1, 2, 3, 4, 52, 102, 152, 202, 252, 300, 301, 302, 303, 304],
J: [0, 1, 2, 3, 4, 52, 102, 152, 202, 250, 252, 302, 300, 301],
K: [0, 4, 50, 100, 150, 200, 250, 300, 151, 152, 103, 54, 203, 254, 304],
L: [0, 50, 100, 150, 200, 250, 300, 301, 302, 303, 304],
M: [
0, 50, 100, 150, 200, 250, 300, 51, 102, 53, 4, 54, 104, 154, 204, 254, 304,
],
N: [
0, 50, 100, 150, 200, 250, 300, 51, 102, 153, 204, 4, 54, 104, 154, 204,
254, 304,
],
Ñ: [
0, 50, 100, 150, 200, 250, 300, 51, 102, 153, 204, 4, 54, 104, 154, 204,
254, 304,
],
O: [1, 2, 3, 50, 100, 150, 200, 250, 301, 302, 303, 54, 104, 154, 204, 254],
P: [0, 50, 100, 150, 200, 250, 300, 1, 2, 3, 54, 104, 151, 152, 153],
Q: [
1, 2, 3, 50, 100, 150, 200, 250, 301, 302, 54, 104, 154, 204, 202, 253, 304,
],
R: [
0, 50, 100, 150, 200, 250, 300, 1, 2, 3, 54, 104, 151, 152, 153, 204, 254,
304,
],
S: [1, 2, 3, 4, 50, 100, 151, 152, 153, 204, 254, 300, 301, 302, 303],
T: [0, 1, 2, 3, 4, 52, 102, 152, 202, 252, 302],
U: [0, 50, 100, 150, 200, 250, 301, 302, 303, 4, 54, 104, 154, 204, 254],
V: [0, 50, 100, 150, 200, 251, 302, 4, 54, 104, 154, 204, 253],
W: [
0, 50, 100, 150, 200, 250, 301, 152, 202, 252, 4, 54, 104, 154, 204, 254,
303,
],
X: [0, 50, 203, 254, 304, 4, 54, 152, 101, 103, 201, 250, 300],
Y: [0, 50, 101, 152, 202, 252, 302, 4, 54, 103],
Z: [0, 1, 2, 3, 4, 54, 103, 152, 201, 250, 300, 301, 302, 303, 304],
"0": [1, 2, 3, 50, 100, 150, 200, 250, 301, 302, 303, 54, 104, 154, 204, 254],
"1": [1, 52, 102, 152, 202, 252, 302, 0, 2, 300, 301, 302, 303, 304],
"2": [0, 1, 2, 3, 54, 104, 152, 153, 201, 250, 300, 301, 302, 303, 304],
"3": [0, 1, 2, 3, 54, 104, 152, 153, 204, 254, 300, 301, 302, 303],
"4": [0, 50, 100, 150, 4, 54, 104, 151, 152, 153, 154, 204, 254, 304],
"5": [0, 1, 2, 3, 4, 50, 100, 151, 152, 153, 204, 254, 300, 301, 302, 303],
"6": [
1, 2, 3, 50, 100, 150, 151, 152, 153, 200, 250, 301, 302, 204, 254, 303,
],
"7": [0, 1, 2, 3, 4, 54, 103, 152, 201, 250, 300],
"8": [
1, 2, 3, 50, 100, 151, 152, 153, 200, 250, 301, 302, 303, 54, 104, 204, 254,
],
"9": [1, 2, 3, 50, 100, 151, 152, 153, 154, 204, 254, 304, 54, 104],
" ": [],
};

View file

@ -1,73 +0,0 @@
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "bun-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

View file

@ -1,64 +0,0 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

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

View file

@ -1,68 +0,0 @@
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 rounded-xl border shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn("flex flex-col gap-1.5 p-6", className)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold tracking-tight", 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 CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("p-6 pt-0", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -1,141 +0,0 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View file

@ -1,165 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
useFormState,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm font-medium", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View file

@ -1,19 +0,0 @@
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(
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4",
className
)}
{...props}
/>
)
}
export { Input }

View file

@ -1,24 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View file

@ -1,179 +0,0 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
"border-input data-[placeholder]:text-muted-foreground aria-invalid:border-destructive ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground 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 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -1,59 +0,0 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,26 +0,0 @@
/**
* This file is the entry point for the React app, it sets up the root
* element and renders the App component to the DOM.
*
* It is included in `src/index.html`.
*/
import { createRoot } from "react-dom/client";
import { StrictMode } from "react";
import App from "./app";
const elem = document.getElementById("root")!;
const app = (
<StrictMode>
<App />
</StrictMode>
);
if (import.meta.hot) {
// With hot module reloading, `import.meta.hot.data` is persisted.
const root = (import.meta.hot.data.root ??= createRoot(elem));
root.render(app);
} else {
// The hot module reloading API is not available in production.
createRoot(elem).render(app);
}

View file

@ -1,52 +0,0 @@
/*@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR&family=Noto+Color+Emoji&display=swap');*/
@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
@import "../styles/globals.css";
@layer base {
:root {
@apply font-sans;
}
body {
@apply m-0 p-0 bg-background text-foreground relative min-h-screen;
}
}
@font-face {
font-family: 'NType82Headline';
src: url('https://f.imnya.ng/font/NType82-Headline.woff2') format('woff2');
}
* {
/*font-family: 'IBM Plex Sans KR', sans-serif;*/
font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
}
.font-ntype {
font-family: "NType82Headline" !important;
}
.tnum {
font-feature-settings: "tnum";
}
.image-scale {
transition-property: scale,border-radius box-shadow;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo)
}
@media (hover: hover) {
.image-scale:hover {
--tw-scale-x:105%;
--tw-scale-y: 105%;
--tw-scale-z: 105%;
scale: var(--tw-scale-x)var(--tw-scale-y);
border-radius: var(--radius-lg)
}
}
.image-scale:hover {
z-index: 10;
box-shadow: 0 15px 45px #0006
}

View file

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
이번 패치하면서 들은 곡
【3D LIVE】インターネット・ガール・バースデー 【#しぐれうい生誕2025】| ういこうせん / しぐれうい
https://www.youtube.com/live/A1Cs9A8JhYs?t=3148
-->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/ico" href="./favicon.ico" />
<title>남현석 | :two_hearts:</title>
<meta name="description" content="남현석 (imnyang, 암냥) - 항상 탐구하고 연구하는 학생 개발자입니다. 여러 분야에 관심이 많습니다." />
<meta name="keywords" content="남현석, imnyang, 암냥, 학생 개발자, 개발자" />
<meta property="og:title" content="남현석 | imnyang(암냥)" />
<meta property="og:description" content="학생 개발자 남현석(imnyang, 암냥)의 포트폴리오입니다. 여러 분야에 관심이 많습니다." />
<meta property="og:image" content="https://f.imnya.ng/profile/banner-profile.avif" />
<meta property="og:url" content="https://imnya.ng" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="남현석 | imnyang(암냥)" />
<meta name="twitter:description" content="학생 개발자 남현석(imnyang, 암냥)의 포트폴리오입니다." />
<meta name="twitter:image" content="https://f.imnya.ng/profile/banner-profile.avif" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4588517451789913" crossorigin="anonymous"></script>
<script type="module" src="./frontend.tsx" async></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -1,67 +0,0 @@
import { serve } from "bun";
import index from "./index.html";
// Parse command line arguments for port
const args = process.argv.slice(2);
const portArgIndex = args.findIndex((arg) => arg === "--port");
const defaultPort =
portArgIndex !== -1 && args[portArgIndex + 1]
? parseInt(args[portArgIndex + 1])
: 3000;
// Function to find available port
async function findAvailablePort(startPort: number): Promise<number> {
let port = startPort;
while (port < 65535) {
try {
const testServer = serve({
port: port,
fetch() {
return new Response("test");
},
});
testServer.stop();
return port;
} catch (error) {
port++;
}
}
throw new Error("No available port found");
}
(async () => {
const port = await findAvailablePort(defaultPort);
const server = serve({
port: port,
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
"/timeline": Response.redirect("/#timeline"),
"/ads.txt": new Response(
"google.com, pub-4588517451789913, DIRECT, f08c47fec0942fa0",
{
headers: {
"content-type": "text/plain",
},
}
),
},
development: process.env.NODE_ENV !== "production" && {
// Enable browser hot reloading in development
hmr: true,
// Echo console logs from the browser to the server
console: true,
},
});
console.clear();
if (process.env.NODE_ENV !== "production") {
console.log(`\x1b[45m Dev \x1b[0m\x1b[35m Bun v${Bun.version}\x1b[0m`);
} else {
console.log(`\x1b[0m\x1b[35m Bun v${Bun.version}\x1b[0m`);
}
console.log(`\n\x1b[34m→ \x1b[35m${server.url}\x1b[0m`);
})();

View file

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

View file

@ -1,233 +0,0 @@
.link-pink {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-pink-500);
background-color: #f6339a1a;
display: inline-block;
rotate: -2deg
}
@media (hover: hover) {
.link-pink:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-pink {
background-color:color-mix(in oklab,var(--color-pink-500)10%,transparent)
}
}
.link-pink:hover,.link-pink:focus {
background-color: #f6339a33
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-pink:hover,.link-pink:focus) {
background-color:color-mix(in oklab,var(--color-pink-500)20%,transparent)
}
}
.link-amber {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-amber-500);
background-color: #f99c001a;
display: inline-block;
rotate: 3deg
}
@media (hover: hover) {
.link-amber:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-amber {
background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)
}
}
.link-amber:hover,.link-amber:focus {
background-color: #f99c0033
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-amber:hover,.link-amber:focus) {
background-color:color-mix(in oklab,var(--color-amber-500)20%,transparent)
}
}
.link-emerald {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-emerald-500);
background-color: #00bb7f1a;
display: inline-block;
rotate: -1deg
}
@media (hover: hover) {
.link-emerald:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-emerald {
background-color:color-mix(in oklab,var(--color-emerald-500)10%,transparent)
}
}
.link-emerald:hover,.link-emerald:focus {
background-color: #00bb7f33
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-emerald:hover,.link-emerald:focus) {
background-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)
}
}
.link-green {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-green-500);
background-color: #00c7581a;
display: inline-block;
rotate: -1deg
}
@media (hover: hover) {
.link-green:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-green {
background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)
}
}
.link-green:hover,.link-green:focus {
background-color: #00c75833
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-green:hover,.link-green:focus) {
background-color:color-mix(in oklab,var(--color-green-500)20%,transparent)
}
}
.link-violet {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-violet-500);
background-color: #8d54ff1a;
display: inline-block;
rotate: -2deg
}
@media (hover: hover) {
.link-violet:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-violet {
background-color:color-mix(in oklab,var(--color-violet-500)10%,transparent)
}
}
.link-violet:hover,.link-violet:focus {
background-color: #8d54ff33
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-violet:hover,.link-violet:focus) {
background-color:color-mix(in oklab,var(--color-violet-500)20%,transparent)
}
}
.link-blue {
height: calc(var(--spacing)*8);
transform: var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,);
cursor: pointer;
border-radius: var(--radius-lg);
padding-inline:calc(var(--spacing)*1.5);transition-property: color,background-color,scale;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo);
color: var(--color-blue-500);
background-color: #3080ff1a;
display: inline-block;
rotate: 1deg
}
@media (hover: hover) {
.link-blue:hover {
--tw-scale-x:110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x)var(--tw-scale-y)
}
}
@supports (color: color-mix(in lab,red,red)) {
.link-blue {
background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)
}
}
.link-blue:hover,.link-blue:focus {
background-color: #3080ff33
}
@supports (color: color-mix(in lab,red,red)) {
:is(.link-blue:hover,.link-blue:focus) {
background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

4
src/type.d.ts vendored
View file

@ -1,4 +0,0 @@
declare module "*.avif" {
const value: string;
export default value;
}