fixed login session for user name, and updated the api calls on the product detials pages.
CI / test (push) Successful in 6s
CI / test (push) Successful in 6s
This commit is contained in:
@@ -1470,6 +1470,11 @@ function GunbuilderPageContent() {
|
|||||||
|
|
||||||
if (groupCategories.length === 0) return null;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`group-${group.id}`}
|
id={`group-${group.id}`}
|
||||||
@@ -1489,6 +1494,23 @@ function GunbuilderPageContent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<table className="min-w-full text-xs md:text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1635,8 +1657,27 @@ function GunbuilderPageContent() {
|
|||||||
<td className="px-3 py-2 align-top">
|
<td className="px-3 py-2 align-top">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{isLocked ? (
|
{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>
|
</div>
|
||||||
) : selectedPart ? (
|
) : selectedPart ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -238,8 +238,11 @@ export default function ProductDetailsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const url = `${API_BASE_URL}/api/v1/products/${numericId}`;
|
const url = `/api/catalog/products/${numericId}`;
|
||||||
const res = await fetch(url, { signal: controller.signal });
|
const res = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to load product (${res.status})`);
|
throw new Error(`Failed to load product (${res.status})`);
|
||||||
@@ -272,8 +275,11 @@ export default function ProductDetailsPage() {
|
|||||||
try {
|
try {
|
||||||
setOffersLoading(true);
|
setOffersLoading(true);
|
||||||
|
|
||||||
const url = `${API_BASE_URL}/api/v1/products/${numericId}/offers`;
|
const url = `/api/catalog/products/${numericId}/offers`;
|
||||||
const res = await fetch(url, { signal: controller.signal });
|
const res = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to load offers (${res.status})`);
|
throw new Error(`Failed to load offers (${res.status})`);
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,8 +31,9 @@ export default async function BuildBreakdownPage({
|
|||||||
}: {
|
}: {
|
||||||
params: { buildId: string };
|
params: { buildId: string };
|
||||||
}) {
|
}) {
|
||||||
// Public detail endpoint
|
// Public detail endpoint - use Next.js API route
|
||||||
const res = await fetch(`${API_BASE_URL}/api/v1/builds/${params.buildId}`, {
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||||
|
const res = await fetch(`${baseUrl}/api/builds/public/${params.buildId}`, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -132,7 +132,7 @@ export default function BuildsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -50,9 +50,12 @@ export default function PartsListPageClient(props: {
|
|||||||
|
|
||||||
// Your current list endpoint:
|
// Your current list endpoint:
|
||||||
// GET /api/v1/products?platform=AR-15&partRoles=upper-receiver
|
// 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) {
|
if (!res.ok) {
|
||||||
const txt = await res.text().catch(() => "");
|
const txt = await res.text().catch(() => "");
|
||||||
throw new Error(`Failed to load products (${res.status}): ${txt}`);
|
throw new Error(`Failed to load products (${res.status}): ${txt}`);
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ async function fetchProductDetail(params: {
|
|||||||
// ---- Preferred (if you add it): GET /api/v1/products/{id}
|
// ---- Preferred (if you add it): GET /api/v1/products/{id}
|
||||||
// If it 404s, we fall back.
|
// If it 404s, we fall back.
|
||||||
try {
|
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" },
|
headers: { Accept: "application/json" },
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
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)
|
// ---- Fallback: use list endpoint + find by id (works right now with your current API)
|
||||||
const listRes = await fetch(
|
const listRes = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`,
|
`/api/catalog/products?platform=${encodeURIComponent(platform)}&partRoles=${encodeURIComponent(partRole)}`,
|
||||||
{ headers: { Accept: "application/json" } }
|
{ headers: { Accept: "application/json" }, credentials: 'include' }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!listRes.ok) {
|
if (!listRes.ok) {
|
||||||
|
|||||||
+11
-3
@@ -75,7 +75,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify session with server
|
// Verify session with server
|
||||||
fetch("/api/auth/me")
|
fetch("/api/auth/me", { credentials: 'include' })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return res.json();
|
return res.json();
|
||||||
@@ -104,7 +104,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [persistUser]);
|
}, [persistUser]);
|
||||||
|
|
||||||
const refreshUser = useCallback(async () => {
|
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) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => res.statusText);
|
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", {
|
const res = await fetch("/api/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: 'include',
|
||||||
body: JSON.stringify({ email, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,6 +171,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const res = await fetch("/api/auth/register", {
|
const res = await fetch("/api/auth/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
@@ -199,7 +204,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
persistUser(null);
|
persistUser(null);
|
||||||
|
|
||||||
// Clear server session cookies
|
// Clear server session cookies
|
||||||
fetch("/api/auth/logout", { method: "POST" }).catch(() => {});
|
fetch("/api/auth/logout", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: 'include',
|
||||||
|
}).catch(() => {});
|
||||||
}, [persistUser]);
|
}, [persistUser]);
|
||||||
|
|
||||||
const value: AuthContextValue = {
|
const value: AuthContextValue = {
|
||||||
|
|||||||
+10
-4
@@ -58,9 +58,12 @@ export async function fetchProducts(params: {
|
|||||||
|
|
||||||
if (sort) sp.set("sort", sort);
|
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})`);
|
if (!res.ok) throw new Error(`Failed to load products (${res.status})`);
|
||||||
const data = (await res.json()) as ProductListItem[];
|
const data = (await res.json()) as ProductListItem[];
|
||||||
|
|
||||||
@@ -83,10 +86,13 @@ export async function fetchProductById(params: {
|
|||||||
const { id, platform } = params;
|
const { id, platform } = params;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
`${API_BASE_URL}/api/v1/products/${encodeURIComponent(id)}` +
|
`/api/catalog/products/${encodeURIComponent(id)}` +
|
||||||
(platform ? `?${new URLSearchParams({ platform })}` : "");
|
(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})`);
|
if (!res.ok) throw new Error(`Failed to load product (${res.status})`);
|
||||||
return (await res.json()) as ProductDetail;
|
return (await res.json()) as ProductDetail;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user