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
105
src/components/Top.tsx
Normal file
105
src/components/Top.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use client";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function Top() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [randomBg, setRandomBg] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
setRandomBg(Math.floor(Math.random() * 14) + 1);
|
||||
}, []);
|
||||
|
||||
const requestPermission = async () => {
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
if (!isIOS)
|
||||
window.addEventListener("deviceorientation", handleDeviceOrientation);
|
||||
|
||||
try {
|
||||
const res = await (DeviceOrientationEvent as any).requestPermission();
|
||||
if (res === "granted") {
|
||||
window.addEventListener("devicemotion", handleDeviceMotion);
|
||||
window.addEventListener("deviceorientation", handleDeviceOrientation);
|
||||
}
|
||||
} catch {
|
||||
window.addEventListener("deviceorientation", handleDeviceOrientation);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeviceOrientation = (event: DeviceOrientationEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
const gamma = event.gamma || 0;
|
||||
const beta = event.beta || 0;
|
||||
|
||||
const x = (gamma / 90) * -50;
|
||||
const y = (beta / 180) * -50;
|
||||
|
||||
const bg = containerRef.current;
|
||||
bg.style.backgroundPosition = `${50 + x}% ${50 + y}%`;
|
||||
|
||||
const img = bg.querySelector("img");
|
||||
if (img) img.style.transform = `translate(${x * 2}px, ${y * 2}px)`;
|
||||
};
|
||||
|
||||
const handleDeviceMotion = (event: DeviceMotionEvent) => {
|
||||
if (!containerRef.current || !event.accelerationIncludingGravity) return;
|
||||
const { x, y } = event.accelerationIncludingGravity;
|
||||
if (x === null || y === null) return;
|
||||
const bg = containerRef.current;
|
||||
bg.style.backgroundPosition = `${50 - x * 3}% ${50 - y * 3}%`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.removeEventListener("deviceorientation", handleDeviceOrientation);
|
||||
window.removeEventListener("devicemotion", handleDeviceMotion);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="top"
|
||||
className="snap-start h-screen w-full relative"
|
||||
onWheel={(e) => {
|
||||
e.preventDefault();
|
||||
window.scrollBy(0, window.innerHeight * (e.deltaY > 0 ? 1 : -1));
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute top-0 right-0 w-[calc(100%-100px)] h-screen lg:h-[calc(100vh-56px)] flex flex-col items-center justify-end lg:rounded-bl-[160px] bg-cover bg-center overflow-hidden"
|
||||
style={{
|
||||
backgroundImage: `url('/background/${randomBg}.avif')`,
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
const { clientX, clientY } = e;
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const x = (clientX / innerWidth - 0.5) * -50;
|
||||
const y = (clientY / innerHeight - 0.5) * -50;
|
||||
|
||||
const bg = e.currentTarget;
|
||||
if (bg) {
|
||||
bg.style.backgroundPosition = `${50 + x}% ${50 + y}%`;
|
||||
}
|
||||
const img = e.currentTarget.querySelector("img");
|
||||
if (img) {
|
||||
img.style.transform = `translate(${x}px, ${y}px)`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={"/char.avif"}
|
||||
alt="character"
|
||||
width={4096}
|
||||
height={4096}
|
||||
className="w-[50vh] lg:w-[30vw] translate-y-[10%] transition-transform duration-100 ease-out"
|
||||
unoptimized
|
||||
onClick={requestPermission}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue