setting up dark/white mode. fixed some other small things
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<main className="min-h-screen bg-white text-zinc-900 dark:bg-black dark:text-zinc-50">
|
||||||
@@ -319,7 +319,7 @@ export default function BuildSummaryPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black text-zinc-50">
|
<main className="min-h-screen bg-white text-zinc-900 dark:bg-black dark:text-zinc-50">
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="mb-6">
|
<header className="mb-6">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { CATEGORIES } from "@/data/gunbuilderParts";
|
|||||||
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
||||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||||
import { PART_ROLE_TO_CATEGORY } from "@/lib/catalogMappings";
|
import { PART_ROLE_TO_CATEGORY } from "@/lib/catalogMappings";
|
||||||
import { Pencil, Link2, Square, CheckSquare } from "lucide-react";
|
import { Pencil, X, Link2, Square, CheckSquare } from "lucide-react";
|
||||||
|
|
||||||
// ✅ Centralized overlap rules + helpers
|
// ✅ Centralized overlap rules + helpers
|
||||||
import OverlapChip from "@/components/builder/OverlapChip";
|
import OverlapChip from "@/components/builder/OverlapChip";
|
||||||
@@ -164,7 +164,9 @@ export default function GunbuilderPage() {
|
|||||||
const selectedParts: Part[] = useMemo(() => {
|
const selectedParts: Part[] = useMemo(() => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const resolved = Object.values(build)
|
const resolved = Object.values(build)
|
||||||
.map((partId) => (partId ? parts.find((p) => p.id === partId) : undefined))
|
.map((partId) =>
|
||||||
|
partId ? parts.find((p) => p.id === partId) : undefined
|
||||||
|
)
|
||||||
.filter(Boolean) as Part[];
|
.filter(Boolean) as Part[];
|
||||||
|
|
||||||
return resolved.filter((p) => {
|
return resolved.filter((p) => {
|
||||||
@@ -368,7 +370,7 @@ export default function GunbuilderPage() {
|
|||||||
brand: p.brand,
|
brand: p.brand,
|
||||||
price: p.price ?? 0,
|
price: p.price ?? 0,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
((p as any).imageUrl ?? (p as any).mainImageUrl) ?? undefined,
|
(p as any).imageUrl ?? (p as any).mainImageUrl ?? undefined,
|
||||||
affiliateUrl: buyUrl,
|
affiliateUrl: buyUrl,
|
||||||
url: buyUrl,
|
url: buyUrl,
|
||||||
notes: undefined,
|
notes: undefined,
|
||||||
@@ -508,7 +510,9 @@ export default function GunbuilderPage() {
|
|||||||
const payload = JSON.stringify(build);
|
const payload = JSON.stringify(build);
|
||||||
const encoded = window.btoa(payload);
|
const encoded = window.btoa(payload);
|
||||||
const origin = window.location?.origin ?? "";
|
const origin = window.location?.origin ?? "";
|
||||||
const url = `${origin}/builder/build?build=${encodeURIComponent(encoded)}`;
|
const url = `${origin}/builder/build?build=${encodeURIComponent(
|
||||||
|
encoded
|
||||||
|
)}`;
|
||||||
setShareUrl(url);
|
setShareUrl(url);
|
||||||
} catch {
|
} catch {
|
||||||
setShareUrl("");
|
setShareUrl("");
|
||||||
@@ -606,7 +610,7 @@ export default function GunbuilderPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black text-zinc-50">
|
<main className="min-h-screen bg-white text-zinc-900 dark:bg-black dark:text-zinc-50">
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="mb-6">
|
<header className="mb-6">
|
||||||
@@ -807,9 +811,9 @@ export default function GunbuilderPage() {
|
|||||||
{/* Layout */}
|
{/* Layout */}
|
||||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
||||||
<p className="mb-4 text-xs text-zinc-500">
|
<p className="mb-4 text-xs text-zinc-500">
|
||||||
Work top-down through the major sections. Each row shows your current
|
Work top-down through the major sections. Each row shows your
|
||||||
pick for that part type, with price and a direct buy link—or a quick
|
current pick for that part type, with price and a direct buy link—or
|
||||||
way to choose a part if you haven't picked one yet.
|
a quick way to choose a part if you haven't picked one yet.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -955,19 +959,21 @@ export default function GunbuilderPage() {
|
|||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{selectedPart && selectedPart.url ? (
|
{selectedPart && selectedPart.url ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<button
|
||||||
href={{
|
type="button"
|
||||||
pathname: `/parts/p/${platform}/${category.id}`,
|
onClick={() => {
|
||||||
query: platform
|
setBuild((prev) => {
|
||||||
? { platform }
|
const next = { ...prev };
|
||||||
: {},
|
delete next[category.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="hidden md:inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
className="inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 hover:bg-red-500/15 hover:border-red-400 transition-colors"
|
||||||
aria-label="Change part"
|
aria-label="Remove part"
|
||||||
title="Change part"
|
title="Remove part"
|
||||||
>
|
>
|
||||||
<Pencil className="h-3.5 w-3.5 text-zinc-300" />
|
<X className="h-3.5 w-3.5 text-zinc-400 hover:text-red-400" />
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={selectedPart.url}
|
href={selectedPart.url}
|
||||||
@@ -984,9 +990,7 @@ export default function GunbuilderPage() {
|
|||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
||||||
pathname: `/parts/p/${platform}/${category.id}`,
|
pathname: `/parts/p/${platform}/${category.id}`,
|
||||||
query: platform
|
query: platform ? { platform } : {},
|
||||||
? { platform }
|
|
||||||
: {},
|
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-1.5 text-xs font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-1.5 text-xs font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
// app/(builder)/layout.tsx
|
// app/(builder)/layout.tsx
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
|
||||||
import { BuilderNav } from "@/components/BuilderNav";
|
import { BuilderNav } from "@/components/BuilderNav";
|
||||||
import { TopNav } from "@/components/TopNav";
|
import { TopNav } from "@/components/TopNav";
|
||||||
|
|
||||||
export default function BuilderLayout({ children }: { children: ReactNode }) {
|
export default function BuilderLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-950 text-zinc-50">
|
<div className="min-h-screen bg-white text-zinc-900 dark:bg-neutral-950 dark:text-zinc-50">
|
||||||
<AuthProvider>
|
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<BuilderNav />
|
<BuilderNav />
|
||||||
<main className="min-h-screen">{children}</main>
|
<main className="min-h-screen">{children}</main>
|
||||||
</AuthProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+17
-4
@@ -2,7 +2,6 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
|
|
||||||
import { Banner } from "@/components/Banner";
|
import { Banner } from "@/components/Banner";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -13,16 +12,30 @@ export const metadata = {
|
|||||||
description: "Build rifles smarter, not harder.",
|
description: "Build rifles smarter, not harder.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const themeScript = `
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
const storedTheme = localStorage.getItem("theme");
|
||||||
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
const theme = storedTheme || (prefersDark ? "dark" : "light");
|
||||||
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className="bg-black text-zinc-50">
|
<head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body className="bg-white text-zinc-950 dark:bg-black dark:text-zinc-50">
|
||||||
|
{" "}
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Banner />
|
<Banner />
|
||||||
{children}
|
{children}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+89
-13
@@ -1,15 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuilderNav.tsx
|
||||||
|
* -----------------------------------------------------------------------------
|
||||||
|
* Top navigation for the Builder experience.
|
||||||
|
*
|
||||||
|
* MAJOR LAYOUT IDEA:
|
||||||
|
* - Keep all primary nav items on the LEFT
|
||||||
|
* - Put *everything else* (search, viewing state, theme toggle later, user menu later)
|
||||||
|
* into a RIGHT "actions cluster" using a single `ml-auto` wrapper.
|
||||||
|
*
|
||||||
|
* WHY:
|
||||||
|
* - Prevents multiple `ml-auto` elements fighting each other
|
||||||
|
* - Makes the right side easy to extend without breaking alignment
|
||||||
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||||
import type { Category } from "@/types/gunbuilder";
|
import type { Category } from "@/types/gunbuilder";
|
||||||
|
|
||||||
type BuilderNavProps = {
|
type BuilderNavProps = {
|
||||||
|
// Optional override if a parent wants to force the active category
|
||||||
activeCategoryId?: string | null;
|
activeCategoryId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to group categories for the dropdowns
|
/**
|
||||||
|
* Helper: group categories for dropdowns.
|
||||||
|
* This assumes your Category has a `group` field like "lower" | "upper" | "accessories".
|
||||||
|
*/
|
||||||
function groupCategories(group: "lower" | "upper" | "accessories"): Category[] {
|
function groupCategories(group: "lower" | "upper" | "accessories"): Category[] {
|
||||||
return CATEGORIES.filter((c) => c.group === group);
|
return CATEGORIES.filter((c) => c.group === group);
|
||||||
}
|
}
|
||||||
@@ -17,32 +36,43 @@ function groupCategories(group: "lower" | "upper" | "accessories"): Category[] {
|
|||||||
export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// ---- Data / derived state --------------------------------------------------
|
||||||
const lower = groupCategories("lower");
|
const lower = groupCategories("lower");
|
||||||
const upper = groupCategories("upper");
|
const upper = groupCategories("upper");
|
||||||
const accessories = groupCategories("accessories");
|
const accessories = groupCategories("accessories");
|
||||||
|
|
||||||
|
// Determine which category is "active" so we can highlight dropdown items
|
||||||
|
// and show the "Viewing" indicator on the right.
|
||||||
const currentCategory =
|
const currentCategory =
|
||||||
activeCategoryId ?? searchParams.get("category") ?? undefined;
|
activeCategoryId ?? searchParams.get("category") ?? undefined;
|
||||||
|
|
||||||
|
// Base route for parts browsing (used for dropdown links + "Clear")
|
||||||
const baseHref = "/parts";
|
const baseHref = "/parts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdown renderer (hover-based).
|
||||||
|
* - Uses Tailwind `group` + `group-hover` to show/hide the menu.
|
||||||
|
* - Highlights the active item.
|
||||||
|
*/
|
||||||
const renderDropdown = (label: string, items: Category[]) => {
|
const renderDropdown = (label: string, items: Category[]) => {
|
||||||
if (!items.length) return null;
|
if (!items.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
|
{/* Dropdown trigger */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`inline-flex items-center gap-1 font-medium tracking-wide transition-colors uppercase text-[11px] ${
|
className={`inline-flex items-center gap-1 font-medium tracking-wide uppercase text-[11px] transition-colors ${
|
||||||
items.some(cat => cat.id === currentCategory)
|
items.some((cat) => cat.id === currentCategory)
|
||||||
? "text-white"
|
? "text-white"
|
||||||
: "text-neutral-200 hover:text-white"
|
: "text-neutral-300 hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
<span className="text-[9px]">▾</span>
|
<span className="text-[9px]">▾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown menu */}
|
||||||
<div className="invisible opacity-0 group-hover:visible group-hover:opacity-100 transition-all absolute left-0 mt-2 rounded-md border border-neutral-800 bg-black/95 shadow-xl z-40 min-w-[220px]">
|
<div className="invisible opacity-0 group-hover:visible group-hover:opacity-100 transition-all absolute left-0 mt-2 rounded-md border border-neutral-800 bg-black/95 shadow-xl z-40 min-w-[220px]">
|
||||||
<ul className="py-2 text-sm">
|
<ul className="py-2 text-sm">
|
||||||
{items.map((cat) => {
|
{items.map((cat) => {
|
||||||
@@ -51,12 +81,11 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
|||||||
<li key={cat.id}>
|
<li key={cat.id}>
|
||||||
<Link
|
<Link
|
||||||
href={`${baseHref}/${cat.id}`}
|
href={`${baseHref}/${cat.id}`}
|
||||||
className={[
|
className={`block w-full px-3 py-1.5 transition-colors ${
|
||||||
"block w-full px-3 py-1.5 text-left text-sm transition-colors",
|
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-800 text-white"
|
? "bg-neutral-800 text-white"
|
||||||
: "text-neutral-300 hover:bg-neutral-800 hover:text-white",
|
: "text-neutral-300 hover:bg-neutral-800 hover:text-white"
|
||||||
].join(" ")}
|
}`}
|
||||||
>
|
>
|
||||||
{cat.name}
|
{cat.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -70,11 +99,23 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto w-full"> {/* ← aligns with topnav */}
|
/**
|
||||||
|
* Outer container matches TopNav width:
|
||||||
|
* - max-w-6xl mx-auto w-full ensures consistent alignment across app
|
||||||
|
*/
|
||||||
|
<div className="max-w-6xl mx-auto w-full">
|
||||||
|
{/**
|
||||||
|
* FLEX STRATEGY:
|
||||||
|
* - Everything before the RIGHT cluster is "left side"
|
||||||
|
* - Then a single `ml-auto` cluster pushes everything else to the right
|
||||||
|
*/}
|
||||||
<nav className="flex items-center gap-6 border-b border-neutral-900 px-6 py-3 text-sm backdrop-blur-sm">
|
<nav className="flex items-center gap-6 border-b border-neutral-900 px-6 py-3 text-sm backdrop-blur-sm">
|
||||||
{/* Primary builder link */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* LEFT SIDE: primary nav */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={'/builder'}
|
href="/builder"
|
||||||
className="font-semibold text-neutral-100 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
className="font-semibold text-neutral-100 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
||||||
>
|
>
|
||||||
Builder
|
Builder
|
||||||
@@ -91,14 +132,44 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
|||||||
Builds
|
Builds
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Right → Current filter */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* RIGHT SIDE: actions + state */}
|
||||||
|
{/* IMPORTANT: this is the ONLY `ml-auto` in the nav to avoid conflicts */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
<div className="ml-auto flex items-center gap-3">
|
||||||
|
{/* Search icon (placeholder action) */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/60 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 hover:border-zinc-500 transition-colors"
|
||||||
|
aria-label="Search (coming soon)"
|
||||||
|
title="Search (coming soon)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="h-4 w-4"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="6" />
|
||||||
|
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* "Viewing" indicator (only shows when a category is active) */}
|
||||||
{currentCategory && (
|
{currentCategory && (
|
||||||
<div className="ml-auto flex items-center gap-2 text-neutral-400 text-xs">
|
<div className="flex items-center gap-2 text-neutral-400 text-xs">
|
||||||
<span className="uppercase tracking-wide text-[10px]">Viewing</span>
|
<span className="uppercase tracking-wide text-[10px]">Viewing</span>
|
||||||
|
|
||||||
<span className="font-medium text-neutral-100">
|
<span className="font-medium text-neutral-100">
|
||||||
{CATEGORIES.find((c) => c.id === currentCategory)?.name ??
|
{CATEGORIES.find((c) => c.id === currentCategory)?.name ??
|
||||||
currentCategory}
|
currentCategory}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Clears category filter -> go back to /parts */}
|
||||||
<Link
|
<Link
|
||||||
href={baseHref}
|
href={baseHref}
|
||||||
className="rounded border border-neutral-700 px-2 py-0.5 text-[10px] uppercase tracking-wide hover:bg-neutral-800 hover:text-white"
|
className="rounded border border-neutral-700 px-2 py-0.5 text-[10px] uppercase tracking-wide hover:bg-neutral-800 hover:text-white"
|
||||||
@@ -107,6 +178,11 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* FUTURE: drop in ThemeToggle, UserMenu, Notifications here */}
|
||||||
|
{/* <ThemeToggle /> */}
|
||||||
|
{/* <UserMenu /> */}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Sun, Moon } from "lucide-react";
|
||||||
|
|
||||||
|
type Theme = "light" | "dark";
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<Theme>("dark");
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = (localStorage.getItem("theme") as Theme | null) ?? "dark";
|
||||||
|
setTheme(stored);
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null; // prevents hydration mismatch
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
const next: Theme = theme === "dark" ? "light" : "dark";
|
||||||
|
setTheme(next);
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
document.documentElement.classList.toggle("dark", next === "dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
className="inline-flex items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-1.5 text-xs font-semibold text-zinc-200 hover:bg-zinc-800 transition-colors
|
||||||
|
dark:border-zinc-700 dark:bg-zinc-900/70 dark:text-zinc-200
|
||||||
|
border-zinc-300 bg-white text-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<>
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
Light
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
Dark
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
|
|
||||||
export function TopNav() {
|
export function TopNav() {
|
||||||
const { user, logout, loading } = useAuth();
|
const { user, logout, loading } = useAuth();
|
||||||
@@ -12,8 +13,7 @@ export function TopNav() {
|
|||||||
const isAuthenticated = !!user;
|
const isAuthenticated = !!user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b border-zinc-800 bg-black/95 backdrop-blur">
|
<header className="border-b border-zinc-200 bg-white/95 dark:border-zinc-800 dark:bg-black/95 backdrop-blur">
|
||||||
|
|
||||||
{/* Main nav row */}
|
{/* Main nav row */}
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
{/* Left: Brand / Home */}
|
{/* Left: Brand / Home */}
|
||||||
@@ -21,7 +21,6 @@ export function TopNav() {
|
|||||||
href="/"
|
href="/"
|
||||||
className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-400"
|
className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-400"
|
||||||
>
|
>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
src="/battl/battl-logo-mark-2.svg"
|
src="/battl/battl-logo-mark-2.svg"
|
||||||
alt="Battl Builders Logo"
|
alt="Battl Builders Logo"
|
||||||
@@ -34,7 +33,7 @@ export function TopNav() {
|
|||||||
{/* Right side actions */}
|
{/* Right side actions */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Search placeholder */}
|
{/* Search placeholder */}
|
||||||
<button
|
{/* <button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/60 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 hover:border-zinc-500 transition-colors"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/60 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 hover:border-zinc-500 transition-colors"
|
||||||
aria-label="Search (coming soon)"
|
aria-label="Search (coming soon)"
|
||||||
@@ -52,7 +51,7 @@ export function TopNav() {
|
|||||||
<circle cx="11" cy="11" r="6" />
|
<circle cx="11" cy="11" r="6" />
|
||||||
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
<line x1="16.5" y1="16.5" x2="21" y2="21" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button> */}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="text-xs text-zinc-500">Checking session…</span>
|
<span className="text-xs text-zinc-500">Checking session…</span>
|
||||||
@@ -83,6 +82,9 @@ export function TopNav() {
|
|||||||
>
|
>
|
||||||
Join Beta
|
Join Beta
|
||||||
</Link>
|
</Link>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PartsBrowseClient
|
* PartsBrowseClient
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
@@ -88,6 +90,7 @@ export default function PartsBrowseClient(props: {
|
|||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const effectivePlatform =
|
const effectivePlatform =
|
||||||
props.platform ?? searchParams.get("platform") ?? "AR-15";
|
props.platform ?? searchParams.get("platform") ?? "AR-15";
|
||||||
@@ -169,6 +172,11 @@ export default function PartsBrowseClient(props: {
|
|||||||
inStockOnly,
|
inStockOnly,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset scroll position whenever we land on or change parts routes
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
const availableBrands = useMemo(
|
const availableBrands = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Array.from(new Set(parts.map((p) => p.brand)))
|
Array.from(new Set(parts.map((p) => p.brand)))
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default function PartsGrid(props: {
|
|||||||
parts: UiPart[];
|
parts: UiPart[];
|
||||||
buildDetailHref: (p: UiPart) => string;
|
buildDetailHref: (p: UiPart) => string;
|
||||||
|
|
||||||
// NEW: optional Add-to-Build behavior
|
|
||||||
onAddToBuild?: (p: UiPart) => void;
|
onAddToBuild?: (p: UiPart) => void;
|
||||||
addLabel?: string;
|
addLabel?: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -32,14 +31,15 @@ export default function PartsGrid(props: {
|
|||||||
{parts.map((part) => (
|
{parts.map((part) => (
|
||||||
<div
|
<div
|
||||||
key={part.id}
|
key={part.id}
|
||||||
className="group relative flex flex-col rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all duration-200 hover:border-amber-400/60 hover:bg-amber-400/10"
|
className="group relative flex flex-col rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all hover:border-amber-400/60 hover:bg-amber-400/10"
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2 justify-between">
|
<div className="mb-3 flex items-center justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-semibold text-zinc-50 truncate">
|
<div className="truncate text-sm font-semibold text-zinc-50">
|
||||||
{part.brand} <span className="font-normal">— {part.name}</span>
|
{part.brand} <span className="font-normal">— {part.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="whitespace-nowrap text-sm font-semibold text-amber-300">
|
<div className="whitespace-nowrap text-sm font-semibold text-amber-300">
|
||||||
${part.price.toFixed(2)}
|
${part.price.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
@@ -48,17 +48,16 @@ export default function PartsGrid(props: {
|
|||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={buildDetailHref(part)}
|
href={buildDetailHref(part)}
|
||||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-2 text-center text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
|
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-2 text-center text-xs font-medium text-zinc-300 hover:border-zinc-600 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
View Details
|
View Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* NEW: Add to Build (preferred primary action if provided) */}
|
|
||||||
{onAddToBuild ? (
|
{onAddToBuild ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onAddToBuild(part)}
|
onClick={() => onAddToBuild(part)}
|
||||||
className="flex-1 rounded-md bg-amber-400 px-3 py-2 text-center text-xs font-semibold text-black hover:bg-amber-300 transition-colors"
|
className="flex-1 rounded-md bg-amber-400 px-3 py-2 text-center text-xs font-semibold text-black hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{addLabel}
|
{addLabel}
|
||||||
</button>
|
</button>
|
||||||
@@ -67,7 +66,7 @@ export default function PartsGrid(props: {
|
|||||||
href={part.buyUrl}
|
href={part.buyUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex-1 rounded-md bg-amber-400 px-3 py-2 text-center text-xs font-semibold text-black hover:bg-amber-300 transition-colors"
|
className="flex-1 rounded-md bg-amber-400 px-3 py-2 text-center text-xs font-semibold text-black hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
Buy
|
Buy
|
||||||
</a>
|
</a>
|
||||||
@@ -80,7 +79,6 @@ export default function PartsGrid(props: {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,38 +88,43 @@ export default function PartsGrid(props: {
|
|||||||
// LIST view
|
// LIST view
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="hidden grid-cols-[minmax(0,3fr)_minmax(0,1fr)_minmax(0,1fr)_auto] px-3 pb-2 text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500 md:grid">
|
{/* Header (DESKTOP ONLY) */}
|
||||||
<span>Part</span>
|
<div className="hidden md:grid md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)_minmax(0,1fr)_260px] md:items-center md:gap-4 px-3 pb-2 text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500"> <span className="min-w-0">Part</span>
|
||||||
<span>Brand</span>
|
<span className="min-w-0">Brand</span>
|
||||||
<span className="text-right">Price</span>
|
<span className="text-right">Price</span>
|
||||||
<span className="pr-2 text-right">Actions</span>
|
<span className="text-right">Actions</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rows */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{parts.map((part) => (
|
{parts.map((part) => (
|
||||||
<div
|
<div
|
||||||
key={part.id}
|
key={part.id}
|
||||||
className="group relative rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all duration-200 hover:border-amber-400/60 hover:bg-amber-400/10"
|
className="group relative rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all hover:border-amber-400/60 hover:bg-amber-400/10"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2 md:grid md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)_minmax(0,1fr)_auto] md:items-center md:gap-4">
|
<div className="flex flex-col gap-2 md:grid md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)_minmax(0,1fr)_260px] md:items-center md:gap-4">
|
||||||
<div className="min-w-0 flex-1">
|
{/* Part */}
|
||||||
<div className="text-sm font-semibold text-zinc-50 truncate">
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-semibold text-zinc-50">
|
||||||
{part.name}
|
{part.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-zinc-400 md:text-left md:text-sm">
|
{/* Brand */}
|
||||||
|
<div className="min-w-0 text-xs text-zinc-400 md:text-sm">
|
||||||
{part.brand}
|
{part.brand}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
<div className="text-sm font-semibold text-amber-300 md:text-right">
|
<div className="text-sm font-semibold text-amber-300 md:text-right">
|
||||||
${part.price.toFixed(2)}
|
${part.price.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={buildDetailHref(part)}
|
href={buildDetailHref(part)}
|
||||||
className="whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-1.5 text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
|
className="whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-1.5 text-xs font-medium text-zinc-300 hover:border-zinc-600 hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
View Details
|
View Details
|
||||||
</Link>
|
</Link>
|
||||||
@@ -130,7 +133,7 @@ export default function PartsGrid(props: {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onAddToBuild(part)}
|
onClick={() => onAddToBuild(part)}
|
||||||
className="whitespace-nowrap rounded-md bg-amber-400 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-300 transition-colors"
|
className="whitespace-nowrap rounded-md bg-amber-400 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{addLabel}
|
{addLabel}
|
||||||
</button>
|
</button>
|
||||||
@@ -139,7 +142,7 @@ export default function PartsGrid(props: {
|
|||||||
href={part.buyUrl}
|
href={part.buyUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="whitespace-nowrap rounded-md bg-amber-400 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-300 transition-colors"
|
className="whitespace-nowrap rounded-md bg-amber-400 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
Buy
|
Buy
|
||||||
</a>
|
</a>
|
||||||
@@ -153,7 +156,6 @@ export default function PartsGrid(props: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-4
@@ -3,11 +3,12 @@ import type { Config } from "tailwindcss";
|
|||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}"
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
|
||||||
],
|
],
|
||||||
theme: {
|
theme: { extend: {} },
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
darkMode: "class",
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user