wired up an accounts page and my builds (my vault)
This commit is contained in:
+115
-26
@@ -1,3 +1,4 @@
|
||||
// app/(builder)/builder/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from "react";
|
||||
@@ -7,7 +8,7 @@ import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||
import { PART_ROLE_TO_CATEGORY } from "@/lib/catalogMappings";
|
||||
import { Pencil, X, Link2, Square, CheckSquare } from "lucide-react";
|
||||
import { X, Link2, Square, CheckSquare } from "lucide-react";
|
||||
|
||||
// ✅ Centralized overlap rules + helpers
|
||||
import OverlapChip from "@/components/builder/OverlapChip";
|
||||
@@ -288,10 +289,6 @@ export default function GunbuilderPage() {
|
||||
|
||||
const handleSelectPart = useCallback(
|
||||
(categoryId: CategoryId, partId: string, _opts?: { confirm?: boolean }) => {
|
||||
// NOTE:
|
||||
// - We DO NOT auto-remove any existing selections.
|
||||
// - We show subtle overlap guidance (chips + an optional “heads up” toast).
|
||||
|
||||
const warn = (msg: string) => {
|
||||
setShareStatus(msg);
|
||||
window.setTimeout(() => setShareStatus(null), 4000);
|
||||
@@ -300,7 +297,6 @@ export default function GunbuilderPage() {
|
||||
const hints = getSelectionHints(categoryId, build);
|
||||
if (hints.length > 0) warn(hints[0]);
|
||||
|
||||
// ✅ Only set the selection. No deletes. No clearing.
|
||||
setBuild((prev) => ({
|
||||
...prev,
|
||||
[categoryId]: partId,
|
||||
@@ -437,6 +433,54 @@ export default function GunbuilderPage() {
|
||||
});
|
||||
}, [platform]);
|
||||
|
||||
// ✅ Load a saved build from Vault via ?load=<uuid>
|
||||
useEffect(() => {
|
||||
const loadUuid = searchParams.get("load");
|
||||
if (!loadUuid) return;
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setShareStatus("Loading build…");
|
||||
|
||||
// NOTE: your backend GET route is /api/v1/products/builds/{uuid}
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/products/builds/${encodeURIComponent(
|
||||
loadUuid
|
||||
)}`,
|
||||
{ credentials: "include" }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "");
|
||||
throw new Error(`Load failed (${res.status}) ${txt}`);
|
||||
}
|
||||
|
||||
const dto = await res.json(); // BuildDto w/ items
|
||||
const nextBuild: Record<string, string> = {};
|
||||
|
||||
for (const it of dto.items ?? []) {
|
||||
if (!it?.slot || !it?.productId) continue;
|
||||
nextBuild[it.slot] = String(it.productId);
|
||||
}
|
||||
|
||||
setBuild(nextBuild);
|
||||
setShareStatus(`Loaded: ${dto.title}`);
|
||||
|
||||
// Clean URL so reload doesn't re-fetch
|
||||
const qp = new URLSearchParams(searchParams.toString());
|
||||
qp.delete("load");
|
||||
router.replace(`/builder?${qp.toString()}`, { scroll: false });
|
||||
} catch (e: any) {
|
||||
setShareStatus(e?.message ?? "Failed to load build");
|
||||
} finally {
|
||||
window.setTimeout(() => setShareStatus(null), 4500);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, router]);
|
||||
|
||||
// Handle URL query parameters:
|
||||
// - ?select=categoryId:partId
|
||||
// - ?remove=categoryId
|
||||
@@ -527,6 +571,53 @@ export default function GunbuilderPage() {
|
||||
[]
|
||||
);
|
||||
|
||||
// Handler to Save user build to account
|
||||
const handleSaveToAccount = async () => {
|
||||
if (selectedParts.length === 0) {
|
||||
setShareStatus("Add a few parts before saving.");
|
||||
return;
|
||||
}
|
||||
|
||||
const title = `${platform} Build`;
|
||||
|
||||
const items = Object.entries(build)
|
||||
.filter(([, partId]) => !!partId)
|
||||
.map(([categoryId, partId]) => ({
|
||||
productId: Number(partId),
|
||||
slot: categoryId,
|
||||
position: 0,
|
||||
quantity: 1,
|
||||
}));
|
||||
|
||||
try {
|
||||
setShareStatus("Saving build…");
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/products/me/builds`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description: null,
|
||||
isPublic: false,
|
||||
items,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "");
|
||||
throw new Error(`Save failed (${res.status}) ${txt}`);
|
||||
}
|
||||
|
||||
const saved = await res.json(); // BuildDto
|
||||
setShareStatus(`Saved! Build UUID: ${saved.uuid}`);
|
||||
} catch (e: any) {
|
||||
setShareStatus(e?.message ?? "Save failed");
|
||||
} finally {
|
||||
window.setTimeout(() => setShareStatus(null), 4500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (selectedParts.length === 0) {
|
||||
setShareStatus("Add a few parts before sharing your build.");
|
||||
@@ -680,20 +771,6 @@ export default function GunbuilderPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-col items-stretch gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/builder/build",
|
||||
query: platform ? { platform } : {},
|
||||
}}
|
||||
className={`w-full sm:w-auto rounded-md border border-amber-400/60 bg-amber-400/10 px-3 py-2 text-sm font-medium text-amber-200 hover:bg-amber-400/15 text-center transition-colors ${
|
||||
selectedParts.length === 0
|
||||
? "opacity-40 cursor-not-allowed pointer-events-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Build Summary
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShare}
|
||||
@@ -724,6 +801,20 @@ export default function GunbuilderPage() {
|
||||
>
|
||||
Clear Build
|
||||
</button>
|
||||
|
||||
{/* Save Build */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveToAccount}
|
||||
disabled={selectedParts.length === 0}
|
||||
className={`w-full sm:w-auto rounded-md px-3 py-2 text-sm font-semibold transition-colors border ${
|
||||
selectedParts.length === 0
|
||||
? "bg-zinc-900/80 text-zinc-600 border-zinc-800 cursor-not-allowed"
|
||||
: "bg-amber-400 text-black border-amber-300 hover:bg-amber-300"
|
||||
}`}
|
||||
>
|
||||
Save Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -877,14 +968,12 @@ export default function GunbuilderPage() {
|
||||
|
||||
<tbody>
|
||||
{groupCategories.map((category) => {
|
||||
const categoryParts =
|
||||
partsByCategory[category.id] ?? [];
|
||||
const categoryParts = partsByCategory[category.id] ?? [];
|
||||
const selectedPartId = build[category.id];
|
||||
|
||||
const selectedPart = selectedPartId
|
||||
? categoryParts.find(
|
||||
(p) => p.id === selectedPartId
|
||||
) ?? parts.find((p) => p.id === selectedPartId)
|
||||
? categoryParts.find((p) => p.id === selectedPartId) ??
|
||||
parts.find((p) => p.id === selectedPartId)
|
||||
: undefined;
|
||||
|
||||
const hasParts = categoryParts.length > 0;
|
||||
@@ -1031,4 +1120,4 @@ export default function GunbuilderPage() {
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user