imnyang/imnya.ng
Watch
1
0
Fork
You've already forked imnya.ng
0

Init NextJS

This commit is contained in:
암냥 2025-09-28 14:52:22 +09:00
commit 7fee80308c
32 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 }

Some files were not shown because too many files have changed in this diff Show more