cleaning up UI
This commit is contained in:
+378
-194
@@ -5,7 +5,6 @@ import Link from "next/link";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||
import { CategoryColumn } from "@/components/CategoryColumn";
|
||||
import { PART_ROLE_TO_CATEGORY } from "@/data/partRoleMappings";
|
||||
|
||||
type GunbuilderProductFromApi = {
|
||||
@@ -22,16 +21,11 @@ type GunbuilderProductFromApi = {
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
|
||||
type BuildState = Partial<Record<CategoryId, string>>; // categoryId -> partId
|
||||
|
||||
const STORAGE_KEY = "gunbuilder-build-state";
|
||||
|
||||
// Group categories into logical sections.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// These IDs must match the ids you have in CATEGORIES.
|
||||
// Anything that doesn't exist will just be skipped.
|
||||
// Category groups for the main grid
|
||||
const CATEGORY_GROUPS: {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -74,6 +68,13 @@ const CATEGORY_GROUPS: {
|
||||
"optic",
|
||||
],
|
||||
},
|
||||
// Optional: future group for accessories/furniture if needed
|
||||
// {
|
||||
// id: "accessories-group",
|
||||
// label: "Accessories & Furniture",
|
||||
// description: "Optics, lights, slings, and other supporting gear.",
|
||||
// categoryIds: ["optic", "sights", ...] as CategoryId[],
|
||||
// },
|
||||
];
|
||||
|
||||
export default function GunbuilderPage() {
|
||||
@@ -95,7 +96,8 @@ export default function GunbuilderPage() {
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const [layoutMode, setLayoutMode] = useState<"cards" | "list">("cards");
|
||||
const [shareStatus, setShareStatus] = useState<string | null>(null);
|
||||
const [shareUrl, setShareUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -203,6 +205,28 @@ export default function GunbuilderPage() {
|
||||
[selectedParts],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// If no parts selected, clear the share URL
|
||||
if (Object.keys(build).length === 0) {
|
||||
setShareUrl("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.stringify(build);
|
||||
const encoded = window.btoa(payload);
|
||||
const origin = window.location?.origin ?? "";
|
||||
const url = `${origin}/gunbuilder/build?build=${encodeURIComponent(
|
||||
encoded,
|
||||
)}`;
|
||||
setShareUrl(url);
|
||||
} catch {
|
||||
setShareUrl("");
|
||||
}
|
||||
}, [build]);
|
||||
|
||||
const handleSelectPart = (categoryId: CategoryId, partId: string) => {
|
||||
setBuild((prev) => ({
|
||||
...prev,
|
||||
@@ -219,6 +243,88 @@ export default function GunbuilderPage() {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleShare = async () => {
|
||||
if (selectedParts.length === 0) {
|
||||
setShareStatus("Add a few parts before sharing your build.");
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("Shadow Standard — The Armory Build");
|
||||
lines.push("");
|
||||
lines.push(`Total: $${totalPrice.toFixed(2)}`);
|
||||
lines.push("");
|
||||
|
||||
summaryCategoryOrder.forEach((categoryId) => {
|
||||
const cat = CATEGORIES.find((c) => c.id === categoryId);
|
||||
if (!cat) return;
|
||||
|
||||
const selectedPartId = build[cat.id];
|
||||
const part = parts.find(
|
||||
(p) => p.id === selectedPartId && p.categoryId === cat.id,
|
||||
);
|
||||
|
||||
if (part) {
|
||||
lines.push(
|
||||
`${cat.name}: ${part.brand} — ${part.name} ($${part.price.toFixed(
|
||||
2,
|
||||
)})`,
|
||||
);
|
||||
} else {
|
||||
lines.push(`${cat.name}: [Not selected]`);
|
||||
}
|
||||
});
|
||||
|
||||
const text = lines.join("\n");
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setShareStatus("Build summary copied to clipboard.");
|
||||
} catch {
|
||||
setShareStatus(
|
||||
"Could not access clipboard. You can copy from the build summary page.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (!shareUrl) {
|
||||
setShareStatus("No shareable link available yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
setShareStatus("Shareable link copied to clipboard.");
|
||||
} catch {
|
||||
setShareStatus("Could not copy link to clipboard.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNativeShare = async () => {
|
||||
if (!shareUrl) {
|
||||
setShareStatus("No shareable link available yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: "Shadow Standard — The Armory Build",
|
||||
text: "Check out my Shadow Standard Armory build.",
|
||||
url: shareUrl,
|
||||
});
|
||||
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.");
|
||||
}
|
||||
} else {
|
||||
// Fallback to copying the link
|
||||
await handleCopyLink();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
<div className="border-b border-amber-500/20 bg-amber-500/5">
|
||||
@@ -239,7 +345,7 @@ export default function GunbuilderPage() {
|
||||
</div>
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
||||
<header className="mb-6">
|
||||
<div>
|
||||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Shadow Standard
|
||||
@@ -254,189 +360,30 @@ export default function GunbuilderPage() {
|
||||
locally so you can come back and refine it anytime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 md:mt-0">
|
||||
<div className="text-xs text-zinc-400">Current build total</div>
|
||||
<div className="text-2xl font-semibold text-amber-300">
|
||||
${totalPrice.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{selectedParts.length} / {CATEGORIES.length} categories filled
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Layout */}
|
||||
<div className="grid gap-4 lg:grid-cols-[3fr,1.25fr]">
|
||||
{/* Categories/parts */}
|
||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs text-zinc-500">
|
||||
Choose one part per category. Switch layouts to browse by cards or a tighter list view.
|
||||
</p>
|
||||
<div className="inline-flex items-center rounded-md border border-zinc-700 bg-zinc-900/70 text-[0.7rem]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLayoutMode("cards")}
|
||||
className={`px-2.5 py-1 rounded-l-md border-r border-zinc-700 transition-colors ${
|
||||
layoutMode === "cards"
|
||||
? "bg-amber-500/20 text-amber-200"
|
||||
: "text-zinc-400 hover:bg-zinc-800"
|
||||
}`}
|
||||
>
|
||||
Card view
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLayoutMode("list")}
|
||||
className={`px-2.5 py-1 rounded-r-md transition-colors ${
|
||||
layoutMode === "list"
|
||||
? "bg-amber-500/20 text-amber-200"
|
||||
: "text-zinc-400 hover:bg-zinc-800"
|
||||
}`}
|
||||
>
|
||||
List view
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{loading && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
Loading parts from backend…
|
||||
</p>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<p className="text-sm text-red-400">
|
||||
{error} — check that the Ballistic API is running and CORS is
|
||||
configured.
|
||||
</p>
|
||||
)}
|
||||
{!loading && !error && (
|
||||
<div className="space-y-6">
|
||||
{CATEGORY_GROUPS.map((group) => {
|
||||
const groupCategories = CATEGORIES.filter((c) =>
|
||||
group.categoryIds.includes(c.id as CategoryId),
|
||||
);
|
||||
|
||||
if (groupCategories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={group.id} className="space-y-3">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-zinc-100">
|
||||
{group.label}
|
||||
</h2>
|
||||
{group.description && (
|
||||
<p className="text-xs text-zinc-500 mt-1 max-w-xl">
|
||||
{group.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{layoutMode === "cards" ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{groupCategories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60"
|
||||
>
|
||||
<CategoryColumn
|
||||
category={category}
|
||||
parts={partsByCategory[category.id]}
|
||||
selectedPartId={build[category.id]}
|
||||
onSelectPart={(partId) =>
|
||||
handleSelectPart(category.id, partId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{groupCategories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60 flex flex-col md:flex-row md:items-start md:justify-between gap-3"
|
||||
>
|
||||
<CategoryColumn
|
||||
category={category}
|
||||
parts={partsByCategory[category.id]}
|
||||
selectedPartId={build[category.id]}
|
||||
onSelectPart={(partId) =>
|
||||
handleSelectPart(category.id, partId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Build Summary */}
|
||||
<aside className="rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4 flex flex-col">
|
||||
<h2 className="text-xs font-semibold tracking-[0.16em] uppercase text-zinc-400 mb-3">
|
||||
Current Build
|
||||
{/* 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 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-[0.16em] text-amber-300">
|
||||
My Build Breakdown
|
||||
</h2>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-1">
|
||||
{selectedParts.length === 0 && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
Select a part from each category to start your build.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{summaryCategoryOrder.map((categoryId) => {
|
||||
const cat = CATEGORIES.find((c) => c.id === categoryId);
|
||||
if (!cat) return null;
|
||||
|
||||
const selectedPartId = build[cat.id];
|
||||
const part = parts.find(
|
||||
(p) =>
|
||||
p.id === selectedPartId && p.categoryId === cat.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={cat.id}>
|
||||
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500">
|
||||
{cat.name}
|
||||
</div>
|
||||
{part ? (
|
||||
<div className="mt-1 flex justify-between items-center gap-2">
|
||||
<div className="text-sm text-zinc-50">
|
||||
<span className="text-zinc-400">
|
||||
{part.brand}{" "}
|
||||
</span>
|
||||
{part.name}
|
||||
</div>
|
||||
<div className="text-xs font-semibold text-amber-300 whitespace-nowrap">
|
||||
${part.price.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 text-xs text-zinc-600">
|
||||
Not selected
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-400">Current build total</div>
|
||||
<div className="text-2xl font-semibold text-amber-300">
|
||||
${totalPrice.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 mt-1">
|
||||
{selectedParts.length} / {CATEGORIES.length} categories filled
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-4 border-t border-zinc-800 pt-3 space-y-2">
|
||||
{/* Primary actions now live under the total */}
|
||||
<div className="mt-2 flex flex-col items-stretch gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Link
|
||||
href="/gunbuilder/build"
|
||||
className={`block w-full 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 ${
|
||||
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"
|
||||
: ""
|
||||
@@ -444,6 +391,18 @@ export default function GunbuilderPage() {
|
||||
>
|
||||
Build Summary
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShare}
|
||||
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-zinc-900/80 text-zinc-200 border-zinc-700 hover:bg-zinc-800"
|
||||
}`}
|
||||
disabled={selectedParts.length === 0}
|
||||
>
|
||||
Copy Build Summary
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -453,20 +412,245 @@ export default function GunbuilderPage() {
|
||||
}
|
||||
}}
|
||||
disabled={selectedParts.length === 0}
|
||||
className={`w-full 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"
|
||||
: ""
|
||||
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" : ""
|
||||
}`}
|
||||
>
|
||||
Clear Build
|
||||
</button>
|
||||
<p className="mt-2 text-[0.7rem] text-zinc-500">
|
||||
View your build breakdown and share it with others.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Share Your Build */}
|
||||
<div className="flex flex-col gap-3 md:items-end md:text-right md:flex-1">
|
||||
{selectedParts.length > 0 && shareUrl && (
|
||||
<div className="w-full md:w-auto">
|
||||
<h3 className="text-[0.7rem] font-semibold uppercase tracking-[0.16em] text-zinc-400">
|
||||
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.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
<div className="text-[0.65rem] uppercase tracking-[0.16em] text-zinc-500 mb-1">
|
||||
Shareable URL
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={shareUrl}
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-950/80 px-2 py-1.5 text-[0.7rem] text-zinc-200 font-mono outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 md:flex-none md:mt-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNativeShare}
|
||||
className="flex-1 md:flex-none rounded-md bg-zinc-900/80 px-3 py-1.5 text-[0.75rem] font-semibold text-zinc-100 border border-zinc-700 hover:bg-zinc-800 hover:border-zinc-500 transition-colors"
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyLink}
|
||||
className="flex-1 md:flex-none rounded-md bg-amber-400 px-3 py-1.5 text-[0.75rem] font-semibold text-black hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shareStatus && (
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
{shareStatus}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Layout */}
|
||||
<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.
|
||||
</p>
|
||||
|
||||
{loading && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
Loading parts from backend…
|
||||
</p>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<p className="text-sm text-red-400">
|
||||
{error} — check that the Ballistic API is running and CORS is
|
||||
configured.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="space-y-6">
|
||||
{CATEGORY_GROUPS.map((group) => {
|
||||
const groupCategories = CATEGORIES.filter((c) =>
|
||||
group.categoryIds.includes(c.id as CategoryId),
|
||||
);
|
||||
|
||||
if (groupCategories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={group.id} className="space-y-3">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-zinc-100">
|
||||
{group.label}
|
||||
</h2>
|
||||
{group.description && (
|
||||
<p className="text-xs text-zinc-500 mt-1 max-w-xl">
|
||||
{group.description}
|
||||
</p>
|
||||
)}
|
||||
</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>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-900/60">
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Component / Part Type
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Brand
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Price
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Sale Price
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Caliber
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Buy / Choose
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupCategories.map((category) => {
|
||||
const categoryParts =
|
||||
partsByCategory[category.id] ?? [];
|
||||
const selectedPartId = build[category.id];
|
||||
const selectedPart = categoryParts.find(
|
||||
(p) => p.id === selectedPartId,
|
||||
);
|
||||
const hasParts = categoryParts.length > 0;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={category.id}
|
||||
className="border-t border-zinc-900 hover:bg-zinc-900/60 transition-colors"
|
||||
>
|
||||
{/* Component / Part Type */}
|
||||
<td className="px-3 py-2 align-top">
|
||||
<div className="font-medium text-zinc-100">
|
||||
{category.name}
|
||||
</div>
|
||||
{selectedPart ? (
|
||||
<div className="text-[0.7rem] text-zinc-500 line-clamp-1">
|
||||
{selectedPart.name}
|
||||
</div>
|
||||
) : hasParts ? (
|
||||
<div className="text-[0.7rem] text-zinc-600">
|
||||
No part selected
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[0.7rem] text-zinc-600">
|
||||
No parts available yet
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Brand */}
|
||||
<td className="px-3 py-2 align-top text-zinc-300">
|
||||
{selectedPart ? selectedPart.brand : "—"}
|
||||
</td>
|
||||
|
||||
{/* Price */}
|
||||
<td className="px-3 py-2 align-top text-right text-amber-300">
|
||||
{selectedPart
|
||||
? `$${selectedPart.price.toFixed(2)}`
|
||||
: "—"}
|
||||
</td>
|
||||
|
||||
{/* Sale Price (placeholder until we wire through sale/original) */}
|
||||
<td className="px-3 py-2 align-top text-right text-zinc-400">
|
||||
{/* TODO: wire in sale/original prices from backend */}
|
||||
{selectedPart ? "—" : "—"}
|
||||
</td>
|
||||
|
||||
{/* Caliber (placeholder for now) */}
|
||||
<td className="px-3 py-2 align-top text-zinc-400">
|
||||
{/* TODO: wire in caliber from ProductSummaryDto when available */}
|
||||
—
|
||||
</td>
|
||||
|
||||
{/* Buy / Choose */}
|
||||
<td className="px-3 py-2 align-top">
|
||||
<div className="flex justify-end gap-2">
|
||||
{selectedPart && selectedPart.url ? (
|
||||
<>
|
||||
<Link
|
||||
href={`/gunbuilder/${category.id}`}
|
||||
className="hidden md:inline-flex rounded-md border border-zinc-700 bg-zinc-900/70 px-2.5 py-1 text-[0.7rem] font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
>
|
||||
Change Part
|
||||
</Link>
|
||||
<a
|
||||
href={selectedPart.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex rounded-md bg-amber-400 px-2.5 py-1 text-[0.7rem] font-semibold text-black hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
Buy
|
||||
</a>
|
||||
</>
|
||||
) : hasParts ? (
|
||||
<Link
|
||||
href={`/gunbuilder/${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.
|
||||
}}
|
||||
>
|
||||
Choose a Part
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-[0.7rem] text-zinc-600">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* What's New */}
|
||||
<section className="mt-8 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4">
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@ export default function HomePage() {
|
||||
<main className="min-h-screen bg-black text-zinc-50 flex items-center justify-center px-4">
|
||||
<div className="max-w-xl text-center">
|
||||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Shadow Standard
|
||||
Shadow Standard Co.
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Gunbuilder Prototype
|
||||
The Build Bench
|
||||
</h1>
|
||||
<p className="mt-3 text-sm md:text-base text-zinc-400">
|
||||
This is a minimal Next.js + Tailwind prototype of the Gunbuilder
|
||||
@@ -20,7 +20,7 @@ export default function HomePage() {
|
||||
href="/gunbuilder"
|
||||
className="inline-flex items-center justify-center rounded-md border border-amber-400/70 bg-amber-400/10 px-4 py-2 text-sm font-medium text-amber-200 hover:bg-amber-400/15"
|
||||
>
|
||||
Open Gunbuilder
|
||||
Open The Build Bench
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user