Files
GearBox/src/client/routes/setups/index.tsx
Jean-Luc Makiola 67099551d0 feat(03-02): setup list page, detail page, and item picker
- Create SetupCard component with name, item count, weight, cost display
- Build setups list page with inline create form and grid layout
- Build setup detail page with category-grouped items and sticky totals bar
- Create ItemPicker component in SlideOutPanel with category-grouped checkboxes
- Add onRemove prop to ItemCard for non-destructive setup item removal
- Setup detail has delete confirmation dialog with collection safety note
- Empty states for both setup list and setup detail pages

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

87 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { useSetups, useCreateSetup } from "../../hooks/useSetups";
import { SetupCard } from "../../components/SetupCard";
export const Route = createFileRoute("/setups/")({
component: SetupsListPage,
});
function SetupsListPage() {
const [newSetupName, setNewSetupName] = useState("");
const { data: setups, isLoading } = useSetups();
const createSetup = useCreateSetup();
function handleCreateSetup(e: React.FormEvent) {
e.preventDefault();
const name = newSetupName.trim();
if (!name) return;
createSetup.mutate(
{ name },
{ onSuccess: () => setNewSetupName("") },
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Create setup form */}
<form onSubmit={handleCreateSetup} className="flex gap-2 mb-6">
<input
type="text"
value={newSetupName}
onChange={(e) => setNewSetupName(e.target.value)}
placeholder="New setup name..."
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
type="submit"
disabled={!newSetupName.trim() || createSetup.isPending}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
{createSetup.isPending ? "Creating..." : "Create"}
</button>
</form>
{/* Loading skeleton */}
{isLoading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2].map((i) => (
<div key={i} className="h-24 bg-gray-200 rounded-xl animate-pulse" />
))}
</div>
)}
{/* Empty state */}
{!isLoading && (!setups || setups.length === 0) && (
<div className="py-16 text-center">
<div className="max-w-md mx-auto">
<div className="text-5xl mb-4">🏕</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
No setups yet
</h2>
<p className="text-sm text-gray-500">
Create one to plan your loadout.
</p>
</div>
</div>
)}
{/* Setup grid */}
{!isLoading && setups && setups.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{setups.map((setup) => (
<SetupCard
key={setup.id}
id={setup.id}
name={setup.name}
itemCount={setup.itemCount}
totalWeight={setup.totalWeight}
totalCost={setup.totalCost}
/>
))}
</div>
)}
</div>
);
}