Files
GearBox/src/client/components/ItemPicker.tsx
Jean-Luc Makiola 9647f5759d feat: redesign weight summary legend and add currency selector
Redesign WeightSummaryCard stats from a disconnected 4-column grid to a
compact legend-style list with color dots, percentages, and a divider
before the total row. Switch chart and legend colors to a neutral gray
palette.

Add a currency selector to settings (USD, EUR, GBP, JPY, CAD, AUD) that
changes the displayed symbol across the app. This is visual only — no
value conversion is performed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:33:07 +01:00

159 lines
4.3 KiB
TypeScript

import { useEffect, useState } from "react";
import { useCurrency } from "../hooks/useCurrency";
import { useItems } from "../hooks/useItems";
import { useSyncSetupItems } from "../hooks/useSetups";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
import { SlideOutPanel } from "./SlideOutPanel";
interface ItemPickerProps {
setupId: number;
currentItemIds: number[];
isOpen: boolean;
onClose: () => void;
}
export function ItemPicker({
setupId,
currentItemIds,
isOpen,
onClose,
}: ItemPickerProps) {
const { data: items } = useItems();
const syncItems = useSyncSetupItems(setupId);
const unit = useWeightUnit();
const currency = useCurrency();
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
// Reset selected IDs when panel opens
useEffect(() => {
if (isOpen) {
setSelectedIds(new Set(currentItemIds));
}
}, [isOpen, currentItemIds]);
function handleToggle(itemId: number) {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(itemId)) {
next.delete(itemId);
} else {
next.add(itemId);
}
return next;
});
}
function handleDone() {
syncItems.mutate(Array.from(selectedIds), {
onSuccess: () => onClose(),
});
}
// Group items by category
const grouped = new Map<
number,
{
categoryName: string;
categoryIcon: string;
items: NonNullable<typeof items>;
}
>();
if (items) {
for (const item of items) {
const group = grouped.get(item.categoryId);
if (group) {
group.items.push(item);
} else {
grouped.set(item.categoryId, {
categoryName: item.categoryName,
categoryIcon: item.categoryIcon,
items: [item],
});
}
}
}
return (
<SlideOutPanel isOpen={isOpen} onClose={onClose} title="Select Items">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto -mx-6 px-6">
{!items || items.length === 0 ? (
<div className="py-8 text-center">
<p className="text-sm text-gray-500">
No items in your collection yet.
</p>
</div>
) : (
Array.from(grouped.entries()).map(
([
categoryId,
{ categoryName, categoryIcon, items: catItems },
]) => (
<div key={categoryId} className="mb-4">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
<LucideIcon
name={categoryIcon}
size={16}
className="inline-block mr-1 text-gray-500"
/>{" "}
{categoryName}
</h3>
<div className="space-y-1">
{catItems.map((item) => (
<label
key={item.id}
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
>
<input
type="checkbox"
checked={selectedIds.has(item.id)}
onChange={() => handleToggle(item.id)}
className="rounded border-gray-300 text-gray-600 focus:ring-gray-400"
/>
<span className="flex-1 text-sm text-gray-900 truncate">
{item.name}
</span>
<span className="text-xs text-gray-400 shrink-0">
{item.weightGrams != null &&
formatWeight(item.weightGrams, unit)}
{item.weightGrams != null &&
item.priceCents != null &&
" · "}
{item.priceCents != null &&
formatPrice(item.priceCents, currency)}
</span>
</label>
))}
</div>
</div>
),
)
)}
</div>
{/* Action buttons */}
<div className="flex gap-3 pt-4 border-t border-gray-100 -mx-6 px-6 pb-2">
<button
type="button"
onClick={onClose}
className="flex-1 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={handleDone}
disabled={syncItems.isPending}
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 disabled:opacity-50 rounded-lg transition-colors"
>
{syncItems.isPending ? "Saving..." : "Done"}
</button>
</div>
</div>
</SlideOutPanel>
);
}