From 44fdfbcd121efced4af60940c592aa9c21c34031 Mon Sep 17 00:00:00 2001 From: Aimaaaan Date: Sat, 18 Apr 2026 14:45:09 +0530 Subject: [PATCH] initial frontend design --- Skills.md | 171 ++++++++++++++ app/globals.css | 213 +++++++++-------- app/layout.tsx | 25 +- app/page.tsx | 280 +++++++++++++++++++++-- bun.lock | 27 +-- components/layout/Footer.tsx | 79 +++++++ components/layout/Navbar.tsx | 53 +++++ components/layout/PageShell.tsx | 22 ++ components/sections/TerminalParallax.tsx | 275 ++++++++++++++++++++++ components/ui/ArchiveBadge.tsx | 26 +++ components/ui/ArchiveTag.tsx | 19 ++ components/ui/DotGridBackground.tsx | 13 ++ components/ui/PolaroidCard.tsx | 48 ++++ components/ui/RepoRow.tsx | 53 +++++ components/ui/SectionHeader.tsx | 27 +++ components/ui/StampLabel.tsx | 29 +++ components/ui/TerminalButton.tsx | 60 +++++ constants/index.ts | 34 +++ constants/terminalData.ts | 81 +++++++ package.json | 1 + types/index.ts | 30 +++ 21 files changed, 1424 insertions(+), 142 deletions(-) create mode 100644 Skills.md create mode 100644 components/layout/Footer.tsx create mode 100644 components/layout/Navbar.tsx create mode 100644 components/layout/PageShell.tsx create mode 100644 components/sections/TerminalParallax.tsx create mode 100644 components/ui/ArchiveBadge.tsx create mode 100644 components/ui/ArchiveTag.tsx create mode 100644 components/ui/DotGridBackground.tsx create mode 100644 components/ui/PolaroidCard.tsx create mode 100644 components/ui/RepoRow.tsx create mode 100644 components/ui/SectionHeader.tsx create mode 100644 components/ui/StampLabel.tsx create mode 100644 components/ui/TerminalButton.tsx create mode 100644 constants/index.ts create mode 100644 constants/terminalData.ts create mode 100644 types/index.ts diff --git a/Skills.md b/Skills.md new file mode 100644 index 0000000..c7c2b11 --- /dev/null +++ b/Skills.md @@ -0,0 +1,171 @@ +# SKILLS.md — Project Orbit / DevNation Frontend + +> **Project context:** This is the frontend of Project Orbit, a developer club platform for DevNation. +> Built with: **Next.js 16 (App Router) · Tailwind CSS · Framer Motion · Lucide React** +> Minimum Node.js version: **18.18.0+** (required by Next.js 16) + +--- + +## ⚠️ CRITICAL: DO NOT TOUCH + +The following are **strictly off-limits** for all contributors: + +### Backend & API +- **Do not modify, refactor, or delete any backend files, routes, or server-side logic.** +- **Do not alter any API contracts** — endpoint paths, request shapes, response shapes, HTTP methods, and status codes must remain exactly as defined. +- **Do not add, remove, or rename API parameters** without explicit approval from the backend owner. +- **Do not introduce new API calls** to endpoints that haven't been defined by the backend team. +- If you believe a backend change is necessary, open a discussion/issue and wait for backend owner sign-off before touching anything. + +> Violating any of the above will break the integration between frontend and backend and will require a rollback. + +--- + +## ✅ Frontend Contribution Guidelines + +### Write Highly Customizable Code + +All frontend code contributed to this project must be **modular, prop-driven, and easy to extend**. Follow these rules: + +#### 1. Components must be prop-driven +Every component should accept props for any value that could reasonably vary — text, colors, sizes, icons, callbacks, visibility toggles. + +```tsx +// ❌ Bad — hardcoded values +export function StatCard() { + return
42 commits
; +} + +// ✅ Good — fully customizable +interface StatCardProps { + label: string; + value: string | number; + accentColor?: string; + icon?: React.ReactNode; +} + +export function StatCard({ label, value, accentColor = "text-green-400", icon }: StatCardProps) { + return ( +
+ {icon && {icon}} + {value} {label} +
+ ); +} +``` + +#### 2. Use TypeScript interfaces for all props +Define a clear `interface` or `type` for every component's props. No implicit `any` types. + +#### 3. Provide sensible defaults +Use default parameter values so components work out of the box but remain overridable. + +#### 4. Avoid magic numbers and hardcoded strings +Use constants, config objects, or Tailwind theme tokens instead: + +```tsx +// ❌ Bad +
+ +// ✅ Good — defined in a shared config or Tailwind class +
+``` + +#### 5. Keep components single-responsibility +One component = one job. Split large components into smaller composable pieces. + +#### 6. Export everything that could be reused +If it might be useful to another page or contributor, export it from the component file. + +--- + +## 🎨 Aesthetic Rules (CS_ARCHIVE_V1.0) + +All UI must conform to the established brutalist-terminal aesthetic. Do not deviate. + +| Token | Value | +|---|---| +| Background | Near-black (`#0a0a0a` or equivalent) | +| Primary font | `Bebas Neue` (display headers) | +| UI font | `IBM Plex Mono` (labels, body, data) | +| Accent | Status green only (`#22c55e` or Tailwind `green-500`) | +| Palette | Strictly monochrome + single accent | +| Borders | Dashed or dotted, zero border-radius | +| Cards | Polaroid-style with physical stamp overlays | + +> Do not introduce new colors, fonts, rounded corners, or gradients without a design review. + +--- + +## 📁 File & Folder Conventions + +``` +/components → Reusable UI components (must follow prop-driven rules above) +/app → Next.js 16 App Router pages, layouts, and Route Handlers +/lib → Utility functions and shared helpers +/types → Shared TypeScript types and interfaces +/constants → App-wide constants (no magic numbers elsewhere) +/hooks → Custom React hooks +/proxy.ts → Next.js 16 network boundary config (replaces middleware for proxy rules) +``` + +- Name component files in `PascalCase`: `StatCard.tsx`, `MemberRow.tsx` +- Name utility/hook files in `camelCase`: `useActivityFeed.ts`, `formatDate.ts` +- Co-locate component-specific styles/logic with the component file + +--- + +## ⚡ Next.js 16 — Key Rules for Contributors + +Next.js 16 introduces breaking changes and new patterns. All contributors **must** follow these: + +### Caching — opt-in only +Next.js 16 removed implicit caching. **All routes are dynamic by default.** To cache, you must explicitly opt in using the new `Cache Components` API and `use cache` directive. + +```tsx +// ✅ Next.js 16 — explicit opt-in caching +"use cache"; + +export default async function MemberStats() { + const data = await fetchStats(); + return ; +} +``` + +Do **not** rely on the old `fetch` auto-caching behavior from Next.js 14 — it no longer works. + +### Middleware → proxy.ts +Next.js 16 replaces Middleware with `proxy.ts` for defining network boundary rules. Do not create or edit `middleware.ts` — use `proxy.ts` at the project root instead. + +### React Compiler is stable +The React Compiler (automatic memoization) is stable in Next.js 16. **Do not manually add `useMemo` or `useCallback`** unless you have a specific reason — the compiler handles it. Unnecessary manual memoization will conflict. + +### React 19.2 features available +The App Router now ships with React 19.2. You can use: +- `useEffectEvent` — extract non-reactive logic from Effects +- `Activity` — hide UI with `display: none` while preserving state +- View Transitions API — animate between navigations + +### Turbopack is the default bundler +Turbopack is now the default for both `next dev` and `next build`. Do not add Webpack plugins or custom Webpack config — raise it as a discussion first. + +### Upgrading (if not already on v16) +```bash +npx @next/codemod@latest upgrade latest +# or manually: +npm install next@latest react@latest react-dom@latest eslint-config-next@latest +``` + +--- + +## 🔄 Workflow + +1. **Pull latest** before starting any work. +2. **Work on a feature branch** — never commit directly to `main`. +3. **Do not merge your own PRs** — at least one other contributor must review. +4. **Test your component in isolation** before integrating it into a page. +5. If you're unsure whether something touches the backend boundary → **ask first**. + +--- + +*Last updated by: Aimaan — Project Orbit frontend lead · Updated for Next.js 16* diff --git a/app/globals.css b/app/globals.css index 99eb7a9..90a37aa 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,99 +1,130 @@ @import "tailwindcss"; -@theme { - --font-mono: "Geist Mono", "Courier New", monospace; - --color-tactical-black: #000000; - --color-tactical-white: #ffffff; +/* ── ORBIT Design System: CS_ARCHIVE_V1.0 ── */ + +@theme inline { + /* Colors */ + --color-bg: #0a0a0a; + --color-surface: #111111; + --color-surface-2: #1a1a1a; + --color-surface-3: #2a2a2a; + --color-accent: #22c55e; + --color-text: #ededed; + --color-text-muted: #888888; + --color-border: #333333; + --color-white: #ffffff; + + /* Fonts */ + --font-heading: var(--font-bebas-neue); + --font-mono: var(--font-ibm-plex-mono); } -:root { - --background: #000000; - --foreground: #ffffff; +/* ── Base Reset ── */ + +* { + border-radius: 0 !important; } body { - background: var(--background); - color: var(--foreground); - font-family: var(--font-mono); - overflow-x: hidden; -} - -/* Hide number input spin buttons */ -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -input[type=number] { - -moz-appearance: textfield; -} - -/* Tactical Archive Effects */ - -.scanlines { - position: relative; - overflow: hidden; -} - -.scanlines::after { - content: " "; - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: linear-gradient( - rgba(18, 16, 16, 0) 50%, - rgba(0, 0, 0, 0.25) 50% - ), - linear-gradient( - 90deg, - rgba(255, 0, 0, 0.06), - rgba(0, 255, 0, 0.02), - rgba(0, 255, 0, 0.06) - ); - z-index: 2; - background-size: 100% 2px, 3px 100%; - pointer-events: none; -} - -.noise { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: url("https://grainy-gradients.vercel.app/noise.svg"); - opacity: 0.05; - pointer-events: none; - z-index: 100; -} - -.tactical-border { - border: 1px solid rgba(255, 255, 255, 0.2); - position: relative; -} - -.tactical-border::before { - content: ""; - position: absolute; - top: -1px; - left: -1px; - width: 10px; - height: 10px; - border-top: 2px solid white; - border-left: 2px solid white; -} - -.tactical-border::after { - content: ""; - position: absolute; - bottom: -1px; - right: -1px; - width: 10px; - height: 10px; - border-bottom: 2px solid white; - border-right: 2px solid white; + background-color: var(--color-bg); + color: var(--color-text); + font-family: var(--font-mono), ui-monospace, monospace; +} + +/* ── Dot Grid Background Pattern ── */ + +.dot-grid-bg { + background-image: radial-gradient(circle, #333333 1px, transparent 1px); + background-size: 24px 24px; +} + +/* ── Stamp Variants ── */ + +.stamp-verified { + color: #22c55e; + border-color: #22c55e; +} + +.stamp-confidential { + color: #f97316; + border-color: #f97316; +} + +.stamp-urgent { + color: #ef4444; + border-color: #ef4444; +} + +.stamp-restricted { + color: #eab308; + border-color: #eab308; +} + +.stamp-archived { + color: #888888; + border-color: #888888; +} + +/* ── Badge Variants ── */ + +.badge-stable { + background-color: #22c55e; + color: #0a0a0a; +} + +.badge-experimental { + background-color: #f97316; + color: #0a0a0a; +} + +.badge-archived { + background-color: #888888; + color: #0a0a0a; +} + +.badge-restricted { + background-color: #eab308; + color: #0a0a0a; +} + +.badge-urgent { + background-color: #ef4444; + color: #ffffff; +} + +/* ── Scrollbar Styling ── */ + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #0a0a0a; +} + +::-webkit-scrollbar-thumb { + background: #333333; } + +::-webkit-scrollbar-thumb:hover { + background: #555555; +} + +/* ── Selection ── */ + +::selection { + background-color: #22c55e; + color: #0a0a0a; +} + +/* ── Terminal Cursor Blink ── */ + +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +.cursor-blink { + animation: blink 1s step-end infinite; +} + diff --git a/app/layout.tsx b/app/layout.tsx index 5068e0a..e25a11a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,27 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Bebas_Neue, IBM_Plex_Mono } from "next/font/google"; +import { DotGridBackground } from "@/components/ui/DotGridBackground"; import "./globals.css"; import { Providers } from "./providers"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const bebasNeue = Bebas_Neue({ + weight: "400", + variable: "--font-bebas-neue", subsets: ["latin"], + display: "swap", }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const ibmPlexMono = IBM_Plex_Mono({ + weight: ["400", "500", "600", "700"], + variable: "--font-ibm-plex-mono", subsets: ["latin"], + display: "swap", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "ORBIT — DevNation", + description: + "Terminal interface for the DevNation collective. Access encrypted project logs, member databases, and upcoming tactical events.", }; export default function RootLayout({ @@ -26,9 +32,10 @@ export default function RootLayout({ return ( - + + {children} diff --git a/app/page.tsx b/app/page.tsx index c8695bb..7af63eb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,30 +1,262 @@ "use client"; -import { signOut, useSession } from "next-auth/react"; +import { motion } from "framer-motion"; +import { PageShell } from "@/components/layout/PageShell"; +import { ArchiveTag } from "@/components/ui/ArchiveTag"; +import { TerminalButton } from "@/components/ui/TerminalButton"; +import { SectionHeader } from "@/components/ui/SectionHeader"; +import { PolaroidCard } from "@/components/ui/PolaroidCard"; +import { StampLabel } from "@/components/ui/StampLabel"; +import { ArchiveBadge } from "@/components/ui/ArchiveBadge"; +import { RepoRow } from "@/components/ui/RepoRow"; -export default function Home() { - const { data: session } = useSession(); +import { TerminalParallax } from "@/components/sections/TerminalParallax"; + +const fadeUp = { + hidden: { opacity: 0, y: 20 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { delay: i * 0.1, duration: 0.5, ease: "easeOut" as const }, + }), +}; + +const sampleRepos = [ + { + name: "TACTICAL-GRID-OS", + description: "A lightweight dashboard for managing archive datasets", + stars: 194, + forks: 17, + status: "stable" as const, + }, + { + name: "MEMBER-REGISTRY-V3", + description: "New member onboarding system with encrypted log-ins", + stars: 63, + forks: 5, + status: "experimental" as const, + }, + { + name: "CLI-TOOLKIT-CORE", + description: "Custom terminal commands for common archival tasks", + stars: 87, + forks: 15, + status: "stable" as const, + }, +]; +export default function Home() { return ( -
-

Dashboard

- -
-

Welcome, {session?.user?.name || "User"}!

-

Your USN is: {session?.user?.usn || "Not set"}

-

Role: {session?.user?.role || "Unknown"}

-
- -

- If you manually deleted your user record in Neon, your browser still holds the JWT session cookie! Click below to clear it and start fresh. -

- - -
+ + {/* ── Hero Section ── */} +
+ + + + + + + CLUB HUB +
+ + (ARCHIVE) + +
+ + + Terminal interface for the DevNation collective. Access + encrypted project logs, member databases, and upcoming tactical + events. + + + + + + +
+ + {/* Metadata line */} + + DATE: 2024.11.02 · STATUS: VERIFIED · LOC: SECTOR_7 · API_ONLINE: 100% + · LABS_AVAILABLE + +
+ + {/* ── Parallax Terminal Section ── */} + + + {/* ── Project Showcase Section ── */} +
+ + +
+ +
+ + + +
+
+ + + + + + +
+ + + +
+
+ + + + +
+
+ + {/* ── Shared Repositories Section ── */} +
+ + +
+ {sampleRepos.map((repo) => ( + + + + ))} +
+
+ + {/* ── Join CTA Section ── */} +
+ {/* Background repeating text */} + + + + +

+ JOIN THE COLLECTIVE +

+

+ CLICK TO INITIALIZE MEMBERSHIP +

+
+ +
+
+
+
); -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock index 656867a..3960f82 100644 --- a/bun.lock +++ b/bun.lock @@ -6,10 +6,9 @@ "name": "dn-orbit", "dependencies": { "@auth/prisma-adapter": "^2.11.1", - "@neondatabase/serverless": "^1.1.0", - "@prisma/adapter-neon": "^7.7.0", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.5.0", + "framer-motion": "^12.38.0", "lucide-react": "^1.8.0", "next": "16.2.1", "next-auth": "^5.0.0-beta.30", @@ -17,7 +16,6 @@ "prisma": "^7.5.0", "react": "19.2.4", "react-dom": "19.2.4", - "ws": "^8.20.0", }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -25,7 +23,6 @@ "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", - "@types/ws": "^8.18.1", "eslint": "^9", "eslint-config-next": "16.2.1", "tailwindcss": "^4", @@ -188,8 +185,6 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@neondatabase/serverless": ["@neondatabase/serverless@1.1.0", "", {}, "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q=="], - "@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="], @@ -220,8 +215,6 @@ "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], - "@prisma/adapter-neon": ["@prisma/adapter-neon@7.7.0", "", { "dependencies": { "@neondatabase/serverless": ">0.6.0 <2", "@prisma/driver-adapter-utils": "7.7.0", "postgres-array": "3.0.4" } }, "sha512-nnQBGUqDBuLDbClSNyZ7T8jv3+wXx9ncA8zaZhVZtP+utq7SdT4azLotTfZjP4hJaP1NXIvCKjp0RfzMq5dyew=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="], "@prisma/client": ["@prisma/client@7.5.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.5.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w=="], @@ -230,11 +223,11 @@ "@prisma/config": ["@prisma/config@7.5.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ=="], - "@prisma/debug": ["@prisma/debug@7.7.0", "", {}, "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA=="], + "@prisma/debug": ["@prisma/debug@7.6.0", "", {}, "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA=="], "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0" } }, "sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], "@prisma/engines": ["@prisma/engines@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "@prisma/fetch-engine": "7.5.0", "@prisma/get-platform": "7.5.0" } }, "sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw=="], @@ -300,8 +293,6 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/type-utils": "8.57.1", "@typescript-eslint/utils": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw=="], @@ -572,6 +563,8 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -784,6 +777,10 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + + "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], @@ -1056,8 +1053,6 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1076,8 +1071,6 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@prisma/adapter-pg/@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], - "@prisma/engines/@prisma/debug": ["@prisma/debug@7.5.0", "", {}, "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw=="], @@ -1134,8 +1127,6 @@ "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@prisma/adapter-pg/@prisma/driver-adapter-utils/@prisma/debug": ["@prisma/debug@7.6.0", "", {}, "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx new file mode 100644 index 0000000..9a8cba9 --- /dev/null +++ b/components/layout/Footer.tsx @@ -0,0 +1,79 @@ +import Link from "next/link"; +import { CodeXml, Globe } from "lucide-react"; +import { SITE_NAME, NAV_LINKS, FOOTER_LINKS } from "@/constants"; + +/** + * Footer — Two-row footer with project branding, links, and social icons. + * + * Top row: project name (left) · nav links (center) · copyright (right) + * Bottom row: policy links (left/center) · social icons (right) + * Server Component — no interactivity needed. + */ +export function Footer() { + const year = new Date().getFullYear(); + + return ( +
+ {/* Top row */} +
+ + {SITE_NAME} + + +
+ {NAV_LINKS.map((link) => ( + + {link.label} + + ))} +
+ + + ©{year}_TACTICAL_ARCHIVE_CS_CLUB + +
+ + {/* Divider */} +
+ + {/* Bottom row */} +
+
+ {FOOTER_LINKS.map((label) => ( + + {label} + + ))} +
+ + +
+
+ ); +} diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx new file mode 100644 index 0000000..9183e2e --- /dev/null +++ b/components/layout/Navbar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { NAV_LINKS, SITE_NAME } from "@/constants"; + +/** + * Navbar — Top navigation bar for the ORBIT platform. + * + * Layout: CS_ARCHIVE_V1.0 branding (left) · nav links (center) · LOGIN button (right) + * Active link gets a dashed underline. Entire bar has a dashed bottom border. + * Client Component because we need usePathname() to highlight the active link. + */ +export function Navbar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/components/layout/PageShell.tsx b/components/layout/PageShell.tsx new file mode 100644 index 0000000..b47604c --- /dev/null +++ b/components/layout/PageShell.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; +import { Navbar } from "@/components/layout/Navbar"; +import { Footer } from "@/components/layout/Footer"; + +interface PageShellProps { + children: ReactNode; +} + +/** + * PageShell — Wraps Navbar + page content + Footer. + * Provides consistent full-height layout structure. + * Main content area has z-10 to sit above the DotGridBackground. + */ +export function PageShell({ children }: PageShellProps) { + return ( + <> + +
{children}
+