diff --git a/app/(app)/(builder)/builder/page.tsx b/app/(app)/(builder)/builder/page.tsx index fb2f84f..989d2d2 100644 --- a/app/(app)/(builder)/builder/page.tsx +++ b/app/(app)/(builder)/builder/page.tsx @@ -1470,6 +1470,11 @@ function GunbuilderPageContent() { if (groupCategories.length === 0) return null; + // Check if there are locked/conflicting parts in this group + const hasConflicts = tableRows.some( + (row) => row.kind === "category" && row.locked + ); + return (
+ Part Conflict Detected +
++ Some parts below are disabled because you've selected a complete assembly that already includes them. To customize individual parts, remove the complete assembly first. +
+|
{isLocked ? (
-
- —
+
+
+ ⚠ Disabled
+
+
) : selectedPart ? (
<>
diff --git a/app/(app)/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx b/app/(app)/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx
index 7e33915..471f743 100644
--- a/app/(app)/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx
+++ b/app/(app)/(builder)/parts/p/[platform]/[partRole]/[productSlug]/page.tsx
@@ -238,8 +238,11 @@ export default function ProductDetailsPage() {
setLoading(true);
setError(null);
- const url = `${API_BASE_URL}/api/v1/products/${numericId}`;
- const res = await fetch(url, { signal: controller.signal });
+ const url = `/api/catalog/products/${numericId}`;
+ const res = await fetch(url, {
+ signal: controller.signal,
+ credentials: 'include',
+ });
if (!res.ok) {
throw new Error(`Failed to load product (${res.status})`);
@@ -272,8 +275,11 @@ export default function ProductDetailsPage() {
try {
setOffersLoading(true);
- const url = `${API_BASE_URL}/api/v1/products/${numericId}/offers`;
- const res = await fetch(url, { signal: controller.signal });
+ const url = `/api/catalog/products/${numericId}/offers`;
+ const res = await fetch(url, {
+ signal: controller.signal,
+ credentials: 'include',
+ });
if (!res.ok) {
throw new Error(`Failed to load offers (${res.status})`);
diff --git a/app/api/builds/public/[id]/route.ts b/app/api/builds/public/[id]/route.ts
new file mode 100644
index 0000000..0fa87f1
--- /dev/null
+++ b/app/api/builds/public/[id]/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const { id } = params;
+
+ // Public build detail - no auth required
+ const res = await fetch(
+ `${API_BASE_URL}/api/v1/builds/${id}`,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ cache: 'no-store',
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: await res.text() },
+ { status: res.status }
+ );
+ }
+
+ return NextResponse.json(await res.json());
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err?.message || 'Failed to fetch build' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/builds/public/route.ts b/app/api/builds/public/route.ts
new file mode 100644
index 0000000..4d8e7df
--- /dev/null
+++ b/app/api/builds/public/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+
+ // Public builds endpoint - no auth required
+ const res = await fetch(
+ `${API_BASE_URL}/api/v1/builds?${searchParams}`,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ cache: 'no-store',
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: await res.text() },
+ { status: res.status }
+ );
+ }
+
+ return NextResponse.json(await res.json());
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err?.message || 'Failed to fetch builds' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/catalog/products/[id]/offers/route.ts b/app/api/catalog/products/[id]/offers/route.ts
new file mode 100644
index 0000000..8f61600
--- /dev/null
+++ b/app/api/catalog/products/[id]/offers/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const { id } = params;
+
+ // No auth required for public catalog
+ const res = await fetch(
+ `${API_BASE_URL}/api/v1/products/${id}/offers`,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ cache: 'no-store',
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: await res.text() },
+ { status: res.status }
+ );
+ }
+
+ return NextResponse.json(await res.json());
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err?.message || 'Failed to fetch product offers' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/catalog/products/[id]/route.ts b/app/api/catalog/products/[id]/route.ts
new file mode 100644
index 0000000..b8992e9
--- /dev/null
+++ b/app/api/catalog/products/[id]/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const { id } = params;
+
+ // No auth required for public catalog
+ const res = await fetch(
+ `${API_BASE_URL}/api/v1/products/${id}`,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ cache: 'no-store',
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: await res.text() },
+ { status: res.status }
+ );
+ }
+
+ return NextResponse.json(await res.json());
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err?.message || 'Failed to fetch product' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/catalog/products/route.ts b/app/api/catalog/products/route.ts
new file mode 100644
index 0000000..9013beb
--- /dev/null
+++ b/app/api/catalog/products/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+
+ // No auth required for public catalog
+ const res = await fetch(
+ `${API_BASE_URL}/api/v1/products?${searchParams}`,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ cache: 'no-store',
+ }
+ );
+
+ if (!res.ok) {
+ return NextResponse.json(
+ { error: await res.text() },
+ { status: res.status }
+ );
+ }
+
+ return NextResponse.json(await res.json());
+ } catch (err: any) {
+ return NextResponse.json(
+ { error: err?.message || 'Failed to fetch products' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/builds/[buildId]/page.tsx b/app/builds/[buildId]/page.tsx
index 1dd06f9..8a24ed7 100644
--- a/app/builds/[buildId]/page.tsx
+++ b/app/builds/[buildId]/page.tsx
@@ -31,8 +31,9 @@ export default async function BuildBreakdownPage({
}: {
params: { buildId: string };
}) {
- // Public detail endpoint
- const res = await fetch(`${API_BASE_URL}/api/v1/builds/${params.buildId}`, {
+ // Public detail endpoint - use Next.js API route
+ const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
+ const res = await fetch(`${baseUrl}/api/builds/public/${params.buildId}`, {
cache: "no-store",
});
diff --git a/app/builds/page.tsx b/app/builds/page.tsx
index 783407e..60488c9 100644
--- a/app/builds/page.tsx
+++ b/app/builds/page.tsx
@@ -132,7 +132,7 @@ export default function BuildsPage() {
setLoading(true);
setError(null);
- const res = await fetch(`${API_BASE_URL}/api/v1/builds?limit=50`, {
+ const res = await fetch('/api/builds/public?limit=50', {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
diff --git a/components/parts/PartsListPageClient.tsx b/components/parts/PartsListPageClient.tsx
index ddc6773..de54735 100644
--- a/components/parts/PartsListPageClient.tsx
+++ b/components/parts/PartsListPageClient.tsx
@@ -50,9 +50,12 @@ export default function PartsListPageClient(props: {
// Your current list endpoint:
// 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 url = `/api/catalog/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`;
- const res = await fetch(url, { headers: { Accept: "application/json" } });
+ const res = await fetch(url, {
+ headers: { Accept: "application/json" },
+ credentials: 'include',
+ });
if (!res.ok) {
const txt = await res.text().catch(() => "");
throw new Error(`Failed to load products (${res.status}): ${txt}`);
diff --git a/components/parts/ProductDetailPageClient.tsx b/components/parts/ProductDetailPageClient.tsx
index 6b59a54..d99ed80 100644
--- a/components/parts/ProductDetailPageClient.tsx
+++ b/components/parts/ProductDetailPageClient.tsx
@@ -41,8 +41,9 @@ async function fetchProductDetail(params: {
// ---- 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/v1/products/${encodeURIComponent(id)}`, {
+ const res = await fetch(`/api/catalog/products/${encodeURIComponent(id)}`, {
headers: { Accept: "application/json" },
+ credentials: 'include',
});
if (res.ok) {
@@ -54,8 +55,8 @@ 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/v1/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`,
- { headers: { Accept: "application/json" } }
+ `/api/catalog/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`,
+ { headers: { Accept: "application/json" }, credentials: 'include' }
);
if (!listRes.ok) {
diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx
index 0a9f41b..ca9842c 100644
--- a/context/AuthContext.tsx
+++ b/context/AuthContext.tsx
@@ -75,7 +75,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
// Verify session with server
- fetch("/api/auth/me")
+ fetch("/api/auth/me", { credentials: 'include' })
.then((res) => {
if (res.ok) {
return res.json();
@@ -104,7 +104,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [persistUser]);
const refreshUser = useCallback(async () => {
- const res = await fetch("/api/auth/me", { cache: "no-store" });
+ const res = await fetch("/api/auth/me", {
+ cache: "no-store",
+ credentials: 'include',
+ });
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
@@ -133,6 +136,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
+ credentials: 'include',
body: JSON.stringify({ email, password }),
});
@@ -167,6 +171,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
+ credentials: 'include',
body: JSON.stringify({
email,
password,
@@ -199,7 +204,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
persistUser(null);
// Clear server session cookies
- fetch("/api/auth/logout", { method: "POST" }).catch(() => {});
+ fetch("/api/auth/logout", {
+ method: "POST",
+ credentials: 'include',
+ }).catch(() => {});
}, [persistUser]);
const value: AuthContextValue = {
diff --git a/lib/catalog.ts b/lib/catalog.ts
index 4b0e451..3b74c97 100644
--- a/lib/catalog.ts
+++ b/lib/catalog.ts
@@ -58,9 +58,12 @@ export async function fetchProducts(params: {
if (sort) sp.set("sort", sort);
- const url = `${API_BASE_URL}/api/v1/products?${sp.toString()}`;
+ const url = `/api/catalog/products?${sp.toString()}`;
- const res = await fetch(url, { cache: "no-store" });
+ const res = await fetch(url, {
+ cache: "no-store",
+ credentials: 'include',
+ });
if (!res.ok) throw new Error(`Failed to load products (${res.status})`);
const data = (await res.json()) as ProductListItem[];
@@ -83,10 +86,13 @@ export async function fetchProductById(params: {
const { id, platform } = params;
const url =
- `${API_BASE_URL}/api/v1/products/${encodeURIComponent(id)}` +
+ `/api/catalog/products/${encodeURIComponent(id)}` +
(platform ? `?${new URLSearchParams({ platform })}` : "");
- const res = await fetch(url, { cache: "no-store" });
+ const res = await fetch(url, {
+ cache: "no-store",
+ credentials: 'include',
+ });
if (!res.ok) throw new Error(`Failed to load product (${res.status})`);
return (await res.json()) as ProductDetail;
}
\ No newline at end of file
+
+
+
+ + Part Conflict + ++ You've selected a complete assembly which already includes this part. Remove the complete assembly to select individual parts. + + |