Init NextJS
This commit is contained in:
parent
a3a53a68ec
commit
7fee80308c
52 changed files with 465 additions and 3542 deletions
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
src/app/globals.css
Normal file
26
src/app/globals.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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
34
src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
282
src/app/page.tsx
282
src/app/page.tsx
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export default function Seperator() {
|
||||
return (
|
||||
<div className="border-t-1 border-muted rounded-full mt-8" />
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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],
|
||||
" ": [],
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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`);
|
||||
})();
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
233
src/link.css
233
src/link.css
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
BIN
src/profile.avif
BIN
src/profile.avif
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB |
4
src/type.d.ts
vendored
4
src/type.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
|||
declare module "*.avif" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue