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>
This commit is contained in:
@@ -1,268 +1,276 @@
|
||||
import { useState } from "react";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
useSetup,
|
||||
useDeleteSetup,
|
||||
useRemoveSetupItem,
|
||||
} from "../../hooks/useSetups";
|
||||
import { useState } from "react";
|
||||
import { CategoryHeader } from "../../components/CategoryHeader";
|
||||
import { ItemCard } from "../../components/ItemCard";
|
||||
import { ItemPicker } from "../../components/ItemPicker";
|
||||
import { formatWeight, formatPrice } from "../../lib/formatters";
|
||||
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,
|
||||
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 { 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);
|
||||
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 (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>
|
||||
);
|
||||
}
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
>();
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
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" }),
|
||||
});
|
||||
}
|
||||
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>
|
||||
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>
|
||||
{/* 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="text-5xl mb-4">📦</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>
|
||||
)}
|
||||
{/* 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}
|
||||
onRemove={() => removeItem.mutate(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</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)}
|
||||
/>
|
||||
{/* 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>
|
||||
);
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,86 +1,93 @@
|
||||
import { useState } from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useSetups, useCreateSetup } from "../../hooks/useSetups";
|
||||
import { useState } from "react";
|
||||
import { SetupCard } from "../../components/SetupCard";
|
||||
import { useCreateSetup, useSetups } from "../../hooks/useSetups";
|
||||
import { LucideIcon } from "../../lib/iconData";
|
||||
|
||||
export const Route = createFileRoute("/setups/")({
|
||||
component: SetupsListPage,
|
||||
component: SetupsListPage,
|
||||
});
|
||||
|
||||
function SetupsListPage() {
|
||||
const [newSetupName, setNewSetupName] = useState("");
|
||||
const { data: setups, isLoading } = useSetups();
|
||||
const createSetup = useCreateSetup();
|
||||
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("") },
|
||||
);
|
||||
}
|
||||
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>
|
||||
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>
|
||||
)}
|
||||
{/* 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>
|
||||
)}
|
||||
{/* Empty state */}
|
||||
{!isLoading && (!setups || setups.length === 0) && (
|
||||
<div className="py-16 text-center">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="mb-4">
|
||||
<LucideIcon
|
||||
name="tent"
|
||||
size={48}
|
||||
className="text-gray-400 mx-auto"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user