fixing platform routing. not done. updated logo on builder
This commit is contained in:
@@ -20,6 +20,8 @@ type GunbuilderProductFromApi = {
|
||||
buyUrl: string | null;
|
||||
};
|
||||
|
||||
const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const;
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
@@ -85,6 +87,7 @@ export default function GunbuilderPage() {
|
||||
const [parts, setParts] = useState<Part[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>("AR-15");
|
||||
const [build, setBuild] = useState<BuildState>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -110,8 +113,10 @@ export default function GunbuilderPage() {
|
||||
setError(null);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`,
|
||||
{ signal: controller.signal },
|
||||
`${API_BASE_URL}/api/products?platform=${encodeURIComponent(
|
||||
platform
|
||||
)}`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -121,29 +126,28 @@ export default function GunbuilderPage() {
|
||||
const data: GunbuilderProductFromApi[] = await res.json();
|
||||
|
||||
const normalized: Part[] = data
|
||||
.map((p): Part | null => {
|
||||
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
|
||||
if (!categoryId) {
|
||||
// Skip any parts we don't know how to map yet
|
||||
return null;
|
||||
}
|
||||
.map((p): Part | null => {
|
||||
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
|
||||
if (!categoryId) {
|
||||
// Skip any parts we don't know how to map yet
|
||||
return null;
|
||||
}
|
||||
|
||||
const buyUrl = p.buyUrl ?? undefined;
|
||||
|
||||
return {
|
||||
id: String(p.id), // ALWAYS string
|
||||
categoryId,
|
||||
name: p.name,
|
||||
brand: p.brand,
|
||||
price: p.price ?? 0,
|
||||
imageUrl: p.mainImageUrl ?? undefined,
|
||||
affiliateUrl: buyUrl, // main link
|
||||
url: buyUrl, // 👈optional alias if you still reference .url. maybe pretty url?
|
||||
notes: undefined,
|
||||
};
|
||||
})
|
||||
.filter((p): p is Part => p !== null);
|
||||
const buyUrl = p.buyUrl ?? undefined;
|
||||
|
||||
return {
|
||||
id: String(p.id), // ALWAYS string
|
||||
categoryId,
|
||||
name: p.name,
|
||||
brand: p.brand,
|
||||
price: p.price ?? 0,
|
||||
imageUrl: p.mainImageUrl ?? undefined,
|
||||
affiliateUrl: buyUrl, // main link
|
||||
url: buyUrl, // 👈optional alias if you still reference .url. maybe pretty url?
|
||||
notes: undefined,
|
||||
};
|
||||
})
|
||||
.filter((p): p is Part => p !== null);
|
||||
|
||||
setParts(normalized);
|
||||
} catch (err: any) {
|
||||
@@ -157,7 +161,16 @@ export default function GunbuilderPage() {
|
||||
fetchProducts();
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
}, [platform]);
|
||||
|
||||
useEffect(() => {
|
||||
// When the platform changes, reset the current build so we don't
|
||||
// show stale part selections from a different platform.
|
||||
setBuild({});
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}, [platform]);
|
||||
|
||||
// Handle URL query parameter for part selection (?select=upper:165)
|
||||
useEffect(() => {
|
||||
@@ -191,9 +204,7 @@ export default function GunbuilderPage() {
|
||||
const partsByCategory: Record<CategoryId, Part[]> = useMemo(() => {
|
||||
const grouped = {} as Record<CategoryId, Part[]>;
|
||||
for (const category of CATEGORIES) {
|
||||
grouped[category.id] = parts.filter(
|
||||
(p) => p.categoryId === category.id,
|
||||
);
|
||||
grouped[category.id] = parts.filter((p) => p.categoryId === category.id);
|
||||
}
|
||||
return grouped;
|
||||
}, [parts]);
|
||||
@@ -201,7 +212,7 @@ export default function GunbuilderPage() {
|
||||
const selectedParts: Part[] = useMemo(() => {
|
||||
return Object.entries(build)
|
||||
.map(([categoryId, partId]) =>
|
||||
parts.find((p) => p.id === partId && p.categoryId === categoryId),
|
||||
parts.find((p) => p.id === partId && p.categoryId === categoryId)
|
||||
)
|
||||
.filter(Boolean) as Part[];
|
||||
}, [build, parts]);
|
||||
@@ -212,14 +223,14 @@ export default function GunbuilderPage() {
|
||||
Object.entries(build).map(([categoryId, partId]) => [
|
||||
categoryId as CategoryId,
|
||||
partId ? true : false,
|
||||
]),
|
||||
])
|
||||
) as Record<CategoryId, unknown>,
|
||||
[build],
|
||||
[build]
|
||||
);
|
||||
|
||||
const totalPrice = useMemo(
|
||||
() => selectedParts.reduce((sum, p) => sum + p.price, 0),
|
||||
[selectedParts],
|
||||
[selectedParts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -236,7 +247,7 @@ export default function GunbuilderPage() {
|
||||
const encoded = window.btoa(payload);
|
||||
const origin = window.location?.origin ?? "";
|
||||
const url = `${origin}/builder/build?build=${encodeURIComponent(
|
||||
encoded,
|
||||
encoded
|
||||
)}`;
|
||||
setShareUrl(url);
|
||||
} catch {
|
||||
@@ -254,10 +265,10 @@ export default function GunbuilderPage() {
|
||||
// Use group ordering for the summary as well
|
||||
const summaryCategoryOrder: CategoryId[] = useMemo(
|
||||
() =>
|
||||
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter(
|
||||
(id) => CATEGORIES.some((c) => c.id === id),
|
||||
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter((id) =>
|
||||
CATEGORIES.some((c) => c.id === id)
|
||||
),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleShare = async () => {
|
||||
@@ -278,14 +289,14 @@ export default function GunbuilderPage() {
|
||||
|
||||
const selectedPartId = build[cat.id];
|
||||
const part = parts.find(
|
||||
(p) => p.id === selectedPartId && p.categoryId === cat.id,
|
||||
(p) => p.id === selectedPartId && p.categoryId === cat.id
|
||||
);
|
||||
|
||||
if (part) {
|
||||
lines.push(
|
||||
`${cat.name}: ${part.brand} — ${part.name} ($${part.price.toFixed(
|
||||
2,
|
||||
)})`,
|
||||
2
|
||||
)})`
|
||||
);
|
||||
} else {
|
||||
lines.push(`${cat.name}: [Not selected]`);
|
||||
@@ -299,7 +310,7 @@ export default function GunbuilderPage() {
|
||||
setShareStatus("Build summary copied to clipboard.");
|
||||
} catch {
|
||||
setShareStatus(
|
||||
"Could not access clipboard. You can copy from the build summary page.",
|
||||
"Could not access clipboard. You can copy from the build summary page."
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -334,7 +345,9 @@ export default function GunbuilderPage() {
|
||||
setShareStatus("Share dialog opened.");
|
||||
} catch {
|
||||
// User canceled or share failed; keep it quiet but let them know
|
||||
setShareStatus("Share canceled or unavailable. You can copy the link instead.");
|
||||
setShareStatus(
|
||||
"Share canceled or unavailable. You can copy the link instead."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback to copying the link
|
||||
@@ -344,7 +357,6 @@ export default function GunbuilderPage() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||
{/* Header */}
|
||||
<header className="mb-6">
|
||||
@@ -361,10 +373,31 @@ export default function GunbuilderPage() {
|
||||
update as you go. This early-access builder keeps your setup saved
|
||||
locally so you can come back and refine it anytime.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<label className="text-xs font-medium text-zinc-400 flex items-center gap-2">
|
||||
Platform
|
||||
<select
|
||||
value={platform}
|
||||
onChange={(e) =>
|
||||
setPlatform(e.target.value as (typeof PLATFORMS)[number])
|
||||
}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
>
|
||||
{PLATFORMS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className="text-[0.7rem] text-zinc-500">
|
||||
Parts list updates automatically when you change platforms.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
{/* Build summary panel */}
|
||||
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4 flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
{/* Left: title + totals + primary actions */}
|
||||
@@ -416,7 +449,9 @@ export default function GunbuilderPage() {
|
||||
}}
|
||||
disabled={selectedParts.length === 0}
|
||||
className={`w-full sm:w-auto rounded-md border border-zinc-700 bg-zinc-900/50 px-3 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors ${
|
||||
selectedParts.length === 0 ? "opacity-40 cursor-not-allowed" : ""
|
||||
selectedParts.length === 0
|
||||
? "opacity-40 cursor-not-allowed"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Clear Build
|
||||
@@ -432,8 +467,8 @@ export default function GunbuilderPage() {
|
||||
Share Your Build
|
||||
</h3>
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
Share this link to let others view your build or bookmark it to come
|
||||
back later.
|
||||
Share this link to let others view your build or bookmark it
|
||||
to come back later.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
@@ -468,9 +503,7 @@ export default function GunbuilderPage() {
|
||||
)}
|
||||
|
||||
{shareStatus && (
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
{shareStatus}
|
||||
</p>
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">{shareStatus}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -511,15 +544,12 @@ export default function GunbuilderPage() {
|
||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
||||
<p className="mb-4 text-xs text-zinc-500">
|
||||
Work top-down through the major sections. Each row shows your
|
||||
current pick for that part type, with price and a direct buy
|
||||
link—or a quick way to choose a part if you haven't picked
|
||||
one yet.
|
||||
current pick for that part type, with price and a direct buy link—or
|
||||
a quick way to choose a part if you haven't picked one yet.
|
||||
</p>
|
||||
|
||||
{loading && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
Loading parts from backend…
|
||||
</p>
|
||||
<p className="text-sm text-zinc-500">Loading parts from backend…</p>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<p className="text-sm text-red-400">
|
||||
@@ -532,7 +562,7 @@ export default function GunbuilderPage() {
|
||||
<div className="space-y-6">
|
||||
{CATEGORY_GROUPS.map((group) => {
|
||||
const groupCategories = CATEGORIES.filter((c) =>
|
||||
group.categoryIds.includes(c.id as CategoryId),
|
||||
group.categoryIds.includes(c.id as CategoryId)
|
||||
);
|
||||
|
||||
if (groupCategories.length === 0) {
|
||||
@@ -584,7 +614,7 @@ export default function GunbuilderPage() {
|
||||
partsByCategory[category.id] ?? [];
|
||||
const selectedPartId = build[category.id];
|
||||
const selectedPart = categoryParts.find(
|
||||
(p) => p.id === selectedPartId,
|
||||
(p) => p.id === selectedPartId
|
||||
);
|
||||
const hasParts = categoryParts.length > 0;
|
||||
|
||||
@@ -663,10 +693,9 @@ export default function GunbuilderPage() {
|
||||
</>
|
||||
) : hasParts ? (
|
||||
<Link
|
||||
href={`/builder/${category.id}`}
|
||||
className="inline-flex rounded-md border border-zinc-700 bg-zinc-900/70 px-2.5 py-1 text-[0.7rem] font-medium text-zinc-200 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
onClick={() => {
|
||||
// If you want to pre-select something later, you can handle here.
|
||||
href={{
|
||||
pathname: `/builder/${category.id}`,
|
||||
query: { platform },
|
||||
}}
|
||||
>
|
||||
Choose a Part
|
||||
@@ -729,4 +758,4 @@ export default function GunbuilderPage() {
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user