rewrite #16
7 changed files with 212 additions and 339 deletions
wow
commit
0d5c3f88a8
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
import { events } from "@/lib/events";
|
import { events } from "@/lib/events";
|
||||||
|
import AboutRow from "@/components/AboutRow.astro";
|
||||||
|
|
||||||
const sortedEvents = [...events].sort((a, b) => b.date.localeCompare(a.date));
|
const sortedEvents = [...events].sort((a, b) => b.date.localeCompare(a.date));
|
||||||
const years = [...new Set(sortedEvents.map((event) => event.date.slice(0, 4)))];
|
const years = [...new Set(sortedEvents.map((event) => event.date.slice(0, 4)))];
|
||||||
|
|
@ -31,104 +32,62 @@ const daysUntilYearEnd = Math.ceil(
|
||||||
>
|
>
|
||||||
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
|
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
|
||||||
<div
|
<div
|
||||||
class="relative z-10 px-6 py-12 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)]"
|
class="relative z-10 px-6 py-12 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)] text-base"
|
||||||
>
|
>
|
||||||
<div
|
<p>
|
||||||
id="experience"
|
안녕하세요, 정보보안과 소프트웨어 엔지니어링을 주로하고 있는
|
||||||
class="max-w-[654px] break-keep text-[24px] leading-[1.2] tracking-[-0.02em] lg:leading-none"
|
암냥입니다.
|
||||||
>
|
|
||||||
<p id="about-title">
|
|
||||||
저는 <b>{years.length}년간 {events.length}개</b>의
|
|
||||||
개성있는 조각들을 모아왔어요.
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>저는...</p>
|
||||||
<section class="mt-[0.75em]" aria-label="Experience">
|
<section class="mt-[0.75em]">
|
||||||
{
|
<div class="mt-[0.5em] flex flex-col gap-[0.34em]">
|
||||||
eventsByYear.map(({ year, events: yearEvents }) => (
|
<AboutRow year="2026" connector="wave">
|
||||||
<section
|
<img
|
||||||
class="py-[0.55em]"
|
class="h-[1em] w-[1em] shrink-0"
|
||||||
aria-labelledby={`experience-${year}`}
|
src="/about/layer7-mark.svg"
|
||||||
>
|
alt=""
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<h3
|
|
||||||
id={`experience-${year}`}
|
|
||||||
class="text-[0.62em] font-bold leading-none"
|
|
||||||
>
|
|
||||||
{year}
|
|
||||||
</h3>
|
|
||||||
<span class="text-[0.43em] font-medium tracking-[0.04em] text-foreground/45">
|
|
||||||
{yearEvents.length}{" "}
|
|
||||||
{yearEvents.length === 1
|
|
||||||
? "EVENT"
|
|
||||||
: "EVENTS"}
|
|
||||||
</span>
|
|
||||||
<div class="h-px flex-1 bg-foreground/10" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ol class="mt-[0.35em]">
|
|
||||||
{yearEvents.map((event) => (
|
|
||||||
<li class="grid min-w-0 grid-cols-[3.5em_minmax(0,1fr)] gap-x-[0.6em] border-b border-foreground/[0.07] py-[0.48em] last:border-b-0">
|
|
||||||
<time
|
|
||||||
class="pt-[0.08em] text-[0.55em] font-medium tabular-nums tracking-[0.02em] text-foreground/50"
|
|
||||||
datetime={event.date}
|
|
||||||
>
|
|
||||||
{getShortDate(event.date)}
|
|
||||||
</time>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<div class="mb-[0.12em] flex items-center gap-[0.45em]">
|
|
||||||
<span class="text-[0.43em] font-semibold tracking-[0.04em] text-foreground/50">
|
|
||||||
{event.category}
|
|
||||||
</span>
|
|
||||||
{event.link && (
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="h-[0.52em] w-[0.52em] text-foreground/40"
|
/>
|
||||||
viewBox="0 0 24 24"
|
<span>선린인터넷고등학교 정보보안 동아리</span>
|
||||||
fill="none"
|
<img
|
||||||
stroke="currentColor"
|
class="h-[1em] w-[1em] shrink-0 [filter:var(--mono-filter)]"
|
||||||
stroke-width="2"
|
src="/about/layer7-wordmark.svg"
|
||||||
stroke-linecap="round"
|
alt=""
|
||||||
stroke-linejoin="round"
|
aria-hidden="true"
|
||||||
>
|
/>
|
||||||
<path d="M7 17 17 7" />
|
<span>Layer7</span>
|
||||||
<path d="M7 7h10v10" />
|
</AboutRow>
|
||||||
</svg>
|
|
||||||
)}
|
<AboutRow year="2026" connector="wave">
|
||||||
|
<img
|
||||||
|
class="h-[1em] w-[1em] shrink-0"
|
||||||
|
src="/about/sunrin.svg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>선린인터넷고등학교 정보보호과</span>
|
||||||
|
</AboutRow>
|
||||||
|
<AboutRow year="2026" connector="line">
|
||||||
|
<img
|
||||||
|
class="h-[1em] w-[1em] shrink-0 object-contain"
|
||||||
|
src="/about/adofai.svg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>ADOFAI.gg Development Team</span>
|
||||||
|
</AboutRow>
|
||||||
|
<AboutRow year="2025" connector="line">
|
||||||
|
<img
|
||||||
|
class="h-[1em] w-[1em] shrink-0 object-contain"
|
||||||
|
src="/about/whitehat.avif"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>화이트햇스쿨 3기 이수</span>
|
||||||
|
</AboutRow>
|
||||||
</div>
|
</div>
|
||||||
{event.link ? (
|
|
||||||
<a
|
|
||||||
href={event.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-[0.62em] leading-[1.35] underline decoration-foreground/20 underline-offset-2 transition-colors hover:decoration-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground"
|
|
||||||
>
|
|
||||||
{event.description}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<p class="text-[0.62em] leading-[1.35]">
|
|
||||||
{event.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
{year === currentYear.toString() && (
|
|
||||||
<div class="my-8 flex items-center gap-4">
|
|
||||||
<div class="flex-1 h-px bg-foreground/10" />
|
|
||||||
<span class="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
✨ | D-{daysUntilYearEnd}동안
|
|
||||||
만들어나갈 멋진 것들을
|
|
||||||
기대해주세요.
|
|
||||||
</span>
|
|
||||||
<div class="flex-1 h-px bg-foreground/10" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
))
|
<p></p>
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
|
||||||
class="mt-6 max-w-[32rem] text-base leading-relaxed text-foreground/65"
|
class="mt-6 max-w-[32rem] text-base leading-relaxed text-foreground/65"
|
||||||
>
|
>
|
||||||
불과 얼음의 춤(A Dance of Fire and Ice)의 이펙트 제작과
|
불과 얼음의 춤(A Dance of Fire and Ice)의 이펙트 제작과
|
||||||
플레이를 함께 다루는 대회, Effect Playing Contest. 2025와
|
플레이를<br /> 함께 다루는 대회, Effect Playing Contest. 2025와
|
||||||
Finale 시즌에 참여하며 방송부터 ARG, 개발까지 함께했어요.
|
Finale 시즌에 참여하며 방송부터 ARG, 개발까지 함께했어요.
|
||||||
</p>
|
</p>
|
||||||
<ul
|
<ul
|
||||||
|
|
@ -178,7 +178,7 @@ const roles = ["Broadcast Manager", "ARG Creator", "Developer"];
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
class="absolute bottom-0 left-[-6%] z-[3] pointer-events-none h-auto w-[13%] aspect-[94/131] rotate-90"
|
class="absolute bottom-[-10%] left-[-6%] z-[3] pointer-events-none h-auto w-[13%] aspect-[94/131] rotate-90"
|
||||||
src="/projects/tape.svg"
|
src="/projects/tape.svg"
|
||||||
alt=""
|
alt=""
|
||||||
width="94"
|
width="94"
|
||||||
|
|
|
||||||
146
src/components/Experience.astro
Normal file
146
src/components/Experience.astro
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
---
|
||||||
|
import { events } from "@/lib/events";
|
||||||
|
|
||||||
|
const sortedEvents = [...events].sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
const years = [...new Set(sortedEvents.map((event) => event.date.slice(0, 4)))];
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
|
||||||
|
const eventsByYear = years.map((year) => ({
|
||||||
|
year,
|
||||||
|
events: sortedEvents.filter((event) => event.date.startsWith(year)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
|
||||||
|
const getShortDate = (date: string) => {
|
||||||
|
const [, month, day] = date.split("-");
|
||||||
|
return `${month}.${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: Used in the Astro template below.
|
||||||
|
const daysUntilYearEnd = Math.ceil(
|
||||||
|
(new Date(currentYear + 1, 0, 1).getTime() - Date.now()) /
|
||||||
|
(1000 * 60 * 60 * 24),
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="about"
|
||||||
|
data-node-id="4:104"
|
||||||
|
class="min-h-full snap-start snap-always bg-background text-foreground"
|
||||||
|
aria-labelledby="about-title"
|
||||||
|
>
|
||||||
|
<div class="grid min-h-full grid-cols-1 lg:grid-cols-2">
|
||||||
|
<div
|
||||||
|
class="relative z-10 px-6 py-12 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="experience"
|
||||||
|
class="max-w-[654px] break-keep text-[24px] leading-[1.2] tracking-[-0.02em] lg:leading-none"
|
||||||
|
>
|
||||||
|
<p id="about-title">
|
||||||
|
저는 <b>{years.length}년간 {events.length}개</b>의 개성있는
|
||||||
|
조각들을 모아왔어요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="mt-[0.75em]" aria-label="Experience">
|
||||||
|
{
|
||||||
|
eventsByYear.map(({ year, events: yearEvents }) => (
|
||||||
|
<section
|
||||||
|
class="py-[0.55em]"
|
||||||
|
aria-labelledby={`experience-${year}`}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h3
|
||||||
|
id={`experience-${year}`}
|
||||||
|
class="text-[0.62em] font-bold leading-none"
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</h3>
|
||||||
|
<span class="text-[0.43em] font-medium tracking-[0.04em] text-foreground/45">
|
||||||
|
{yearEvents.length}{" "}
|
||||||
|
{yearEvents.length === 1
|
||||||
|
? "EVENT"
|
||||||
|
: "EVENTS"}
|
||||||
|
</span>
|
||||||
|
<div class="h-px flex-1 bg-foreground/10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol class="mt-[0.35em]">
|
||||||
|
{yearEvents.map((event) => (
|
||||||
|
<li class="grid min-w-0 grid-cols-[3.5em_minmax(0,1fr)] gap-x-[0.6em] border-b border-foreground/[0.07] py-[0.48em] last:border-b-0">
|
||||||
|
<time
|
||||||
|
class="pt-[0.08em] text-[0.55em] font-medium tabular-nums tracking-[0.02em] text-foreground/50"
|
||||||
|
datetime={event.date}
|
||||||
|
>
|
||||||
|
{getShortDate(event.date)}
|
||||||
|
</time>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="mb-[0.12em] flex items-center gap-[0.45em]">
|
||||||
|
<span class="text-[0.43em] font-semibold tracking-[0.04em] text-foreground/50">
|
||||||
|
{event.category}
|
||||||
|
</span>
|
||||||
|
{event.link && (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-[0.52em] w-[0.52em] text-foreground/40"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M7 17 17 7" />
|
||||||
|
<path d="M7 7h10v10" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{event.link ? (
|
||||||
|
<a
|
||||||
|
href={event.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-[0.62em] leading-[1.35] underline decoration-foreground/20 underline-offset-2 transition-colors hover:decoration-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground"
|
||||||
|
>
|
||||||
|
{event.description}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<p class="text-[0.62em] leading-[1.35]">
|
||||||
|
{event.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{year === currentYear.toString() && (
|
||||||
|
<div class="my-8 flex items-center gap-4">
|
||||||
|
<div class="flex-1 h-px bg-foreground/10" />
|
||||||
|
<span class="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
✨ | D-{daysUntilYearEnd}동안
|
||||||
|
만들어나갈 멋진 것들을 기대해주세요.
|
||||||
|
</span>
|
||||||
|
<div class="flex-1 h-px bg-foreground/10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative order-first h-[min(150vw,720px)] min-h-[600px] overflow-hidden lg:sticky lg:top-0 lg:order-none lg:h-[calc(100svh-64px)] lg:min-h-0 lg:self-start"
|
||||||
|
aria-label="활동 사진"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="absolute inset-0 h-full w-full select-none pointer-events-none object-contain"
|
||||||
|
src="/side.avif"
|
||||||
|
alt="활동 사진 콜라주"
|
||||||
|
width="3840"
|
||||||
|
height="4320"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -22,11 +22,11 @@
|
||||||
<span class="block" data-node-id="43:16">
|
<span class="block" data-node-id="43:16">
|
||||||
<span class="font-bold">현실</span><span
|
<span class="font-bold">현실</span><span
|
||||||
class="font-normal text-foreground/50"
|
class="font-normal text-foreground/50"
|
||||||
>로 만들어나가는</span
|
>{"로 만들어나가는 "}
|
||||||
><br class="hidden max-md:block md:hidden" /><span
|
</span><br class="hidden max-md:block md:hidden" /><span
|
||||||
class="font-bold"
|
class="font-bold"
|
||||||
>
|
>
|
||||||
남현석</span
|
암냥</span
|
||||||
><span class="font-normal text-foreground/50">입니다.</span>
|
><span class="font-normal text-foreground/50">입니다.</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
---
|
|
||||||
const projects = [
|
|
||||||
{
|
|
||||||
name: 'EPC : Finale Broadcast Manager & Developer',
|
|
||||||
url: 'https://www.youtube.com/playlist?list=PLZeYZotn5_IPFvDUWdEUu9xOBwOkdZ0F0',
|
|
||||||
desc: 'ADOFAI is web-scale',
|
|
||||||
detail: '달성이 주관하고 ADOFAI.gg가 공동 주최하는 Effect Playing Contest : Finale의 방송 화면 및 ARG를 담당하였습니다.',
|
|
||||||
tags: ['React', 'ElysiaJS'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'EPC 2025 Broadcast Manager',
|
|
||||||
url: 'https://www.youtube.com/playlist?list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD',
|
|
||||||
desc: 'ADOFAI is web-scale',
|
|
||||||
detail: '달성이 주관하고 ADOFAI.gg가 공동 주최하는 Effect Playing Contest 2025 방송 화면의 대부분의 기능을 개발하였습니다.',
|
|
||||||
tags: ['React', 'ElysiaJS'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newsletter',
|
|
||||||
url: 'https://github.com/imnyang/newsletter',
|
|
||||||
desc: 'For Memos',
|
|
||||||
detail: '그저 이메일이 오면 Discord 웹훅으로 포워딩합니다.',
|
|
||||||
tags: ['Rust', 'IMAP']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'memos-rss',
|
|
||||||
url: 'https://github.com/imnyang/memos-rss',
|
|
||||||
desc: 'For Memos',
|
|
||||||
detail: 'Discord 포럼 채널에 RSS 피드에 올라온 내용을 포워딩합니다.',
|
|
||||||
tags: ['Rust', 'Discord Bot', 'RSS']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'today.isangjeong',
|
|
||||||
url: 'https://instagram.com/today.isangjeong',
|
|
||||||
desc: 'Instagram Bot',
|
|
||||||
detail: '매일 학교의 급식 메뉴를 자동으로 업로드하는 인스타그램 봇입니다.',
|
|
||||||
tags: ['TypeScript', 'Instagram', '@napi-rs/canvas']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
---
|
|
||||||
|
|
||||||
<section class="break-keep break-words w-full">
|
|
||||||
<div class="space-y-8" id="projects-list">
|
|
||||||
{projects.map((project, idx) => (
|
|
||||||
<div class={`space-y-2 project-item ${idx >= 3 ? 'hidden' : ''}`}>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div class="flex justify-between items-center gap-y-2 gap-x-4">
|
|
||||||
{project.url ? (
|
|
||||||
<a href={project.url} target="_blank" rel="noopener noreferrer" class="text-lg text-nowrap items-center gap-x-2 flex flex-row">
|
|
||||||
{project.name}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-arrow-out-up-right"><path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/><path d="m21 3-9 9"/><path d="M15 3h6v6"/></svg>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span class="text-lg">{project.name}</span>
|
|
||||||
)}
|
|
||||||
<span class="text-sm text-muted-foreground text-nowrap">{project.desc}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-muted-foreground">{project.detail}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 *:bg-muted *:px-2 *:py-0.5 *:rounded *:text-xs *:font-medium *:text-muted-foreground">
|
|
||||||
{project.tags.map((tag) => (
|
|
||||||
<span>{tag}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{projects.length > 3 && (
|
|
||||||
<button
|
|
||||||
id="load-more-btn"
|
|
||||||
class="mt-6 bg-muted text-muted-foreground rounded font-medium text-sm hover:bg-muted/80 px-4 py-2 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
더 불러오기
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let visibleCount = 3;
|
|
||||||
const btn = document.getElementById('load-more-btn');
|
|
||||||
const items = document.querySelectorAll('.project-item');
|
|
||||||
|
|
||||||
if (btn) {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
visibleCount += 3;
|
|
||||||
items.forEach((item, idx) => {
|
|
||||||
if (idx < visibleCount) {
|
|
||||||
item.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (visibleCount >= items.length) {
|
|
||||||
btn.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
---
|
|
||||||
import { events } from "@/lib/events";
|
|
||||||
|
|
||||||
const years = [
|
|
||||||
...new Set(events.map((e) => new Date(e.date).getFullYear())),
|
|
||||||
].sort((a, b) => b - a);
|
|
||||||
|
|
||||||
const eventsByYear: Record<number, typeof events> = {};
|
|
||||||
for (const year of years) {
|
|
||||||
eventsByYear[year] = events.filter(
|
|
||||||
(e) => new Date(e.date).getFullYear() === year,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const daysUntilYearEnd = Math.ceil(
|
|
||||||
(new Date(currentYear + 1, 0, 1).getTime() - new Date().getTime()) /
|
|
||||||
(1000 * 60 * 60 * 24),
|
|
||||||
);
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="timeline"
|
|
||||||
class="w-full flex flex-col items-center justify-center mt-8"
|
|
||||||
>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex flex-col md:flex-row gap-4 h-full">
|
|
||||||
<!-- Left: Year buttons -->
|
|
||||||
<div
|
|
||||||
class="w-full md:w-24 flex flex-row md:flex-col gap-2 overflow-y-auto pr-2"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
years.map((year, i) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-year={year}
|
|
||||||
class:list={[
|
|
||||||
"year-btn font-semibold px-4 py-2 rounded-lg text-sm transition-colors cursor-pointer",
|
|
||||||
i === 0
|
|
||||||
? "bg-foreground text-background"
|
|
||||||
: "border border-border bg-background text-foreground hover:bg-muted",
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{year}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Events -->
|
|
||||||
<div class="flex-1 overflow-y-auto pr-2">
|
|
||||||
{
|
|
||||||
years.map((year, i) => (
|
|
||||||
<div
|
|
||||||
data-events-year={year}
|
|
||||||
class:list={["year-events space-y-2", i !== 0 && "hidden"]}
|
|
||||||
>
|
|
||||||
{eventsByYear[year].map((event) => (
|
|
||||||
<div class="rounded-lg border bg-background px-4 py-3">
|
|
||||||
<div class="flex flex-row gap-2 mb-1">
|
|
||||||
<span class="text-md font-semibold">
|
|
||||||
{new Date(event.date).toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "2-digit",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span class="text-md font-semibold text-muted-foreground">
|
|
||||||
ㆍ{event.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{event.link ? (
|
|
||||||
<a href={event.link} class="">
|
|
||||||
{event.description}{" "}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="inline-block w-4 h-4 mb-1 ml-1"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
||||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span>{event.description}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{year === currentYear && (
|
|
||||||
<div class="my-8 flex items-center gap-4">
|
|
||||||
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-muted-foreground to-transparent" />
|
|
||||||
<span class="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
✨ | D-{daysUntilYearEnd}동안 만들어나갈 멋진 것들을
|
|
||||||
기대해주세요.
|
|
||||||
</span>
|
|
||||||
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-muted-foreground to-transparent" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const btns = document.querySelectorAll<HTMLButtonElement>(".year-btn");
|
|
||||||
const panels = document.querySelectorAll<HTMLElement>(".year-events");
|
|
||||||
|
|
||||||
btns.forEach((btn) => {
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
const year = btn.dataset.year;
|
|
||||||
|
|
||||||
btns.forEach((b) => {
|
|
||||||
b.className =
|
|
||||||
"year-btn font-semibold px-4 py-2 rounded text-sm transition-colors cursor-pointer border border-border bg-background text-foreground hover:bg-muted";
|
|
||||||
});
|
|
||||||
btn.className =
|
|
||||||
"year-btn font-semibold px-4 py-2 rounded text-sm transition-colors cursor-pointer bg-foreground text-background";
|
|
||||||
|
|
||||||
panels.forEach((panel) => {
|
|
||||||
if (panel.dataset.eventsYear === year) {
|
|
||||||
panel.classList.remove("hidden");
|
|
||||||
} else {
|
|
||||||
panel.classList.add("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
Loading…
Reference in a new issue