setting up dark/white mode. fixed some other small things

This commit is contained in:
2025-12-18 21:06:08 -05:00
parent 7f3818f795
commit c6d1bce771
11 changed files with 260 additions and 107 deletions
+1
View File
@@ -0,0 +1 @@
<main className="min-h-screen bg-white text-zinc-900 dark:bg-black dark:text-zinc-50">
+2 -2
View File
@@ -319,8 +319,8 @@ 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">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
+27 -23
View File
@@ -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 linkor a quick current pick for that part type, with price and a direct buy linkor
way to choose a part if you haven&apos;t picked one yet. a quick way to choose a part if you haven&apos;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"
> >
@@ -1027,4 +1031,4 @@ export default function GunbuilderPage() {
</div> </div>
</main> </main>
); );
} }
+4 -7
View File
@@ -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>
); );
} }
+22 -9
View File
@@ -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,17 +12,31 @@ 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 }} />
<AuthProvider> </head>
<Banner />
{children}
</AuthProvider>
<body className="bg-white text-zinc-950 dark:bg-black dark:text-zinc-50">
{" "}
<AuthProvider>
<Banner />
{children}
</AuthProvider>
</body> </body>
</html> </html>
); );
} }
+102 -26
View File
@@ -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,22 +132,57 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
Builds Builds
</Link> </Link>
{/* Right → Current filter */} {/* ------------------------------------------------------------------ */}
{currentCategory && ( {/* RIGHT SIDE: actions + state */}
<div className="ml-auto flex items-center gap-2 text-neutral-400 text-xs"> {/* IMPORTANT: this is the ONLY `ml-auto` in the nav to avoid conflicts */}
<span className="uppercase tracking-wide text-[10px]">Viewing</span> {/* ------------------------------------------------------------------ */}
<span className="font-medium text-neutral-100"> <div className="ml-auto flex items-center gap-3">
{CATEGORIES.find((c) => c.id === currentCategory)?.name ?? {/* Search icon (placeholder action) */}
currentCategory} <button
</span> type="button"
<Link 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"
href={baseHref} aria-label="Search (coming soon)"
className="rounded border border-neutral-700 px-2 py-0.5 text-[10px] uppercase tracking-wide hover:bg-neutral-800 hover:text-white" 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"
> >
Clear <circle cx="11" cy="11" r="6" />
</Link> <line x1="16.5" y1="16.5" x2="21" y2="21" />
</div> </svg>
)} </button>
{/* "Viewing" indicator (only shows when a category is active) */}
{currentCategory && (
<div className="flex items-center gap-2 text-neutral-400 text-xs">
<span className="uppercase tracking-wide text-[10px]">Viewing</span>
<span className="font-medium text-neutral-100">
{CATEGORIES.find((c) => c.id === currentCategory)?.name ??
currentCategory}
</span>
{/* Clears category filter -> go back to /parts */}
<Link
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"
>
Clear
</Link>
</div>
)}
{/* FUTURE: drop in ThemeToggle, UserMenu, Notifications here */}
{/* <ThemeToggle /> */}
{/* <UserMenu /> */}
</div>
</nav> </nav>
</div> </div>
); );
+49
View File
@@ -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>
);
}
+15 -13
View File
@@ -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,20 +21,19 @@ 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" width={260} // adjust to taste
width={260} // adjust to taste height={48} // adjust to taste
height={48} // adjust to taste className="block"
className="block" />
/>
</Link> </Link>
{/* 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,10 +82,13 @@ export function TopNav() {
> >
Join Beta Join Beta
</Link> </Link>
<div className="flex items-center gap-3">
<ThemeToggle />
</div>
</> </>
)} )}
</div> </div>
</div> </div>
</header> </header>
); );
} }
+8
View File
@@ -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)))
+25 -23
View File
@@ -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
View File
@@ -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;