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>
159 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|