From 7f3818f7955406369f681b0ce007e0f71868f616 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 18 Dec 2025 19:18:15 -0500 Subject: [PATCH] ui using new v1 api for products --- app/(builder)/builder/build/page.tsx | 4 +- app/(builder)/builder/page.tsx | 4 +- app/(builder)/builder/summary/page.tsx | 2 +- .../parts/[partRole]/[productSlug]/page.tsx | 2 +- .../[partRole]/_old_prod_details/data.ts | 4 +- .../[partRole]/_old_prod_details/page.tsx | 2 +- .../[partRole]/[productSlug]/page.tsx | 2 +- app/admin/products/page.tsx | 6 +- components/parts/PartsBrowseClient.tsx | 181 +++++++++++++----- components/parts/PartsListPageClient.tsx | 4 +- components/parts/ProductDetailPageClient.tsx | 6 +- lib/catalog.ts | 10 +- 12 files changed, 153 insertions(+), 74 deletions(-) diff --git a/app/(builder)/builder/build/page.tsx b/app/(builder)/builder/build/page.tsx index 976253e..2201882 100644 --- a/app/(builder)/builder/build/page.tsx +++ b/app/(builder)/builder/build/page.tsx @@ -177,12 +177,12 @@ export default function BuildSummaryPage() { setError(null); // scoped (platform) - const scopedUrl = `${API_BASE_URL}/api/products?platform=${encodeURIComponent( + const scopedUrl = `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent( platform )}`; // universal (optional) - const universalUrl = `${API_BASE_URL}/api/products`; + const universalUrl = `${API_BASE_URL}/api/v1/products`; const [scopedRes, universalRes] = await Promise.all([ fetch(scopedUrl, { signal: controller.signal }), diff --git a/app/(builder)/builder/page.tsx b/app/(builder)/builder/page.tsx index cd9c03a..e82d5ff 100644 --- a/app/(builder)/builder/page.tsx +++ b/app/(builder)/builder/page.tsx @@ -316,11 +316,11 @@ export default function GunbuilderPage() { setLoading(true); setError(null); - const scopedUrl = `${API_BASE_URL}/api/products?platform=${encodeURIComponent( + const scopedUrl = `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent( platform )}`; - const universalUrl = `${API_BASE_URL}/api/products`; + const universalUrl = `${API_BASE_URL}/api/v1/products`; const [scopedRes, universalRes] = await Promise.all([ fetch(scopedUrl, { signal: controller.signal }), diff --git a/app/(builder)/builder/summary/page.tsx b/app/(builder)/builder/summary/page.tsx index 236a873..cbb090c 100644 --- a/app/(builder)/builder/summary/page.tsx +++ b/app/(builder)/builder/summary/page.tsx @@ -116,7 +116,7 @@ export default function BuildDetailsPage() { setPartsError(null); const res = await fetch( - `${API_BASE_URL}/api/products?platform=${encodeURIComponent(platform)}`, + `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent(platform)}`, { signal: controller.signal }, ); diff --git a/app/(builder)/parts/[partRole]/[productSlug]/page.tsx b/app/(builder)/parts/[partRole]/[productSlug]/page.tsx index 1f824b9..ea5b164 100644 --- a/app/(builder)/parts/[partRole]/[productSlug]/page.tsx +++ b/app/(builder)/parts/[partRole]/[productSlug]/page.tsx @@ -122,7 +122,7 @@ export default function ProductDetailsPage() { setLoading(true); setError(null); - const url = `${API_BASE_URL}/api/products/${numericId}`; + const url = `${API_BASE_URL}/api/v1/products/${numericId}`; const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { diff --git a/app/(builder)/parts/[partRole]/_old_prod_details/data.ts b/app/(builder)/parts/[partRole]/_old_prod_details/data.ts index f1805ca..bb2f5f1 100644 --- a/app/(builder)/parts/[partRole]/_old_prod_details/data.ts +++ b/app/(builder)/parts/[partRole]/_old_prod_details/data.ts @@ -56,13 +56,13 @@ export function getPricingHistory( /** * Get retailer offers for a part * Real implementation using Ballistic backend: - * GET /api/products/{productId}/offers + * GET /api/v1/products/{productId}/offers */ export async function getRetailerOffers( productId: string, ): Promise { const res = await fetch( - `${API_BASE_URL}/api/products/${productId}/offers`, + `${API_BASE_URL}/api/v1/products/${productId}/offers`, { // This will be called from the client detail page, // so we *don't* use cache here. diff --git a/app/(builder)/parts/[partRole]/_old_prod_details/page.tsx b/app/(builder)/parts/[partRole]/_old_prod_details/page.tsx index 463ff99..73ba000 100644 --- a/app/(builder)/parts/[partRole]/_old_prod_details/page.tsx +++ b/app/(builder)/parts/[partRole]/_old_prod_details/page.tsx @@ -135,7 +135,7 @@ export default function CategoryPage() { search.append("partRoles", r); } - const url = `${API_BASE_URL}/api/products?${search.toString()}`; + const url = `${API_BASE_URL}/api/v1/products?${search.toString()}`; const res = await fetch(url, { signal: controller.signal }); diff --git a/app/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx b/app/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx index 97154c6..164b85e 100644 --- a/app/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx +++ b/app/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx @@ -172,7 +172,7 @@ export default function ProductDetailsPage() { setLoading(true); setError(null); - const url = `${API_BASE_URL}/api/products/${numericId}`; + const url = `${API_BASE_URL}/api/v1/products/${numericId}`; const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { diff --git a/app/admin/products/page.tsx b/app/admin/products/page.tsx index 30a89cc..94b0e7e 100644 --- a/app/admin/products/page.tsx +++ b/app/admin/products/page.tsx @@ -22,7 +22,7 @@ type PlatformOption = { }; // Backend currently filters products by canonical platform strings like "AR-15". -// DB platform keys are like "AR15" / "AR9". Translate when calling /api/products. +// DB platform keys are like "AR15" / "AR9". Translate when calling /api/v1/products. const platformKeyToApiPlatform = (key: string) => { const k = (key ?? "").toUpperCase().trim(); if (!k) return "AR-15"; @@ -123,7 +123,7 @@ export default function AdminProductsPage() { const apiPlatform = platformKeyToApiPlatform(platform); // NOTE: backend endpoint expects `platform`, not `platformKey`. - const url = `${API_BASE_URL}/api/products?platform=${encodeURIComponent( + const url = `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent( apiPlatform )}`; @@ -402,7 +402,7 @@ export default function AdminProductsPage() { )}
- Backend: {API_BASE_URL} · Endpoint: /api/products?platform=... · Page size: {pageSize} + Backend: {API_BASE_URL} · Endpoint: /api/v1/products?platform=... · Page size: {pageSize}
diff --git a/components/parts/PartsBrowseClient.tsx b/components/parts/PartsBrowseClient.tsx index 7ddf627..0ecd752 100644 --- a/components/parts/PartsBrowseClient.tsx +++ b/components/parts/PartsBrowseClient.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; /** * PartsBrowseClient * ----------------------------------------------------------------------------- @@ -20,7 +21,10 @@ import Pagination from "@/components/parts/Pagination"; import PlatformSwitcher from "@/components/parts/PlatformSwitcher"; import type { CategoryId } from "@/types/gunbuilder"; -import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings"; +import { + PART_ROLE_TO_CATEGORY, + normalizePartRole, +} from "@/lib/catalogMappings"; type ViewMode = "card" | "list"; type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc"; @@ -49,7 +53,8 @@ type UiPart = { inStock?: boolean; }; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; const PAGE_SIZE = 24; function normalizeId(id: string | number) { @@ -70,9 +75,9 @@ function toSlug(s: string) { */ function buildDetailHref(platform: string, partRole: string, p: UiPart) { const slug = toSlug(`${p.brand ?? ""} ${p.name ?? ""}`); - return `/parts/p/${encodeURIComponent(platform)}/${encodeURIComponent(partRole)}/${encodeURIComponent( - `${p.id}-${slug}` - )}`; + return `/parts/p/${encodeURIComponent(platform)}/${encodeURIComponent( + partRole + )}/${encodeURIComponent(`${p.id}-${slug}`)}`; } export default function PartsBrowseClient(props: { @@ -84,7 +89,8 @@ export default function PartsBrowseClient(props: { const router = useRouter(); const searchParams = useSearchParams(); - const effectivePlatform = props.platform ?? searchParams.get("platform") ?? "AR-15"; + const effectivePlatform = + props.platform ?? searchParams.get("platform") ?? "AR-15"; const partRole = props.partRole; const [viewMode, setViewMode] = useState("list"); @@ -95,7 +101,10 @@ export default function PartsBrowseClient(props: { 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, }); @@ -116,7 +125,7 @@ export default function PartsBrowseClient(props: { search.set("platform", effectivePlatform); search.append("partRoles", partRole); - const url = `${API_BASE_URL}/api/products?${search.toString()}`; + const url = `${API_BASE_URL}/api/v1/products?${search.toString()}`; const res = await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error(`Failed to load products (${res.status})`); @@ -150,7 +159,15 @@ export default function PartsBrowseClient(props: { useEffect(() => { setCurrentPage(1); - }, [partRole, effectivePlatform, brandFilter, sortBy, searchQuery, priceRange, inStockOnly]); + }, [ + partRole, + effectivePlatform, + brandFilter, + sortBy, + searchQuery, + priceRange, + inStockOnly, + ]); const availableBrands = useMemo( () => @@ -161,7 +178,8 @@ export default function PartsBrowseClient(props: { ); const priceBounds = useMemo(() => { - if (parts.length === 0) return { min: null as number | null, max: null as number | null }; + if (parts.length === 0) + return { min: null as number | null, max: null as number | null }; let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; @@ -198,7 +216,9 @@ export default function PartsBrowseClient(props: { const effectiveMin = priceRange.min ?? priceBounds.min; const effectiveMax = priceRange.max ?? priceBounds.max; if (effectiveMin != null && effectiveMax != null) { - result = result.filter((p) => p.price >= effectiveMin && p.price <= effectiveMax); + result = result.filter( + (p) => p.price >= effectiveMin && p.price <= effectiveMax + ); } if (brandFilter.length > 0) { @@ -234,10 +254,22 @@ export default function PartsBrowseClient(props: { } return result; - }, [parts, brandFilter, sortBy, searchQuery, priceRange, priceBounds.min, priceBounds.max, inStockOnly]); + }, [ + parts, + brandFilter, + sortBy, + searchQuery, + priceRange, + priceBounds.min, + priceBounds.max, + inStockOnly, + ]); const totalPages = useMemo( - () => (filteredParts.length === 0 ? 1 : Math.ceil(filteredParts.length / PAGE_SIZE)), + () => + filteredParts.length === 0 + ? 1 + : Math.ceil(filteredParts.length / PAGE_SIZE), [filteredParts.length] ); @@ -250,21 +282,30 @@ export default function PartsBrowseClient(props: { const visibleRange = useMemo(() => { if (filteredParts.length === 0) 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]); const headingTitle = props.title ?? `${partRole.replaceAll("-", " ")} parts`; - const headingSubtitle = props.subtitle ?? "Browse available parts pulled from your Ballistic backend."; + const headingSubtitle = + props.subtitle ?? + "Browse available parts pulled from your Ballistic backend."; // ✅ Add-to-build handler: deep-links into builder’s existing ?select= logic const handleAddToBuild = (p: UiPart) => { const normalizedRole = normalizePartRole(partRole); const categoryId: CategoryId | null = - PART_ROLE_TO_CATEGORY[partRole] ?? PART_ROLE_TO_CATEGORY[normalizedRole] ?? null; + PART_ROLE_TO_CATEGORY[partRole] ?? + PART_ROLE_TO_CATEGORY[normalizedRole] ?? + null; if (!categoryId) { - alert(`No CategoryId mapping found for role "${partRole}". Add it to catalogMappings.`); + alert( + `No CategoryId mapping found for role "${partRole}". Add it to catalogMappings.` + ); return; } @@ -286,36 +327,63 @@ export default function PartsBrowseClient(props: {

- {headingTitle} {effectivePlatform} + {headingTitle}{" "} + {effectivePlatform}

-

{headingSubtitle}

+

+ {headingSubtitle} +

- +
- {!loading && !error && parts.length > 0 && ( -
- - + ← Back to Build + + + {parts.length > 0 && ( +
+ + +
+ )}
)} @@ -352,35 +420,46 @@ export default function PartsBrowseClient(props: { )} {loading ? ( -

Loading…

+

+ Loading… +

) : error ? (

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

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

No parts found for this role yet.

+

+ No parts found for this role yet. +

) : ( buildDetailHref(effectivePlatform, partRole, p)} + buildDetailHref={(p) => + buildDetailHref(effectivePlatform, partRole, p) + } onAddToBuild={handleAddToBuild} addLabel="Add to Build" /> )} - {!loading && !error && filteredParts.length > 0 && totalPages > 1 && ( - setCurrentPage((p) => Math.max(1, p - 1))} - onNext={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} - /> - )} + {!loading && + !error && + filteredParts.length > 0 && + totalPages > 1 && ( + setCurrentPage((p) => Math.max(1, p - 1))} + onNext={() => + setCurrentPage((p) => Math.min(totalPages, p + 1)) + } + /> + )} ); -} \ No newline at end of file +} diff --git a/components/parts/PartsListPageClient.tsx b/components/parts/PartsListPageClient.tsx index ef76f31..ae5678d 100644 --- a/components/parts/PartsListPageClient.tsx +++ b/components/parts/PartsListPageClient.tsx @@ -48,8 +48,8 @@ export default function PartsListPageClient(props: { setError(null); // Your current list endpoint: - // GET /api/products?platform=AR-15&partRoles=upper-receiver - const url = `${API_BASE_URL}/api/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`; + // GET /api/v1/products?platform=AR-15&partRoles=upper-receiver + const url = `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`; const res = await fetch(url, { headers: { Accept: "application/json" } }); if (!res.ok) { diff --git a/components/parts/ProductDetailPageClient.tsx b/components/parts/ProductDetailPageClient.tsx index a4b0695..7c9beb2 100644 --- a/components/parts/ProductDetailPageClient.tsx +++ b/components/parts/ProductDetailPageClient.tsx @@ -38,10 +38,10 @@ async function fetchProductDetail(params: { if (!id) throw new Error(`Invalid product URL slug: '${productSlug}' (could not parse id)`); - // ---- Preferred (if you add it): GET /api/products/{id} + // ---- Preferred (if you add it): GET /api/v1/products/{id} // If it 404s, we fall back. try { - const res = await fetch(`${API_BASE_URL}/api/products/${encodeURIComponent(id)}`, { + const res = await fetch(`${API_BASE_URL}/api/v1/products/${encodeURIComponent(id)}`, { headers: { Accept: "application/json" }, }); @@ -54,7 +54,7 @@ async function fetchProductDetail(params: { // ---- Fallback: use list endpoint + find by id (works right now with your current API) const listRes = await fetch( - `${API_BASE_URL}/api/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`, + `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`, { headers: { Accept: "application/json" } } ); diff --git a/lib/catalog.ts b/lib/catalog.ts index c6014c3..8c41b7b 100644 --- a/lib/catalog.ts +++ b/lib/catalog.ts @@ -42,7 +42,7 @@ export function slugify(input: string) { /** * Your existing list endpoint: - * GET /api/products?platform=AR-15&partRoles=complete-upper + * GET /api/v1/products?platform=AR-15&partRoles=complete-upper */ export async function fetchProducts(params: { platform: string; @@ -51,7 +51,7 @@ export async function fetchProducts(params: { const { platform, partRole } = params; const url = - `${API_BASE_URL}/api/products?` + + `${API_BASE_URL}/api/v1/products?` + new URLSearchParams({ platform, partRoles: partRole, @@ -70,9 +70,9 @@ export async function fetchProducts(params: { /** * You likely have (or should add) a detail endpoint like: - * GET /api/products/{id}?platform=AR-15 OR GET /api/products/{id} + * GET /api/v1/products/{id}?platform=AR-15 OR GET /api/v1/products/{id} * - * We'll call /api/products/{id} and optionally include platform as a query param. + * We'll call /api/v1/products/{id} and optionally include platform as a query param. */ export async function fetchProductById(params: { id: string; @@ -81,7 +81,7 @@ export async function fetchProductById(params: { const { id, platform } = params; const url = - `${API_BASE_URL}/api/products/${encodeURIComponent(id)}` + + `${API_BASE_URL}/api/v1/products/${encodeURIComponent(id)}` + (platform ? `?${new URLSearchParams({ platform })}` : ""); const res = await fetch(url, { cache: "no-store" });