바로 포폴 정상화화
This commit is contained in:
parent
f85634f651
commit
de09401b68
45 changed files with 4401 additions and 1800 deletions
16
src/App.tsx
Normal file
16
src/App.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Routes, Route } from "react-router";
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
|
||||
import Home from "./routes/Home";
|
||||
import Timeline from "./routes/Timeline";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/timeline" element={<Timeline />} />
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
97
src/components/BottomBar.tsx
Normal file
97
src/components/BottomBar.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Send, AlignJustify, BadgeCheck, House, CircleHelp, ChartGantt, PhoneCall } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
|
||||
export default function BottomBar() {
|
||||
const location = useLocation();
|
||||
|
||||
const [email, setEmail] = useState<string>('me@imnya.ng');
|
||||
|
||||
useEffect(() => {
|
||||
const emaillist = ['me', 'mail', 'not', 'cat', 'neko', 'meow', 'heart']
|
||||
const domainlist = ['imnya.ng', 'al-1s.kr']
|
||||
|
||||
// furry is 0.001%
|
||||
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();
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<Link to={`mailto:${email}`} className="flex flex-row gap-4"><Send width={16} /> {email}</Link>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button><AlignJustify /></button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<House />
|
||||
)}
|
||||
<Link to="/#top">Home</Link>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<CircleHelp />
|
||||
)}
|
||||
<Link to="/#about">About</Link>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<ChartGantt />
|
||||
)}
|
||||
<Link to="/#timeline">Timeline</Link>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<PhoneCall />
|
||||
)}
|
||||
<Link to="/#contact">Contact</Link>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>© 2021-2025 imnyang</DropdownMenuLabel>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
80
src/components/Home/About.tsx
Normal file
80
src/components/Home/About.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { useEffect, useState, useRef } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export default function About() {
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const aboutRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
if (isVisible && count < 15) {
|
||||
const timer = setTimeout(() => setCount(count + 1), count === 0 ? 600 : 25);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isVisible, count]);
|
||||
|
||||
const [posts, setPosts] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("https://api.imnya.ng/rss", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data) {
|
||||
setPosts(data.slice(0, 3));
|
||||
} else {
|
||||
console.error("Error: data is undefined");
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error fetching posts:", error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={aboutRef} className="w-full h-screen flex flex-col items-center justify-center">
|
||||
<div className="w-full md:w-[50%] p-4">
|
||||
<h1 className="text-2xl font-bold">About</h1>
|
||||
<p className="mt-2">안녕하세요! 저는 암냥이라는 이름으로 활동하고 있는 학생 개발자 남현석입니다.</p>
|
||||
<p className="mt-2">현재 팀 <a href="https://sqlare.com">Sqlare</a>, <a href="https://orygonix.com">TEAM ORYGON:IX</a>에서 활동하고 있습니다.</p>
|
||||
</div>
|
||||
<div className="w-full md:w-[50%] p-4">
|
||||
<strong>현재까지의 수상 경력</strong>
|
||||
<p>{count}회</p>
|
||||
</div>
|
||||
<div className="w-full md:w-[50%] p-4">
|
||||
<strong>최근 블로그 글</strong>
|
||||
<ul>
|
||||
{posts.map((post, index) => (
|
||||
<li key={index}>
|
||||
<Link to={post.link}>{post.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/components/Home/Contact.tsx
Normal file
42
src/components/Home/Contact.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Github, Instagram, Rss } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function Contact() {
|
||||
return (
|
||||
<div className="w-full h-screen flex items-center justify-center">
|
||||
<div className="w-full md:w-[50%] p-4 flex items-center justify-center flex-row gap-4">
|
||||
<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/fur_local" 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/loopback.ip" 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>
|
||||
);
|
||||
}
|
||||
170
src/components/Home/Timeline.tsx
Normal file
170
src/components/Home/Timeline.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||
import { Link } from "react-router";
|
||||
|
||||
|
||||
const events = [
|
||||
{
|
||||
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: "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",
|
||||
link: "https://lgaiyouthcamp.or.kr/",
|
||||
},
|
||||
{
|
||||
date: "2024-03-24",
|
||||
description: "Dreamhack #133",
|
||||
link: "https://dreamhack.io/users/40116/wargame",
|
||||
},
|
||||
{
|
||||
date: "2023-12-20",
|
||||
description: "LG AI 청소년 캠프 1기 합격",
|
||||
category: "Education",
|
||||
},
|
||||
{
|
||||
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() {
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full h-screen md:p-16 md:pb-16 flex flex-col items-center justify-center">
|
||||
<h1 className="text-2xl font-bold mb-4 w-full">Timeline (Beta)</h1>
|
||||
<ScrollArea className="w-full h-[85%] whitespace-nowrap rounded-md border">
|
||||
<div className="flex w-max space-x-4 p-4">
|
||||
|
||||
{Array.from(new Set(events.map(event => new Date(event.date).getFullYear()))).sort((a, b) => b - a).map(year => (
|
||||
<div key={year} className="mb-6">
|
||||
<h2 className="text-3xl font-bold text-transparent opacity-60 select-none" style={{ WebkitTextStrokeWidth: '1px', WebkitTextStrokeColor: 'var(--foreground)' }}>{year}</h2>
|
||||
{events.filter(event => new Date(event.date).getFullYear() === year).map((event, index) => (
|
||||
<div key={index} className="my-2">
|
||||
<p className="text-lg font-semibold">{new Date(event.date).toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' })}</p>
|
||||
{event.link ? (
|
||||
<Link to={event.link} className="text-md">{event.description}</Link>
|
||||
) : (
|
||||
<span className="text-md">{event.description}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/components/Home/Top.tsx
Normal file
37
src/components/Home/Top.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function Top() {
|
||||
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-[40%] 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-4xl font-bold">Nam<br/>HyunSuk</h1>
|
||||
<p className="mt-4 text-xl">
|
||||
암냥이라는 이름으로 활동하고 있는<br/> 학생 개발자 남현석라고 합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="w-full md:w-[60%] h-full flex justify-center md:justify-end items-end bg-transparent md:bg-[#654e5c] rounded-3xl mt-8 md:mt-0 select-none">
|
||||
<img
|
||||
src="https://f.imnya.ng/profile/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.avif"
|
||||
className="w-[60%] max-w-[360px] select-none pointer-events-none"
|
||||
title="Special Thanks to @dob2_"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="end" side="bottom" className="px-2 py-1 text-xs">Special Thanks to @dob2_</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/components/theme-provider.tsx
Normal file
73
src/components/theme-provider.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
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 = "vite-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
|
||||
}
|
||||
199
src/components/ui/dropdown-menu.tsx
Normal file
199
src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
46
src/components/ui/scroll-area.tsx
Normal file
46
src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
59
src/components/ui/tooltip.tsx
Normal file
59
src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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 = 4,
|
||||
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 max-w-sm rounded-md px-3 py-1.5 text-xs",
|
||||
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 }
|
||||
129
src/index.css
Normal file
129
src/index.css
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
|
||||
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@import "tailwindcss";
|
||||
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(0 0% 3.9%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(0 0% 3.9%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(0 0% 3.9%);
|
||||
--primary: hsl(0 0% 9%);
|
||||
--primary-foreground: hsl(0 0% 98%);
|
||||
--secondary: hsl(0 0% 96.1%);
|
||||
--secondary-foreground: hsl(0 0% 9%);
|
||||
--muted: hsl(0 0% 96.1%);
|
||||
--muted-foreground: hsl(0 0% 45.1%);
|
||||
--accent: hsl(0 0% 96.1%);
|
||||
--accent-foreground: hsl(0 0% 9%);
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
--destructive-foreground: hsl(0 0% 98%);
|
||||
--border: hsl(0 0% 89.8%);
|
||||
--input: hsl(0 0% 89.8%);
|
||||
--ring: hsl(0 0% 3.9%);
|
||||
--chart-1: hsl(12 76% 61%);
|
||||
--chart-2: hsl(173 58% 39%);
|
||||
--chart-3: hsl(197 37% 24%);
|
||||
--chart-4: hsl(43 74% 66%);
|
||||
--chart-5: hsl(27 87% 67%);
|
||||
--radius: 0.6rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(0 0% 3.9%);
|
||||
--foreground: hsl(0 0% 98%);
|
||||
--card: hsl(0 0% 3.9%);
|
||||
--card-foreground: hsl(0 0% 98%);
|
||||
--popover: hsl(0 0% 3.9%);
|
||||
--popover-foreground: hsl(0 0% 98%);
|
||||
--primary: hsl(0 0% 98%);
|
||||
--primary-foreground: hsl(0 0% 9%);
|
||||
--secondary: hsl(0 0% 14.9%);
|
||||
--secondary-foreground: hsl(0 0% 98%);
|
||||
--muted: hsl(0 0% 14.9%);
|
||||
--muted-foreground: hsl(0 0% 63.9%);
|
||||
--accent: hsl(0 0% 14.9%);
|
||||
--accent-foreground: hsl(0 0% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(0 0% 98%);
|
||||
--border: hsl(0 0% 14.9%);
|
||||
--input: hsl(0 0% 14.9%);
|
||||
--ring: hsl(0 0% 83.1%);
|
||||
--chart-1: hsl(220 70% 50%);
|
||||
--chart-2: hsl(160 60% 45%);
|
||||
--chart-3: hsl(30 80% 55%);
|
||||
--chart-4: hsl(280 65% 60%);
|
||||
--chart-5: hsl(340 75% 55%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-primary text-background;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2 h-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-muted;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded-full bg-muted-foreground;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0 1000px hsl(var(--background)) inset !important;
|
||||
box-shadow: 0 0 0 1000px hsl(var(--background)) inset !important;
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
}
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
16
src/main.tsx
Normal file
16
src/main.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from "react-router";
|
||||
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import BottomBar from './components/BottomBar.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<BottomBar />
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
14
src/routes/Home.css
Normal file
14
src/routes/Home.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#fullpage {
|
||||
scroll-snap-type: y mandatory;
|
||||
overflow-y: scroll;
|
||||
height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.section {
|
||||
scroll-snap-align: start;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
38
src/routes/Home.tsx
Normal file
38
src/routes/Home.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import Top from "@/components/Home/Top";
|
||||
import About from "@/components/Home/About";
|
||||
import Timeline from "@/components/Home/Timeline";
|
||||
|
||||
import './Home.css';
|
||||
import Contact from '@/components/Home/Contact';
|
||||
|
||||
export default function Home() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash) {
|
||||
const element = document.getElementById(location.hash.substring(1));
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div id="fullpage" className="bg-background text-foreground w-full">
|
||||
<div id="top" className="bg-background text-foreground w-full h-screen flex items-center justify-center section">
|
||||
<Top />
|
||||
</div>
|
||||
<div id="about" className="bg-background text-foreground w-full h-screen flex items-center justify-center section">
|
||||
<About />
|
||||
</div>
|
||||
<div id="timeline" className="bg-background text-foreground w-full h-screen flex items-center justify-center section">
|
||||
<Timeline />
|
||||
</div>
|
||||
<div id="contact" className="bg-background text-foreground w-full h-screen flex items-center justify-center section">
|
||||
<Contact />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/routes/Timeline.tsx
Normal file
11
src/routes/Timeline.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
export default function Timeline() {
|
||||
useEffect(() => {
|
||||
window.location.href = "/#timeline";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<h1 className="w-full h-screen flex items-center justify-center">Redirecting</h1>
|
||||
)
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
Loading…
Add table
Add a link
Reference in a new issue