Files
GearBox/src/client/routes/setups/$setupId.tsx
Jean-Luc Makiola 7c3740fc72 refactor: replace remaining emojis with Lucide icons
Replace all raw emoji characters in dashboard cards, empty states,
and onboarding wizard with LucideIcon components for visual consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:47:50 +01:00

277 lines
7.8 KiB
TypeScript

import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { CategoryHeader } from "../../components/CategoryHeader";
import { ItemCard } from "../../components/ItemCard";
import { ItemPicker } from "../../components/ItemPicker";
import {
useDeleteSetup,
useRemoveSetupItem,
useSetup,
} from "../../hooks/useSetups";
import { formatPrice, formatWeight } from "../../lib/formatters";
import { LucideIcon } from "../../lib/iconData";
export const Route = createFileRoute("/setups/$setupId")({
component: SetupDetailPage,
});
function SetupDetailPage() {
const { setupId } = Route.useParams();
const navigate = useNavigate();
const numericId = Number(setupId);
const { data: setup, isLoading } = useSetup(numericId);
const deleteSetup = useDeleteSetup();
const removeItem = useRemoveSetupItem(numericId);
const [pickerOpen, setPickerOpen] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
if (isLoading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-48" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-40 bg-gray-200 rounded-xl" />
))}
</div>
</div>
</div>
);
}
if (!setup) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<p className="text-gray-500">Setup not found.</p>
</div>
);
}
// Compute totals from items
const totalWeight = setup.items.reduce(
(sum, item) => sum + (item.weightGrams ?? 0),
0,
);
const totalCost = setup.items.reduce(
(sum, item) => sum + (item.priceCents ?? 0),
0,
);
const itemCount = setup.items.length;
const currentItemIds = setup.items.map((item) => item.id);
// Group items by category
const groupedItems = new Map<
number,
{
items: typeof setup.items;
categoryName: string;
categoryIcon: string;
}
>();
for (const item of setup.items) {
const group = groupedItems.get(item.categoryId);
if (group) {
group.items.push(item);
} else {
groupedItems.set(item.categoryId, {
items: [item],
categoryName: item.categoryName,
categoryIcon: item.categoryIcon,
});
}
}
function handleDelete() {
deleteSetup.mutate(numericId, {
onSuccess: () => navigate({ to: "/setups" }),
});
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Setup-specific sticky bar */}
<div className="sticky top-14 z-[9] bg-gray-50 border-b border-gray-100 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-12">
<h2 className="text-base font-semibold text-gray-900 truncate">
{setup.name}
</h2>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>
<span className="font-medium text-gray-700">{itemCount}</span>{" "}
{itemCount === 1 ? "item" : "items"}
</span>
<span>
<span className="font-medium text-gray-700">
{formatWeight(totalWeight)}
</span>{" "}
total
</span>
<span>
<span className="font-medium text-gray-700">
{formatPrice(totalCost)}
</span>{" "}
cost
</span>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3 py-4">
<button
type="button"
onClick={() => setPickerOpen(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add Items
</button>
<button
type="button"
onClick={() => setConfirmDelete(true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-600 bg-red-50 hover:bg-red-100 rounded-lg transition-colors"
>
Delete Setup
</button>
</div>
{/* Empty state */}
{itemCount === 0 && (
<div className="py-16 text-center">
<div className="max-w-md mx-auto">
<div className="mb-4">
<LucideIcon
name="package"
size={48}
className="text-gray-400 mx-auto"
/>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
No items in this setup
</h2>
<p className="text-sm text-gray-500 mb-6">
Add items from your collection to build this loadout.
</p>
<button
type="button"
onClick={() => setPickerOpen(true)}
className="inline-flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
Add Items
</button>
</div>
</div>
)}
{/* Items grouped by category */}
{itemCount > 0 && (
<div className="pb-6">
{Array.from(groupedItems.entries()).map(
([
categoryId,
{ items: categoryItems, categoryName, categoryIcon },
]) => {
const catWeight = categoryItems.reduce(
(sum, item) => sum + (item.weightGrams ?? 0),
0,
);
const catCost = categoryItems.reduce(
(sum, item) => sum + (item.priceCents ?? 0),
0,
);
return (
<div key={categoryId} className="mb-8">
<CategoryHeader
categoryId={categoryId}
name={categoryName}
icon={categoryIcon}
totalWeight={catWeight}
totalCost={catCost}
itemCount={categoryItems.length}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{categoryItems.map((item) => (
<ItemCard
key={item.id}
id={item.id}
name={item.name}
weightGrams={item.weightGrams}
priceCents={item.priceCents}
categoryName={categoryName}
categoryIcon={categoryIcon}
imageFilename={item.imageFilename}
productUrl={item.productUrl}
onRemove={() => removeItem.mutate(item.id)}
/>
))}
</div>
</div>
);
},
)}
</div>
)}
{/* Item Picker */}
<ItemPicker
setupId={numericId}
currentItemIds={currentItemIds}
isOpen={pickerOpen}
onClose={() => setPickerOpen(false)}
/>
{/* Delete Confirmation Dialog */}
{confirmDelete && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/30"
onClick={() => setConfirmDelete(false)}
/>
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Delete Setup
</h3>
<p className="text-sm text-gray-600 mb-6">
Are you sure you want to delete{" "}
<span className="font-medium">{setup.name}</span>? This will not
remove items from your collection.
</p>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setConfirmDelete(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={handleDelete}
disabled={deleteSetup.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 rounded-lg transition-colors"
>
{deleteSetup.isPending ? "Deleting..." : "Delete"}
</button>
</div>
</div>
</div>
)}
</div>
);
}