fixed login session for user name, and updated the api calls on the product detials pages.
CI / test (push) Successful in 6s

This commit is contained in:
2026-01-25 20:20:48 -05:00
parent 3aee0e6755
commit b09ccc542e
13 changed files with 256 additions and 21 deletions
+43 -2
View File
@@ -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 (
<div
id={`group-${group.id}`}
@@ -1489,6 +1494,23 @@ function GunbuilderPageContent() {
)}
</div>
</div>
{hasConflicts && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 p-3">
<div className="flex items-start gap-2">
<span className="text-amber-400 text-sm"></span>
<div className="flex-1">
<p className="text-sm font-medium text-amber-300">
Part Conflict Detected
</p>
<p className="text-xs text-zinc-300 mt-1">
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.
</p>
</div>
</div>
</div>
)}
<div className="overflow-x-auto rounded-md border border-zinc-800 bg-zinc-950/80">
<table className="min-w-full text-xs md:text-sm">
<thead>
@@ -1635,8 +1657,27 @@ function GunbuilderPageContent() {
<td className="px-3 py-2 align-top">
<div className="flex justify-end gap-2">
{isLocked ? (
<div className="text-[0.7rem] text-zinc-500">
<div className="flex items-center gap-2">
<span className="text-[0.7rem] text-amber-400/70">
⚠ Disabled
</span>
<div className="group relative">
<button
type="button"
className="text-zinc-500 hover:text-zinc-300 text-xs"
title="Why is this disabled?"
>
</button>
<div className="invisible group-hover:visible absolute right-0 top-6 z-10 w-64 rounded-md border border-zinc-800 bg-zinc-950 p-3 text-xs text-zinc-300 shadow-xl">
<p className="font-semibold text-amber-300 mb-1">
Part Conflict
</p>
<p>
You've selected a complete assembly which already includes this part. Remove the complete assembly to select individual parts.
</p>
</div>
</div>
</div>
) : selectedPart ? (
<>
@@ -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})`);
+35
View File
@@ -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 }
);
}
}
+32
View File
@@ -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 }
);
}
}
@@ -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 }
);
}
}
+35
View File
@@ -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 }
);
}
}
+32
View File
@@ -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 }
);
}
}
+3 -2
View File
@@ -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",
});
+1 -1
View File
@@ -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" },
+5 -2
View File
@@ -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}`);
+4 -3
View File
@@ -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) {
+11 -3
View File
@@ -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 = {
+10 -4
View File
@@ -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;
}