"use client"; import { useEffect, useMemo, useState } from "react"; type ProductSummary = { id: number; name: string; brand: string; platform: string; partRole: string; price: number | null; buyUrl: string | null; imageUrl: string | null; }; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; type PlatformOption = { key: string; // e.g. AR15, AR9 label: string; // e.g. "Armalite Rifle model 15" }; // Backend currently filters products by canonical platform strings like "AR-15". // DB platform keys are like "AR15" / "AR9". Translate when calling /api/products. const platformKeyToApiPlatform = (key: string) => { const k = (key ?? "").toUpperCase().trim(); if (!k) return "AR-15"; if (k === "AR15" || k === "AR-15") return "AR-15"; if (k === "AR10" || k === "AR-10") return "AR-10"; if (k === "AR9" || k === "AR-9") return "AR-9"; // Fallback: if it already contains a dash, assume it's canonical. if (k.includes("-")) return k; // Otherwise, best-effort: insert a dash after AR. if (k.startsWith("AR") && k.length > 2) return `AR-${k.slice(2)}`; return k; }; const FALLBACK_PLATFORMS: PlatformOption[] = [ { key: "AR15", label: "AR-15" }, { key: "AR10", label: "AR-10" }, { key: "AR9", label: "AR-9" }, ]; export default function AdminProductsPage() { const [platforms, setPlatforms] = useState(FALLBACK_PLATFORMS); const [platform, setPlatform] = useState("AR15"); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [products, setProducts] = useState([]); const [query, setQuery] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); // Load available platforms (dynamic if backend supports it; otherwise fallback) useEffect(() => { const controller = new AbortController(); async function loadPlatforms() { try { const res = await fetch(`${API_BASE_URL}/api/platforms`, { signal: controller.signal, }); if (!res.ok) return; const data = (await res.json()) as unknown; // Expected shape: // [{ id, key, label, is_active, ... }] const items = Array.isArray(data) ? data : []; const cleaned: PlatformOption[] = items .map((item) => { if (!item || typeof item !== "object") return null; const obj = item as Record; const key = typeof obj.key === "string" ? obj.key.trim() : ""; const label = typeof obj.label === "string" ? obj.label.trim() : ""; // Some payloads might use isActive; yours currently uses is_active const activeRaw = obj.is_active ?? obj.isActive; const isActive = typeof activeRaw === "boolean" ? activeRaw : typeof activeRaw === "number" ? activeRaw === 1 : true; if (!key || !label || !isActive) return null; return { key, label } as PlatformOption; }) .filter((v): v is PlatformOption => v !== null); if (cleaned.length === 0) return; // Keep them stable and unique by key const byKey = new Map(); for (const p of cleaned) byKey.set(p.key, p); const unique = Array.from(byKey.values()); setPlatforms(unique); setPlatform((prev) => (unique.some((p) => p.key === prev) ? prev : unique[0].key)); } catch (err: any) { if (err?.name === "AbortError") return; // ignore (fallback list stays) } } loadPlatforms(); return () => controller.abort(); }, []); // Load products for selected platform useEffect(() => { const controller = new AbortController(); async function loadProducts() { try { setLoading(true); setError(null); const apiPlatform = platformKeyToApiPlatform(platform); // NOTE: backend endpoint expects `platform`, not `platformKey`. const url = `${API_BASE_URL}/api/products?platform=${encodeURIComponent( apiPlatform )}`; const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { throw new Error(`Failed to load products (${res.status})`); } const data = (await res.json()) as ProductSummary[]; setProducts(Array.isArray(data) ? data : []); setPage(1); } catch (err: any) { if (err?.name === "AbortError") return; setError(err?.message ?? "Failed to load products"); setProducts([]); } finally { setLoading(false); } } loadProducts(); return () => controller.abort(); }, [platform]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return products; return products.filter((p) => { const name = (p.name ?? "").toLowerCase(); const brand = (p.brand ?? "").toLowerCase(); const role = (p.partRole ?? "").toLowerCase(); return name.includes(q) || brand.includes(q) || role.includes(q); }); }, [products, query]); // Reset to page 1 when the search query changes useEffect(() => { setPage(1); }, [query, platform, pageSize]); const totalPages = useMemo(() => { const total = filtered.length; return total === 0 ? 1 : Math.ceil(total / pageSize); }, [filtered.length, pageSize]); const clampedPage = useMemo(() => { if (page < 1) return 1; if (page > totalPages) return totalPages; return page; }, [page, totalPages]); const pageStart = useMemo(() => (clampedPage - 1) * pageSize, [clampedPage, pageSize]); const pageEndExclusive = useMemo( () => Math.min(pageStart + pageSize, filtered.length), [pageStart, pageSize, filtered.length] ); const paginated = useMemo(() => { return filtered.slice(pageStart, pageEndExclusive); }, [filtered, pageStart, pageEndExclusive]); return (

Admin

Products Catalog

Read-only view of products coming from the Ballistic backend. Filter by platform, search, and spot-check pricing + buy links.

setQuery(e.target.value)} placeholder="brand, name, part role…" className="w-64 max-w-full rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:ring-1 focus:ring-amber-400" />
{loading ? ( Loading… ) : error ? ( {error} ) : ( Showing{" "} {filtered.length === 0 ? 0 : pageStart + 1}-{pageEndExclusive} {" "}of{" "} {filtered.length} {" "}{filtered.length === 1 ? "product" : "products"} )}
{!loading && !error && filtered.length > 0 && (
Page {clampedPage} of{" "} {totalPages}
)}
{loading ? ( ) : error ? ( ) : paginated.length === 0 ? ( ) : ( paginated.map((p) => ( )) )}
Product Brand Platform Part Role Price Link
Loading products…
{error}
No products found.
{p.name}
ID: {p.id}
{p.brand} {p.platform ?? platformKeyToApiPlatform(platform)} {p.partRole} {p.price == null ? "—" : `$${Number(p.price).toFixed(2)}`} {p.buyUrl ? ( Open ) : ( )}
{!loading && !error && filtered.length > 0 && (
Page{" "} {clampedPage} {" "} of{" "} {totalPages}
)}
Backend: {API_BASE_URL} · Endpoint: /api/products?platform=... · Page size: {pageSize}
); }