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 && (
-
-
@@ -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" });