feat(ui/ux): add timeline, hero, and NeoFetch components + data/hooks
- Add timeline route helper
- src/app/timeline/route.ts
- simple helper to navigate to #timeline
- Add NeoFetch component (client)
- src/components/NeoFetch.tsx
- Displays avatar iframe, uptime calculation, experience count, WakaTime stats, terminal/ip, locale and colour palette
- Uses custom hooks useIpData and useWakaTimeData, and events data
- Add Top (hero) component (client)
- src/components/Top.tsx
- Full-screen hero with randomized background, parallax on mouse, device orientation & motion handlers, requestPermission trigger on image click
- Includes Sidebar import and optimized Image usage
- Add Timeline UI component (client)
- src/components/timeline.tsx
- Year selector + filtered event list with links and icons
- Handles initial selection and rendering grouped by year
- Add reusable Timeline primitives (client)
- src/components/ui/timeline.tsx
- Timeline context and composable parts: Timeline, TimelineItem, Indicator, Separator, Date, Title, Content, Header
- Orientation support and controlled/uncontrolled API
- Add data & hooks
- src/lib/events.ts
- Seeded events array (education/awards/conference entries) used by timeline and NeoFetch
- src/hooks/use-ip-data.ts
- Fetches terminal/ip info from https://api.imnya.ng/ip
- src/hooks/use-wakatime-data.ts
- Fetches WakaTime summary from https://api.imnya.ng/wakatime
Notes:
- All new components are client-side ("use client")
- Adds device motion/orientation listeners with cleanup
- Provides basic error handling for network hooks
- Improves homepage/UX with interactive hero and timeline data visualization
This commit is contained in:
parent
2e1f6a7ec4
commit
96a43a9a3c
34 changed files with 771 additions and 63 deletions
86
src/components/timeline.tsx
Normal file
86
src/components/timeline.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"use client";
|
||||
import { events } from "@/lib/events";
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function TimelineComponent() {
|
||||
const [selectedYear, setSelectedYear] = useState<number | null>(null);
|
||||
|
||||
const years = Array.from(
|
||||
new Set(events.map((event) => new Date(event.date).getFullYear()))
|
||||
).sort((a, b) => b - a);
|
||||
|
||||
useEffect(() => {
|
||||
if (years.length > 0 && selectedYear === null) {
|
||||
setSelectedYear(years[0]);
|
||||
}
|
||||
}, [years, selectedYear]);
|
||||
|
||||
const filteredEvents = selectedYear
|
||||
? events.filter(
|
||||
(event) => new Date(event.date).getFullYear() === selectedYear
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div
|
||||
id="timeline"
|
||||
className="w-full flex flex-col items-center justify-center px-12 mt-8"
|
||||
>
|
||||
<div className="w-full">
|
||||
<h1 className="text-2xl font-bold mb-4 w-full">🌠 수상 및 교육</h1>
|
||||
<br />
|
||||
<div className="flex flex-col md:flex-row gap-4 h-full">
|
||||
{/* Left column - Year buttons */}
|
||||
<div className="w-full md:w-24 flex flex-row md:flex-col gap-2 overflow-y-auto pr-2">
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
onClick={() => setSelectedYear(year)}
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all text-sm ${
|
||||
selectedYear === year
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-background border border-border hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right column - Events */}
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
<div className="space-y-2">
|
||||
{filteredEvents.map((event, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-background px-4 py-3"
|
||||
>
|
||||
<div className="flex flex-row gap-2 mb-1">
|
||||
<span className="text-md font-semibold fixed-width-number">
|
||||
{new Date(event.date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span className="text-md font-semibold fixed-width-number text-muted-foreground">
|
||||
ㆍ{event.category}
|
||||
</span>
|
||||
</div>
|
||||
{event.link ? (
|
||||
<a href={event.link} className="">
|
||||
{event.description}{" "}
|
||||
<LinkIcon className="inline-block w-4 h-4 mb-1 ml-1" />
|
||||
</a>
|
||||
) : (
|
||||
<span>{event.description}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue