From 777618f684a712940a91f5cfe2eab8c2ac0310b0 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 11 Dec 2025 16:27:28 -0500 Subject: [PATCH] fixed platform selector and part selector --- app/(builder)/builder/[categoryId]/page.tsx | 876 ++++++++++---------- app/(builder)/builder/page.tsx | 46 +- components/CategoryColumn.tsx | 14 +- 3 files changed, 498 insertions(+), 438 deletions(-) diff --git a/app/(builder)/builder/[categoryId]/page.tsx b/app/(builder)/builder/[categoryId]/page.tsx index c0010c8..a606572 100644 --- a/app/(builder)/builder/[categoryId]/page.tsx +++ b/app/(builder)/builder/[categoryId]/page.tsx @@ -6,7 +6,6 @@ import { useParams, useRouter, useSearchParams } from "next/navigation"; import { CATEGORIES } from "@/data/gunbuilderParts"; import type { CategoryId, Part } from "@/types/gunbuilder"; - type ViewMode = "card" | "list"; type GunbuilderProductFromApi = { @@ -31,7 +30,6 @@ const PLATFORMS = ["AR-15", "AR-10"]; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; - // sort options type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc"; @@ -61,7 +59,10 @@ export default function CategoryPage() { const [brandFilter, setBrandFilter] = useState([]); const [sortBy, setSortBy] = useState("relevance"); const [searchQuery, setSearchQuery] = useState(""); - const [priceRange, setPriceRange] = useState<{ min: number | null; max: number | null }>({ + const [priceRange, setPriceRange] = useState<{ + min: number | null; + max: number | null; + }>({ min: null, max: null, }); @@ -80,13 +81,12 @@ export default function CategoryPage() { } }); - const initialPlatform = - (searchParams.get("platform") as string) || "AR-15"; + const initialPlatform = (searchParams.get("platform") as string) || "AR-15"; const [platform, setPlatform] = useState(initialPlatform); const category = useMemo( () => CATEGORIES.find((c) => c.id === categoryId), - [categoryId], + [categoryId] ); useEffect(() => { @@ -113,7 +113,7 @@ export default function CategoryPage() { const data: GunbuilderProductFromApi[] = await res.json(); const normalized: UiPart[] = data.map((p) => ({ - id: String(p.id), // normalize to string for BuildState + id: String(p.id), // normalize to string for BuildState categoryId, name: p.name, brand: p.brand, @@ -159,14 +159,21 @@ export default function CategoryPage() { delete updated[categoryId]; return updated; }); - } else { - // Add to build and navigate back to main gunbuilder page - setBuild((prev) => ({ - ...prev, - [categoryId]: partId, - })); - router.push("/builder"); + return; } + + // Add to build and navigate back to main builder page. + // Pass selection via URL so `/builder` can apply it. + setBuild((prev) => ({ + ...prev, + [categoryId]: partId, + })); + + const qp = new URLSearchParams(); + qp.set("platform", platform || "AR-15"); + qp.set("select", `${categoryId}:${partId}`); + + router.push(`/builder?${qp.toString()}`); }; // available brands for filter @@ -175,7 +182,7 @@ export default function CategoryPage() { Array.from(new Set(parts.map((p) => p.brand))) .filter(Boolean) .sort((a, b) => a.localeCompare(b)), - [parts], + [parts] ); const priceBounds = useMemo(() => { if (parts.length === 0) { @@ -244,8 +251,7 @@ export default function CategoryPage() { const name = p.name.toLowerCase(); const brand = p.brand.toLowerCase(); return ( - name.includes(normalizedQuery) || - brand.includes(normalizedQuery) + name.includes(normalizedQuery) || brand.includes(normalizedQuery) ); }); } @@ -278,7 +284,10 @@ export default function CategoryPage() { }, [parts, brandFilter, sortBy, build, categoryId, searchQuery]); const totalPages = useMemo( - () => (filteredParts.length === 0 ? 1 : Math.ceil(filteredParts.length / PAGE_SIZE)), + () => + filteredParts.length === 0 + ? 1 + : Math.ceil(filteredParts.length / PAGE_SIZE), [filteredParts, PAGE_SIZE] ); @@ -291,11 +300,18 @@ export default function CategoryPage() { useEffect(() => { // whenever the category or filters change, jump back to page 1 setCurrentPage(1); - }, [categoryId, brandFilter, sortBy, searchQuery, priceRange, inStockOnly, platform]); + }, [ + categoryId, + brandFilter, + sortBy, + searchQuery, + priceRange, + inStockOnly, + platform, + ]); useEffect(() => { - const nextPlatform = - (searchParams.get("platform") as string) || "AR-15"; + const nextPlatform = (searchParams.get("platform") as string) || "AR-15"; setPlatform(nextPlatform); }, [searchParams]); @@ -304,11 +320,13 @@ export default function CategoryPage() { return { start: 0, end: 0 }; } const start = (currentPage - 1) * PAGE_SIZE + 1; - const end = Math.min(start + paginatedParts.length - 1, filteredParts.length); + const end = Math.min( + start + paginatedParts.length - 1, + filteredParts.length + ); return { start, end }; }, [filteredParts.length, currentPage, paginatedParts.length, PAGE_SIZE]); - if (!category) { return (
@@ -386,456 +404,452 @@ export default function CategoryPage() { {/* Parts Grid/List */}
-
- {/* Left sidebar: filters */} - {!loading && !error && parts.length > 0 && ( - - )} - - {/* Right side: toolbar + results */} -
- {/* Top toolbar: count + search + sort */} - {!loading && !error && parts.length > 0 && ( -
-
- Showing{" "} - - {visibleRange.start}-{visibleRange.end} - {" "} - of{" "} - - {filteredParts.length} - {" "} - matching parts -
-
-
- -
- setSearchQuery(e.target.value)} - placeholder={`Search ${category.name.toLowerCase()}...`} - className="rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 pr-6 text-xs text-zinc-200 focus:outline-none focus:ring-1 focus:ring-amber-400" - /> - {searchQuery && ( - - )} + {availableBrands.length === 0 ? ( +

+ No brands available yet. +

+ ) : ( +
+ {availableBrands.map((b) => { + const checked = brandFilter.includes(b); + return ( + + ); + })}
-
-
- - -
-
- - -
+ )} + {brandFilter.length === 0 && availableBrands.length > 0 && ( +

+ No brands selected — showing all. +

+ )}
-
+ + {/* In-stock toggle */} +
+ +

+ Hides items marked out of stock by the backend. +

+
+ )} - {/* Results / states */} - {loading ? ( -

- Loading {category.name.toLowerCase()}… -

- ) : error ? ( -

- {error} — check that the Ballistic API is running. -

- ) : filteredParts.length === 0 ? ( -

- No parts available for this category yet. -

- ) : viewMode === "card" ? ( -
- {paginatedParts.map((part) => { - const isSelected = build[categoryId] === part.id; - - return ( -
-
-
-
- {part.brand}{" "} - - — {part.name} - -
- {part.notes && ( -

- {part.notes} -

- )} -
-
- ${part.price.toFixed(2)} -
-
-
- - View Details - - + {/* Right side: toolbar + results */} +
+ {/* Top toolbar: count + search + sort */} + {!loading && !error && parts.length > 0 && ( +
+
+ Showing{" "} + + {visibleRange.start}-{visibleRange.end} + {" "} + of{" "} + + {filteredParts.length} + {" "} + matching parts +
+
+
+ +
+ setSearchQuery(e.target.value)} + placeholder={`Search ${category.name.toLowerCase()}...`} + className="rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 pr-6 text-xs text-zinc-200 focus:outline-none focus:ring-1 focus:ring-amber-400" + /> + {searchQuery && ( + + )}
- ); - })} -
- ) : ( - <> - {/* Header row for list view */} -
- Part - Brand - Price - Actions +
+ + +
+
+ + +
+
+ )} -
+ {/* Results / states */} + {loading ? ( +

+ Loading {category.name.toLowerCase()}… +

+ ) : error ? ( +

+ {error} — check that the Ballistic API is running. +

+ ) : filteredParts.length === 0 ? ( +

+ No parts available for this category yet. +

+ ) : viewMode === "card" ? ( +
{paginatedParts.map((part) => { const isSelected = build[categoryId] === part.id; return (
-
-
+
+
- {part.name} + {part.brand}{" "} + — {part.name}
{part.notes && ( -

+

{part.notes}

)}
-
- {part.brand} -
-
+
${part.price.toFixed(2)}
-
- - View Details - - -
+
+
+ + View Details + +
); })}
- - )} + ) : ( + <> + {/* Header row for list view */} +
+ Part + Brand + Price + Actions +
- {/* Pagination controls */} - {filteredParts.length > 0 && totalPages > 1 && ( -
-
- Page{" "} - - {currentPage} - {" "} - of{" "} - - {totalPages} - +
+ {paginatedParts.map((part) => { + const isSelected = build[categoryId] === part.id; + + return ( +
+
+
+
+ {part.name} +
+ {part.notes && ( +

+ {part.notes} +

+ )} +
+
+ {part.brand} +
+
+ ${part.price.toFixed(2)} +
+
+ + View Details + + +
+
+
+ ); + })} +
+ + )} + + {/* Pagination controls */} + {filteredParts.length > 0 && totalPages > 1 && ( +
+
+ Page{" "} + + {currentPage} + {" "} + of{" "} + + {totalPages} + +
+
+ + +
-
- - -
-
- )} + )} +
-
); -} \ No newline at end of file +} diff --git a/app/(builder)/builder/page.tsx b/app/(builder)/builder/page.tsx index 2bde5b7..952aea4 100644 --- a/app/(builder)/builder/page.tsx +++ b/app/(builder)/builder/page.tsx @@ -87,7 +87,16 @@ export default function GunbuilderPage() { const [parts, setParts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>("AR-15"); + const isValidPlatform = ( + value: string | null + ): value is (typeof PLATFORMS)[number] => + !!value && (PLATFORMS as readonly string[]).includes(value); + + const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>(() => { + if (typeof window === "undefined") return "AR-15"; + const initial = new URLSearchParams(window.location.search).get("platform"); + return isValidPlatform(initial) ? initial : "AR-15"; + }); const [build, setBuild] = useState(() => { if (typeof window !== "undefined") { const stored = localStorage.getItem(STORAGE_KEY); @@ -189,11 +198,26 @@ export default function GunbuilderPage() { return updated; }); - router.replace("/builder", { scroll: false }); + const qp = searchParams.get("platform"); + const nextPlatform = isValidPlatform(qp) ? qp : platform; + + router.replace( + `/builder?platform=${encodeURIComponent(nextPlatform)}`, + { + scroll: false, + } + ); } } }, [searchParams, router]); + useEffect(() => { + const qp = searchParams.get("platform"); + if (isValidPlatform(qp) && qp !== platform) { + setPlatform(qp); + } + }, [searchParams, platform]); + // Persist build state to localStorage whenever it changes useEffect(() => { if (typeof window !== "undefined") { @@ -695,10 +719,24 @@ export default function GunbuilderPage() { - Choose a Part + + + + Choose a Part ) : ( diff --git a/components/CategoryColumn.tsx b/components/CategoryColumn.tsx index f556472..bd5d20f 100644 --- a/components/CategoryColumn.tsx +++ b/components/CategoryColumn.tsx @@ -9,6 +9,7 @@ interface CategoryColumnProps { parts: Part[]; selectedPartId?: string; onSelectPart: (partId: string) => void; + platform?: string; } export function CategoryColumn({ @@ -16,6 +17,7 @@ export function CategoryColumn({ parts, selectedPartId, onSelectPart, + platform, }: CategoryColumnProps) { // Show selected part if available, otherwise show placeholder const displayedPart = selectedPartId @@ -37,8 +39,11 @@ export function CategoryColumn({

No part selected

{parts.length >= 1 && ( View All ({parts.length})