더 나은 TimeTable로 찾아왔습니다.
키보드 유저에 대한 배려가 있었습니다.
This commit is contained in:
parent
bfda6c0aef
commit
857105f563
10 changed files with 471 additions and 254 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Send, AlignJustify, BadgeCheck, House, CircleHelp, ChartGantt, PhoneCall } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
|
||||
export default function BottomBar() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState<string>('me@imnya.ng');
|
||||
|
||||
|
|
@ -34,11 +35,19 @@ export default function BottomBar() {
|
|||
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>
|
||||
<button onClick={() => navigate("/#top")} accessKey="1" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
||||
<button onClick={() => navigate("/#about")} accessKey="2" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
||||
<button onClick={() => navigate("/#project")} accessKey="3" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
||||
<button onClick={() => navigate("/#timeline")} accessKey="4" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
||||
<button onClick={() => navigate("/#contact")} accessKey="5" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
@ -46,43 +55,73 @@ export default function BottomBar() {
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/#top" className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<House />
|
||||
)}
|
||||
Home
|
||||
<Link to="/#top" className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||
{location.hash === "#top" ? (
|
||||
<BadgeCheck width={16} height={16} />
|
||||
) : (
|
||||
<House width={16} height={16} />
|
||||
)}
|
||||
Home
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">Alt + 1</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/#about" className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#about" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<CircleHelp />
|
||||
)}
|
||||
About
|
||||
<Link to="/#about" className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||
{location.hash === "#about" ? (
|
||||
<BadgeCheck width={16} height={16} />
|
||||
) : (
|
||||
<CircleHelp width={16} height={16} />
|
||||
)}
|
||||
About
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">Alt + 2</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/#timeline" className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#timeline" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<ChartGantt />
|
||||
)}
|
||||
Timeline
|
||||
<Link to="/#project" className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||
{location.hash === "#project" ? (
|
||||
<BadgeCheck width={16} height={16} />
|
||||
) : (
|
||||
<ChartGantt width={16} height={16} />
|
||||
)}
|
||||
Project
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">Alt + 3</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/#contact" className="flex flex-row gap-2 items-center">
|
||||
{location.hash === "#contact" ? (
|
||||
<BadgeCheck />
|
||||
) : (
|
||||
<PhoneCall />
|
||||
)}
|
||||
Contact
|
||||
<Link to="/#timeline" className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||
{location.hash === "#timeline" ? (
|
||||
<BadgeCheck width={16} height={16} />
|
||||
) : (
|
||||
<ChartGantt width={16} height={16} />
|
||||
)}
|
||||
Timeline
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">Alt + 4</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/#contact" className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||
{location.hash === "#contact" ? (
|
||||
<BadgeCheck width={16} height={16} />
|
||||
) : (
|
||||
<PhoneCall width={16} height={16} />
|
||||
)}
|
||||
Contact
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">Alt + 5</p>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
|
|
|||
|
|
@ -2,38 +2,6 @@ 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(() => {
|
||||
|
|
@ -55,15 +23,11 @@ export default function About() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={aboutRef} className="w-full h-screen flex flex-col items-center justify-center">
|
||||
<div 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>
|
||||
<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>
|
||||
<p>With <a href="https://sqlare.com">Sqlare</a>, <a href="https://team.orygonix.com">TEAM. ORYGON:IX</a></p>
|
||||
</div>
|
||||
<div className="w-full md:w-[50%] p-4">
|
||||
<strong>최근 블로그 글</strong>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export default function Project() {
|
|||
return (
|
||||
<div 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">Project</h1>
|
||||
<h1 className="text-2xl font-bold">📖 Project</h1>
|
||||
<p className="mt-2">Soon™</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,172 +1,214 @@
|
|||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||
import { Accordion, AccordionContent, AccordionItem } from "@/components/ui/accordion";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
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: "2025-01-10",
|
||||
description:
|
||||
"선린인터넷고 중학생 특별교육 이수",
|
||||
category: "Education",
|
||||
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-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",
|
||||
},
|
||||
{
|
||||
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:
|
||||
"선린인터넷고 중학생 특별교육 이수",
|
||||
category: "Education",
|
||||
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 & 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() {
|
||||
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">
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const TimelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
{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>
|
||||
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 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>
|
||||
<p>현재까지 {count}개의 개성있는 조각들이 모였어요!</p>
|
||||
<br/>
|
||||
<Accordion type="single" collapsible 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 max-h-60 overflow-y-auto">
|
||||
{events.filter(event => new Date(event.date).getFullYear() === year).map((event, index) => (
|
||||
<div key={index} className="my-2">
|
||||
<p className="text-md font-semibold fixed-width-number">{new Date(event.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit' })} ㆍ {event.category}</p>
|
||||
{event.link ? (
|
||||
<Link to={event.link}>{event.description}</Link>
|
||||
) : (
|
||||
<span>{event.description}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
64
src/components/ui/accordion.tsx
Normal file
64
src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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 last:border-b-0", 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(
|
||||
"ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all hover:underline focus-visible:ring-4 focus-visible:outline-1 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 }
|
||||
76
src/components/ui/card.tsx
Normal file
76
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
@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";
|
||||
@plugin "tailwindcss-animate";
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
|
||||
|
||||
* {
|
||||
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;
|
||||
|
|
@ -93,6 +92,26 @@
|
|||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -126,4 +145,8 @@
|
|||
box-shadow: 0 0 0 1000px hsl(var(--background)) inset !important;
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-width-number {
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
scroll-snap-type: y mandatory;
|
||||
overflow-y: scroll;
|
||||
height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.section {
|
||||
scroll-snap-align: start;
|
||||
|
|
@ -11,4 +10,4 @@
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue