Init NextJS #4

Merged
imnyang merged 11 commits from nextjs into main 2025-10-23 20:20:18 +09:00
34 changed files with 771 additions and 63 deletions
Showing only changes of commit 96a43a9a3c - Show all commits

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
암냥 2025-10-22 22:33:40 +09:00

2
.gitignore vendored
View file

@ -39,3 +39,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
certificates

View file

@ -40,6 +40,7 @@
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"next": "15.5.4", "next": "15.5.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.0", "react-day-picker": "^9.11.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@ -180,6 +181,8 @@
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
@ -214,6 +217,8 @@
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
@ -226,6 +231,10 @@
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
@ -256,10 +265,14 @@
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@ -464,6 +477,8 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-day-picker": ["react-day-picker@9.11.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-L4FYOaPrr3+AEROeP6IG2mCORZZfxJDkJI2df8mv1jyPrNYeccgmFPZDaHyAuPCBCddQFozkxbikj2NhMEYfDQ=="], "react-day-picker": ["react-day-picker@9.11.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-L4FYOaPrr3+AEROeP6IG2mCORZZfxJDkJI2df8mv1jyPrNYeccgmFPZDaHyAuPCBCddQFozkxbikj2NhMEYfDQ=="],

View file

@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
allowedDevOrigins: ['imnyang.dev'],
}; };
export default nextConfig; export default nextConfig;

View file

@ -46,6 +46,7 @@
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"next": "15.5.4", "next": "15.5.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.0", "react-day-picker": "^9.11.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",

BIN
public/background/1.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/background/10.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/background/11.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/background/12.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/background/13.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/background/14.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/background/2.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/background/3.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/background/4.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
public/background/5.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/background/6.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/background/7.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/background/8.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
public/background/9.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/bg.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
public/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

BIN
public/char.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,12 +1,12 @@
@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@font-face { @font-face {
font-family: 'NType82Headline'; font-family: "NType82Headline";
src: url('https://f.imnya.ng/font/NType82-Headline.woff2') format('woff2'); src: url("https://f.imnya.ng/font/NType82-Headline.woff2") format("woff2");
} }
.font-ntype { .font-ntype {
@ -14,43 +14,6 @@
} }
:root { :root {
--background: hsl(340 40% 98%);
--foreground: hsl(315 21% 8%);
--card: hsl(340 40% 98%);
--card-foreground: hsl(315 21% 8%);
--popover: hsl(340 40% 98%);
--popover-foreground: hsl(315 21% 8%);
--primary: hsl(340 25% 15%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(340 25% 95%);
--secondary-foreground: hsl(240 5.9% 10%);
--muted: hsl(340 20% 95%);
--muted-foreground: hsl(340 10% 60%);
--accent: hsl(340 25% 94%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(340 25% 90%);
--input: hsl(340 25% 90%);
--ring: hsl(315 21% 8%);
--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;
--sidebar-background: hsl(340 25% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(340 20% 95%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(340 20% 90%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--sidebar: hsl(0 0% 98%);
}
.dark {
--background: hsl(315 21% 8%); --background: hsl(315 21% 8%);
--foreground: hsl(0 0% 98%); --foreground: hsl(0 0% 98%);
--card: hsl(315 21% 8%); --card: hsl(315 21% 8%);
@ -151,5 +114,9 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system,
BlinkMacSystemFont, system-ui, "Segoe UI", "Apple SD Gothic Neo",
"Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", sans-serif;
} }
} }

View file

@ -1,6 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import Sidebar from "@/components/sidebar";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "남현석 | :two_hearts: imnya.ng", title: "남현석 | :two_hearts: imnya.ng",
@ -15,7 +14,6 @@ export default function RootLayout({
return ( return (
<html lang="ko"> <html lang="ko">
<body className="antialiased"> <body className="antialiased">
<Sidebar />
{children} {children}
</body> </body>
</html> </html>

View file

@ -1,18 +1,26 @@
import NeoFetch from "@/components/NeoFetch";
import TimelineComponent from "@/components/timeline";
import Top from "@/components/Top";
export default function Home() { export default function Home() {
return ( return (
<main> <main className="min-h-screen">
<div id="top" className="h-screen flex"> <Top />
<div <section id="terminal" className="h-screen">
className="absolute top-0 right-0 min-w-[calc(100vw-100px)] min-h-[calc(100vh-56px)] flex flex-col rounded-bl-[160px]" <NeoFetch />
style={{ <div className="px-12">
background: "linear-gradient(110deg, #FF8989 21.29%, rgba(85, 0, 255, 0.2) 100%)", <h1 className="text-2xl font-bold mb-4 w-full">💕 About</h1>
}} <p> , .</p>
></div> <p> 4 Python으로 , TypeScript를 .</p>
</div> <p> .</p>
<div id="about" className="h-screen flex p-4"> <br />
<h1 className="text-2xl">About</h1> <p> .</p>
<p> .</p> <a href="https://www.youtube.com/watch?v=XTdqpdTMZbw&list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD">Effect Playing Contest 2025 Broadcast Develop</a><br />
</div> <a href="https://github.com/imnyang/today.isangjeong">today.isangjeong</a>
</div>
<TimelineComponent />
</section>
</main> </main>
); );
} }

View file

@ -0,0 +1,3 @@
export const timeline = () => {
window.location.hash = '#timeline';
};

110
src/components/NeoFetch.tsx Normal file
View file

@ -0,0 +1,110 @@
"use client";
import { events } from "@/lib/events";
import { useIpData } from "../hooks/use-ip-data";
import { useWakaTimeData } from "../hooks/use-wakatime-data";
export default function NeoFetch() {
const ipData = useIpData();
const wakaTimeData = useWakaTimeData();
return (
<div>
<div className="flex flex-col md:flex-row">
<iframe
src="https://f.imnya.ng/.art.html"
className="border-0 w-[430px] h-[430px] scale-75"
></iframe>
<div className="px-12 md:py-12 text-lg font-mono">
<a href="mailto:contact@imnya.ng" className="text-[#FFD7D7]">
imnyang<span className="text-foreground">@</span>adofai.gg
</a>
<p>----------</p>
<p className="text-foreground">
<span className="text-[#FFD7D7]">Uptime</span>:{" "}
{(() => {
const startDate = new Date("2010-11-08T00:00:00+09:00");
const now = new Date();
let diff = now.getTime() - startDate.getTime();
const years = Math.floor(diff / (1000 * 60 * 60 * 24 * 365));
diff %= 1000 * 60 * 60 * 24 * 365;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
diff %= 1000 * 60 * 60 * 24;
const hours = Math.floor(diff / (1000 * 60 * 60));
diff %= 1000 * 60 * 60;
const mins = Math.floor(diff / (1000 * 60));
return `${years} years, ${days} days, ${hours} hours, ${mins} mins`;
})()}
</p>
<p className="text-foreground">
<span className="text-[#FFD7D7]">Experience</span>:{" "}
{Object.values(events).flat().length}
</p>
<p className="text-foreground">
<span className="text-[#FFD7D7]">WakaTime</span>:{" "}
{wakaTimeData?.data?.total_seconds_including_other_language
? (() => {
const seconds = wakaTimeData.data.total_seconds_including_other_language;
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days} days, ${hours} hours, ${minutes} minutes, ${Math.round(
seconds % 60
)} seconds`;
})()
: "N/A"}
</p>
<p className="text-foreground">
<span className="text-[#FFD7D7]">Most used Language</span>:{" "}
{wakaTimeData?.data?.languages[0]?.name || "N/A"}{" "}
{wakaTimeData?.data?.languages[0]?.total_seconds
? (() => {
const seconds = wakaTimeData.data.languages[0].total_seconds;
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `(${days} days, ${hours} hours, ${minutes} minutes, ${Math.round(
seconds % 60
)} seconds)`;
})()
: "N/A"}
</p>
<p>
<span className="text-[#FFD7D7]">Terminal</span>: {ipData}
</p>
<p className="text-foreground">
<span className="text-[#FFD7D7]">Locale</span>: ko_KR.UTF-8
</p>
<br />
<div id="color" className="grid grid-cols-8 w-fit">
<p className="bg-background w-8"></p>
<p className="bg-[#f38ba8] w-8"></p>
<p className="bg-[#a6e3a1] w-8"></p>
<p className="bg-[#f9e2af] w-8"></p>
<p className="bg-[#89b4fa] w-8"></p>
<p className="bg-[#f5c2e7] w-8"></p>
<p className="bg-[#94e2d5] w-8"></p>
<p className="bg-muted-foreground w-8"></p>
<p className="bg-[#45475a] w-8"></p>
<p className="bg-[#f0c0cd] w-8"></p>
<p className="bg-[#c3e9bf] w-8"></p>
<p className="bg-[#f0e0bf] w-8"></p>
<p className="bg-[#c3d9fd] w-8"></p>
<p className="bg-[#f8d2ee] w-8"></p>
<p className="bg-[#b1faee] w-8"></p>
<p className="bg-foreground w-8"></p>
</div>
</div>
</div>
</div>
);
}

105
src/components/Top.tsx Normal file
View 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>
);
}

View file

@ -1,12 +1,26 @@
import Image from "next/image"; import Image from "next/image";
import { Popover } from "./ui/popover";
export default function Sidebar() { export default function Sidebar() {
return ( return (
<div className="w-[100px] h-screen absolute top-0 left-0 flex flex-col items-center gap-6"> <div className="w-[100px] h-screen absolute top-0 left-0 flex flex-col items-center justify-between">
<Image src="/Frame.svg" alt="logo" className="w-fit h-fit mt-[50px]" width={30} height={30} /> <div className="w-full h-full flex flex-col items-center gap-6">
<div className="font-ntype rotate-90 mt-8 text-3xl opacity-70 flex flex-row gap-2 w-full"> <Image
<h1><a href="mailto:me@imnya.ng">me@imnya.ng</a></h1> src="/Frame.svg"
</div> alt="logo"
className="w-fit h-fit mt-[50px]"
width={30}
height={30}
/>
<div className="font-ntype rotate-90 mt-8 text-3xl opacity-70 flex flex-row gap-2 w-full">
<h1>
<a href="mailto:me@imnya.ng">me@imnya.ng</a>
</h1>
</div> </div>
) </div>
} <div>
<Popover />
</div>
</div>
);
}

View 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>
);
}

View file

@ -0,0 +1,210 @@
"use client"
import * as React from "react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
// Types
type TimelineContextValue = {
activeStep: number
setActiveStep: (step: number) => void
}
// Context
const TimelineContext = React.createContext<TimelineContextValue | undefined>(
undefined
)
const useTimeline = () => {
const context = React.useContext(TimelineContext)
if (!context) {
throw new Error("useTimeline must be used within a Timeline")
}
return context
}
// Components
interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: number
value?: number
onValueChange?: (value: number) => void
orientation?: "horizontal" | "vertical"
}
function Timeline({
defaultValue = 1,
value,
onValueChange,
orientation = "vertical",
className,
...props
}: TimelineProps) {
const [activeStep, setInternalStep] = React.useState(defaultValue)
const setActiveStep = React.useCallback(
(step: number) => {
if (value === undefined) {
setInternalStep(step)
}
onValueChange?.(step)
},
[value, onValueChange]
)
const currentStep = value ?? activeStep
return (
<TimelineContext.Provider
value={{ activeStep: currentStep, setActiveStep }}
>
<div
data-slot="timeline"
className={cn(
"group/timeline flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col",
className
)}
data-orientation={orientation}
{...props}
/>
</TimelineContext.Provider>
)
}
// TimelineContent
function TimelineContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="timeline-content"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
// TimelineDate
interface TimelineDateProps extends React.HTMLAttributes<HTMLTimeElement> {
asChild?: boolean
}
function TimelineDate({
asChild = false,
className,
...props
}: TimelineDateProps) {
const Comp = asChild ? Slot.Root : "time"
return (
<Comp
data-slot="timeline-date"
className={cn(
"text-muted-foreground mb-1 block text-xs font-medium group-data-[orientation=vertical]/timeline:max-sm:h-4",
className
)}
{...props}
/>
)
}
// TimelineHeader
function TimelineHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div data-slot="timeline-header" className={cn(className)} {...props} />
)
}
// TimelineIndicator
interface TimelineIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean
}
function TimelineIndicator({
asChild = false,
className,
children,
...props
}: TimelineIndicatorProps) {
return (
<div
data-slot="timeline-indicator"
className={cn(
"border-primary/20 group-data-completed/timeline-item:border-primary absolute size-4 rounded-full border-2 group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:left-0 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:top-0 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2",
className
)}
aria-hidden="true"
{...props}
>
{children}
</div>
)
}
// TimelineItem
interface TimelineItemProps extends React.HTMLAttributes<HTMLDivElement> {
step: number
}
function TimelineItem({ step, className, ...props }: TimelineItemProps) {
const { activeStep } = useTimeline()
return (
<div
data-slot="timeline-item"
className={cn(
"group/timeline-item has-[+[data-completed]]:[&_[data-slot=timeline-separator]]:bg-primary relative flex flex-1 flex-col gap-0.5 group-data-[orientation=horizontal]/timeline:mt-8 group-data-[orientation=horizontal]/timeline:not-last:pe-8 group-data-[orientation=vertical]/timeline:ms-8 group-data-[orientation=vertical]/timeline:not-last:pb-12",
className
)}
data-completed={step <= activeStep || undefined}
{...props}
/>
)
}
// TimelineSeparator
function TimelineSeparator({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="timeline-separator"
className={cn(
"bg-primary/10 absolute self-start group-last/timeline-item:hidden group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:h-0.5 group-data-[orientation=horizontal]/timeline:w-[calc(100%-1rem-0.25rem)] group-data-[orientation=horizontal]/timeline:translate-x-4.5 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-0.25rem)] group-data-[orientation=vertical]/timeline:w-0.5 group-data-[orientation=vertical]/timeline:-translate-x-1/2 group-data-[orientation=vertical]/timeline:translate-y-4.5",
className
)}
aria-hidden="true"
{...props}
/>
)
}
// TimelineTitle
function TimelineTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3
data-slot="timeline-title"
className={cn("text-sm font-medium", className)}
{...props}
/>
)
}
export {
Timeline,
TimelineContent,
TimelineDate,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
TimelineTitle,
}

21
src/hooks/use-ip-data.ts Normal file
View file

@ -0,0 +1,21 @@
import { useEffect, useState } from "react";
export function useIpData() {
const [ipData, setIpData] = useState<string>("");
useEffect(() => {
const fetchIpData = async () => {
try {
const response = await fetch("https://api.imnya.ng/ip");
const data = await response.text();
setIpData(data);
} catch (error) {
console.error("Failed to fetch IP data:", error);
}
};
fetchIpData();
}, []);
return ipData;
}

View file

@ -0,0 +1,21 @@
import { useEffect, useState } from "react";
export function useWakaTimeData() {
const [wakaTimeData, setWakaTimeData] = useState<any>(null);
useEffect(() => {
const fetchWakaTimeData = async () => {
try {
const response = await fetch("https://api.imnya.ng/wakatime");
const data = await response.json();
setWakaTimeData(data);
} catch (error) {
console.error("Failed to fetch WakaTime data:", error);
}
};
fetchWakaTimeData();
}, []);
return wakaTimeData;
}

146
src/lib/events.ts Normal file
View file

@ -0,0 +1,146 @@
export const events = [
{
description: "화이트햇스쿨 3기 이수",
category: "Education",
date: "2025-09-24",
link: "https://whitehatschool.kr/home/kor/main.do",
},
{
date: "2025-09-13",
description: "선린인터넷고 소프트웨어 나눔축제 AnA 이수",
category: "Education",
link: "https://ssf.sunrin.io/",
},
{
date: "2025-07-26",
description: "선린인터넷고 여름방학 중학생 특별교육 우수 이수 (프로그래밍)",
category: "Education",
link: "https://sunrint.sen.hs.kr/",
},
{
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: "선린인터넷고 겨울방학 중학생 특별교육 이수 (IT경영학과)",
category: "Education",
link: "https://sunrint.sen.hs.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",
},
];