좀 많은게 바뀐거 같아요

This commit is contained in:
imnyang 2025-05-07 22:49:43 +09:00
commit 0dfe8ee371
29 changed files with 505 additions and 1106 deletions

View file

@ -1,85 +0,0 @@
import { useEffect } from "react";
import Top from "@/components/Home/Top";
import About from "@/components/Home/About";
import Timeline from "@/components/Home/Timeline";
import Contact from "@/components/Home/Contact";
import Project from "@/components/Home/Project";
import "./index.css";
import Wakatime from "./components/Home/Wakatime";
export function App() {
useEffect(() => {
// 초기 로드 시 hash에 맞게 스크롤
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();
// 스크롤 시 hash 업데이트 로직
const sections = document.querySelectorAll(".hash");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.history.replaceState(null, "", `#${entry.target.id}`);
}
});
},
{ threshold: 0.9 } // 90% 보이면 활성화
);
sections.forEach(section => observer.observe(section));
return () => {
sections.forEach(section => observer.unobserve(section));
};
}, []);
useEffect(() => {
// img 위에서 스크롤 방지
const handleWheel = (event: WheelEvent) => {
if (event.target instanceof HTMLImageElement) {
event.preventDefault();
}
};
document.addEventListener("wheel", handleWheel, { passive: false });
return () => {
document.removeEventListener("wheel", handleWheel);
};
}, []);
return (
<div className="bg-background text-foreground w-full h-full overflow-y-auto">
<div id="top" className="hash"> {/* stupid copliot this is not section */}
<Top />
</div>
<div id="about" className="section hash"> {/* Hey Stupid Copliot This is section */}
<About />
</div>
<div id="wakatime" className="section hash"> {/* Hey Stupid Copliot This is section */}
<Wakatime />
</div>
<div id="project" className="section hash"> {/* Hey Stupid Copliot This is section */}
<Project />
</div>
<div id="timeline" className="section hash"> {/* Hey Stupid Copliot This is section */}
<Timeline />
</div>
<div id="contact" className="section hash"> {/* Hey Stupid Copliot This is section */}
<Contact />
</div>
</div>
);
}

20
src/app/index.tsx Normal file
View file

@ -0,0 +1,20 @@
import "../index.css";
import { BrowserRouter, Routes, Route } from "react-router";
import { Page } from "./page";
import RedirectTimeline from "./utils/RedirectTimeline";
import NotFound from "./utils/NotFound";
import { ThemeProvider } from "@/components/theme-provider";
export default function App() {
return (
<ThemeProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Page />} />
<Route path="/timeline" element={<RedirectTimeline />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}

75
src/app/page.tsx Normal file
View file

@ -0,0 +1,75 @@
import "../link.css";
import { useState, useEffect } from "react";
import Image from "@/profile.avif";
import Timeline from "@/components/TimeLine";
import Contact from "@/components/Contact";
export function Page() {
const [age, setAge] = useState<number>(0);
const [post, setPost] = useState<any>({});
useEffect(() => {
// 나이 계산
const referenceDate = new Date(2010, 10, 8); // 2010년 11월 8일 (0-indexed)
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(() => {
// 블로그 데이터 가져오기
fetch("https://api.imnya.ng/rss")
.then(response => response.json())
.then(data => {
if (data) setPost(data[0] || {});
})
.catch(error => console.error("Error fetching posts:", error));
}, []);
return (
<div className="flex flex-col justify-center">
<div className="max-w-3xl px-4 mx-auto pt-24 pb-12 leading-8">
<h1 className="text-5xl font-medium font-serif font-ntype mb-4">imnya.ng</h1>
<p>
<span className="font-extrabold"> </span> <span className="font-extrabold"></span> <span className="font-extrabold"></span>.
</p>
<p>
{" "}
<a className="link-pink" target="_blank" href="https://github.com/team-neko/two_hearts" title="Chrome New Tab Extension">
Two Hearts
</a>,{" "}
<a className="link-amber" target="_blank" href="https://instagram.com/today.isangjeong" title="Post meal in Instagram">
</a>,{" "}
<a className="link-emerald" target="_blank" href="https://github.com/team-neko/dynamic-kawaii" title="Dark Pink VSCode Theme">
Dynamic Kawaii
</a> .
</p>
<picture className="block bg-gray-100 my-4 rounded-xl aspect-3-2 overflow-hidden image-scale object-shadowed">
<img src={Image} className="w-full aspect-3-2 object-cover object-center" />
</picture>
<p>
{age} <span className="font-extrabold"> </span> {" "}
<span className="font-extrabold"> </span> <span className="font-extrabold"> </span> .
</p>
<br />
<p>
:{" "}
<a href={post.link} className="text-muted-foreground">
{post.title}
</a>
</p>
<Timeline />
<Contact />
</div>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function NotFound() {
return (
<div>
<h1>404 - Not Found</h1>
<p>The page you are looking for does not exist.</p>
</div>
);
}

View file

@ -0,0 +1,10 @@
import { useEffect } from "react";
import { useNavigate } from "react-router";
export default function RedirectTimeline() {
const navigate = useNavigate();
useEffect(() => {
navigate("/#timeline");
}, [navigate]);
return (<></>)
}

View file

@ -1,111 +0,0 @@
import { useState, useEffect } from "react";
import { Send, AlignJustify, BadgeCheck, House, CircleHelp, ChartGantt, PhoneCall } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function BottomBar() {
const [email, setEmail] = useState<string>('me@imnya.ng');
const [hash, setHash] = useState<string>(window.location.hash);
const [accessKeyCombo, setAccessKeyCombo] = useState<string>("Alt");
useEffect(() => {
const emaillist = ['me', 'mail', 'not', 'cat', 'neko', 'meow', 'heart'];
const domainlist = ['imnya.ng', 'al-1s.kr'];
const randomEmail = () => {
const random = Math.floor(Math.random() * 1000);
if (random === 0) {
setEmail(`furry@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
} else {
setEmail(`${emaillist[Math.floor(Math.random() * emaillist.length)]}@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
}
};
randomEmail();
const handleHashChange = () => {
setHash(window.location.hash);
};
window.addEventListener('hashchange', handleHashChange);
return () => {
window.removeEventListener('hashchange', handleHashChange);
};
}, []);
useEffect(() => {
const ua = navigator.userAgent;
let keyCombo = "Alt";
if (/Mac/i.test(ua)) {
keyCombo = "Control + Option";
} else if (/Linux/i.test(ua)) {
keyCombo = "Alt";
if (/Firefox/i.test(ua)) {
keyCombo = "Alt + Shift";
}
} else if (/Windows/i.test(ua)) {
if (/Firefox/i.test(ua)) {
keyCombo = "Alt + Shift";
}
}
setAccessKeyCombo(keyCombo);
}, []);
return (
<div className="w-full flex justify-center fixed bottom-0 z-50">
<header className="bg-background/75 text-foreground w-full md:w-[50%] h-12 border rounded-full select-none m-4 mt-2">
<div className="flex items-center justify-between w-full h-full py-4 px-8">
<a href={`mailto:${email}`} className="flex flex-row gap-4"><Send width={16} /> {email}</a>
<div>
{["top", "about", "project", "timeline", "contact"].map((section, index) => (
<button
key={section}
onClick={() => window.location.hash = `#${section}`}
accessKey={(index + 1).toString()}
className="w-[0px] h-[0px] text-[0px] text-background"
></button>
))}
</div>
<div className="flex flex-row items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button><AlignJustify /></button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{["top", "about", "project", "timeline", "contact"].map((section, index) => {
const icons = [House, CircleHelp, ChartGantt, ChartGantt, PhoneCall];
const Icon = icons[index];
return (
<DropdownMenuItem asChild key={section}>
<a href={`#${section}`} className="flex flex-row items-center justify-between">
<div className="flex flex-row gap-2 items-center justify-between h-4">
{hash === `#${section}` ? (
<BadgeCheck width={16} height={16} />
) : (
<Icon width={16} height={16} />
)}
{section.charAt(0).toUpperCase() + section.slice(1)}
</div>
<p className="text-muted-foreground">{accessKeyCombo} + {index + 1}</p>
</a>
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
<DropdownMenuLabel>© 2021-2025 imnyang</DropdownMenuLabel>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
</div>
);
}

View file

@ -5,7 +5,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "../ui/button";
import { Button } from "@/components/ui/button";
export default function Contact() {
return (

View file

@ -1,117 +0,0 @@
import { useEffect, useRef, useState } from "react";
export default function About() {
const [wakatime, setWakatime] = useState<any>();
const [time, setTime] = useState<number>(0);
const [post, setPost] = useState<any>({});
const [age, setAge] = useState<number>(0);
const [totalSeconds, setTotalSeconds] = useState<number>(0);
const [isVisible, setIsVisible] = useState<boolean>(false);
const AboutRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// 나이 계산
const referenceDate = new Date(2010, 10, 8); // 2010년 11월 8일 (0-indexed)
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(() => {
// 블로그 데이터 가져오기
fetch("https://api.imnya.ng/rss")
.then(response => response.json())
.then(data => {
if (data) setPost(data[0] || {});
})
.catch(error => console.error("Error fetching posts:", error));
}, []);
useEffect(() => {
// Intersection Observer로 isVisible 상태 변경
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (AboutRef.current) observer.observe(AboutRef.current);
return () => {
if (AboutRef.current) observer.unobserve(AboutRef.current);
};
}, []);
useEffect(() => {
// Wakatime 데이터 가져오기 (한 번만 실행)
fetch("https://api.imnya.ng/wakatime")
.then(response => response.json())
.then(data => {
if (data) {
const roundedSeconds = Math.round(data.data.total_seconds); // 반올림
setTotalSeconds(roundedSeconds);
setWakatime(data.data);
}
})
.catch(error => console.error("Error fetching Wakatime data:", error));
}, []);
useEffect(() => {
if (!isVisible) return;
const start = Date.now() - time;
let animationFrameId: number;
const tick = () => {
const elapsed = Date.now() - start;
if (elapsed >= totalSeconds) {
setTime(totalSeconds);
return;
}
setTime(elapsed);
animationFrameId = requestAnimationFrame(tick);
};
animationFrameId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(animationFrameId);
}, [isVisible, totalSeconds]);
return (
<div className="w-full flex flex-col items-center justify-center" ref={AboutRef}>
<div className="w-full md:w-[50%] p-4">
<h1 className="text-2xl font-bold">🤔 About</h1>
<div className="flex items-start justify-center flex-col p-2 mt-2 w-full">
<div className="flex flex-col font-light text-2xl">
<h1> <strong className="font-black"> </strong> </h1>
<h1> <strong className="font-black"></strong> </h1>
<h1> <strong className="font-black"></strong>.</h1>
</div>
<div className="mt-8">
<p>{age} </p>
<p> </p>
<p> </p>
<p> .</p>
<br/>
<a className="mt-4 text-foreground" href="https://wakatime.com/@imnyang" target="_blank" rel="noopener noreferrer">
Wakatime Mar 18th ~ : <span className="tnum text-muted-foreground">{time}</span>s
</a>
<h1 className="text-foreground">
: <a href={post.link} className="text-muted-foreground">{post.title}</a>
</h1>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,39 +0,0 @@
import ProjectCard from "../ProjectCard";
const projects = [
{
name: "🍴 오늘 인천 상정중학교",
description: "오늘의 급식을 인스타그램에서도 빠르게",
link: "https://www.instagram.com/today.isangjeong/"
},
{
name: "💕 Two Hearts",
description: "Chrome 새탭을 더 좋게 (사실 Edge에서 쓰려고 만듦)",
link: "https://github.com/imnyang/two_hearts"
},
{
name: "🩷 Dynamic Kawaii",
description: "진정한 VSCode 테마",
link: "https://github.com/imnyang/dynamic-kawaii"
},
{
name: "💊 FakeAlyac",
description: "어? 내 시스템 트레이에 있는거 알약 아닌데?",
link: "https://github.com/imnyang/FakeAlyac"
}
]
export default function Project() {
return (
<div className="w-full flex flex-col items-center justify-center select-none">
<div className="w-full md:w-[50%] p-4">
<h1 className="text-2xl font-bold">📖 Project</h1>
<div className="mt-4 gap-4 grid grid-cols-1 md:grid-cols-2">
{projects.map((project, index) => (
<ProjectCard key={index} name={project.name} description={project.description} link={project.link} />
))}
</div>
</div>
</div>
);
}

View file

@ -1,65 +0,0 @@
import { useState } from "react";
import "../../index.css";
import Image from "@/profile.avif";
export default function Top() {
const [mousePos, setMousePos] = useState({ x: 50, y: 50 });
const [scale, setScale] = useState(1.15);
const [isHovering, setIsHovering] = useState(false);
const handleMouseMove = (e) => {
const { left, top, width, height } = e.currentTarget.getBoundingClientRect();
const x = ((e.clientX - left) / width) * 100;
const y = ((e.clientY - top) / height) * 100;
setMousePos((prev) => ({
x: prev.x + (x - prev.x) * 0.1,
y: prev.y + (y - prev.y) * 0.1,
}));
};
const handleWheel = (e) => {
e.preventDefault();
setScale((prev) => {
const newScale = prev + e.deltaY * -0.0025;
return Math.min(Math.max(newScale, 1), 3);
});
};
return (
<div className="bg-background text-foreground w-full h-screen flex items-center justify-center">
<div className="flex flex-col md:flex-row w-full md:w-[50%] h-full py-16 md:py-32">
<div className="w-full md:w-[45%] h-full flex flex-col justify-center items-center md:items-end md:pr-16">
<div className="text-center md:text-right">
<h1 className="text-5xl ntypefont">
Nam
<br />
HyunSuk
</h1>
<p className="mt-4 text-xl">
<br /> .
</p>
</div>
</div>
<div
className="md:w-[55%] select-none p-5 md:p-0 w-full h-full flex justify-center md:justify-end items-end rounded-3xl mb-8 md:mb-0 overflow-hidden"
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onWheel={handleWheel}
>
<img
src={Image}
alt="Me"
className="w-full h-full object-cover rounded-3xl transition-transform duration-500 ease-out"
style={{
transform: `scale(${isHovering ? scale : 1})`,
transformOrigin: `${mousePos.x}% ${mousePos.y}%`,
}}
/>
</div>
</div>
</div>
);
}

View file

@ -1,40 +0,0 @@
import React, { useEffect, useState } from "react";
export default function Wakatime() {
const [wakatime, setWakatime] = useState<any>();
useEffect(() => {
// Wakatime 데이터 가져오기 (한 번만 실행)
fetch("https://api.imnya.ng/wakatime")
.then(response => response.json())
.then(data => {
if (data) {
setWakatime(data.data);
}
})
.catch(error => console.error("Error fetching Wakatime data:", error));
}, []);
return (
<div className="w-full flex flex-col items-center justify-center">
<div className="w-full md:w-[50%] p-4">
<a className="text-2xl font-bold" href="https://wakatime.com/@imnyang" target="_blank" rel="noopener noreferrer">🍝 Wakatime</a>
<p>Dashboards for developers</p>
<br />
{wakatime && wakatime.languages && (
<div>
<p> : {(wakatime.human_readable_total)}</p>
<p> : {wakatime.human_readable_daily_average}</p>
<br />
<p> :</p>
<ul>
{wakatime.languages.slice(0, 3).map((language: any, index: number) => (
<li key={index}>{index+1}. {language.name}: {language.percent}%</li>
))}
</ul>
</div>
)}
</div>
</div>
)
}

View file

@ -1,27 +0,0 @@
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
interface ProjectCardProps {
name: string;
description: string;
link: string;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ name, description, link }) => {
return (
<a href={link}>
<Card>
<CardHeader>
<CardTitle>{name}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
</Card>
</a>
);
}
export default ProjectCard;

View file

@ -176,9 +176,9 @@ export default function Timeline() {
}, [isVisible, count]);
return (
<div ref={TimelineRef} className="w-full flex flex-col items-center justify-center p-4">
<div className="w-full md:w-[50%] p-4">
<h1 className="text-2xl font-bold mb-4 w-full">🌠 Timeline</h1>
<div 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">

View file

@ -1,255 +0,0 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
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 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-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 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View file

@ -34,7 +34,7 @@ function TooltipTrigger({
function TooltipContent({
className,
sideOffset = 4,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@ -44,7 +44,7 @@ function TooltipContent({
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 max-w-sm rounded-md px-3 py-1.5 text-xs",
"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}

View file

@ -7,17 +7,12 @@
import { createRoot } from "react-dom/client";
import { StrictMode } from "react";
import { App } from "./App";
import { ThemeProvider } from "@/components/theme-provider";
import BottomBar from "./components/BottomBar";
import App from "./app";
const elem = document.getElementById("root")!;
const app = (
<StrictMode>
<ThemeProvider storageKey="bun-ui-theme">
<BottomBar />
<App />
</ThemeProvider>
<App />
</StrictMode>
);

View file

@ -1,13 +1,51 @@
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
@import "../styles/globals.css";
.section {
@apply py-16 border-b-1 border-muted;
@layer base {
:root {
@apply font-sans;
}
body {
@apply m-0 p-0 bg-background text-foreground relative min-h-screen;
}
}
@layer base {
html,
body,
#root {
@apply h-full m-0 p-0;
@font-face {
font-family: 'NType82Headline';
src: url('https://f.imnya.ng/font/NType82-Headline.woff2') format('woff2');
}
* {
font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
}
.font-ntype {
font-family: "NType82Headline" !important;
}
.tnum {
font-feature-settings: "tnum";
}
.image-scale {
transition-property: scale,border-radius box-shadow;
transition-duration: .7s;
transition-timing-function: var(--ease-out-expo)
}
@media (hover: hover) {
.image-scale:hover {
--tw-scale-x:105%;
--tw-scale-y: 105%;
--tw-scale-z: 105%;
scale: var(--tw-scale-x)var(--tw-scale-y);
border-radius: var(--radius-lg)
}
}
}
.image-scale:hover {
z-index: 10;
box-shadow: 0 15px 45px #0006
}

View file

@ -3,12 +3,12 @@
<head>
<!--
이번 패치하면서 들은 곡
Laur - Gears of Fate [AWC 2025 Finals Tiebreaker]
https://www.youtube.com/watch?v=-bnrmxa2dW0
ブルーアーカイブ Blue Archive OST 266 (히카리 메모리얼)
https://www.youtube.com/watch?v=04rUEmcjkLQ
-->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
<link rel="icon" type="image/ico" href="./favicon.ico" />
<title>남현석 | :two_hearts:</title>
<meta name="description" content="항상 탐구하고 연구하는 평범한 학생 개발자입니다." />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4588517451789913" crossorigin="anonymous"></script>

View file

@ -1,6 +1,5 @@
import { serve } from "bun";
import index from "./index.html";
import serveStatic from "serve-static-bun";
// Parse command line arguments for port
const args = process.argv.slice(2);
@ -8,14 +7,8 @@ const portArgIndex = args.findIndex(arg => arg === "--port");
const port = portArgIndex !== -1 && args[portArgIndex + 1] ?
parseInt(args[portArgIndex + 1]) : 3000;
const server = serve({
development: {
// New: enable console log streaming
console: true,
// Enable hot module reloading
hmr: true,
},
const server = serve({
port: port,
routes: {
// Serve index.html for all unmatched routes.
@ -30,7 +23,21 @@ const server = serve({
},
),
},
development: process.env.NODE_ENV !== "production",
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.log(`🚀 Server running at ${server.url}`);
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`);

233
src/link.css Normal file
View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Before After
Before After

4
src/type.d.ts vendored Normal file
View file

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

9
src/types.d.ts vendored
View file

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