From 9647f5759d019777627ae3749d67c7e700d88ce1 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 20:33:07 +0100 Subject: [PATCH] feat: redesign weight summary legend and add currency selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/client/components/CandidateCard.tsx | 97 +++++++++---- src/client/components/CategoryHeader.tsx | 4 +- src/client/components/ItemCard.tsx | 15 +- src/client/components/ItemPicker.tsx | 4 +- src/client/components/SetupCard.tsx | 4 +- src/client/components/ThreadCard.tsx | 9 +- src/client/components/WeightSummaryCard.tsx | 88 +++++++----- src/client/hooks/useCurrency.ts | 12 ++ src/client/lib/formatters.ts | 22 ++- src/client/routeTree.gen.ts | 33 ++++- src/client/routes/collection/index.tsx | 150 +++++++++++++++----- src/client/routes/index.tsx | 4 +- src/client/routes/settings.tsx | 104 ++++++++++++++ src/client/routes/setups/$setupId.tsx | 69 ++++----- 14 files changed, 470 insertions(+), 145 deletions(-) create mode 100644 src/client/hooks/useCurrency.ts create mode 100644 src/client/routes/settings.tsx diff --git a/src/client/components/CandidateCard.tsx b/src/client/components/CandidateCard.tsx index 254d483..e1b198f 100644 --- a/src/client/components/CandidateCard.tsx +++ b/src/client/components/CandidateCard.tsx @@ -1,3 +1,4 @@ +import { useCurrency } from "../hooks/useCurrency"; import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; @@ -34,6 +35,7 @@ export function CandidateCard({ onStatusChange, }: CandidateCardProps) { const unit = useWeightUnit(); + const currency = useCurrency(); const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const openConfirmDeleteCandidate = useUIStore( (s) => s.openConfirmDeleteCandidate, @@ -42,14 +44,74 @@ export function CandidateCard({ const openExternalLink = useUIStore((s) => s.openExternalLink); return ( -
+ - - {isActive && ( - - )} -
- + ); } diff --git a/src/client/components/CategoryHeader.tsx b/src/client/components/CategoryHeader.tsx index 33ee346..1556895 100644 --- a/src/client/components/CategoryHeader.tsx +++ b/src/client/components/CategoryHeader.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories"; +import { useCurrency } from "../hooks/useCurrency"; import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; @@ -23,6 +24,7 @@ export function CategoryHeader({ itemCount, }: CategoryHeaderProps) { const unit = useWeightUnit(); + const currency = useCurrency(); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(name); const [editIcon, setEditIcon] = useState(icon); @@ -87,7 +89,7 @@ export function CategoryHeader({

{name}

{itemCount} {itemCount === 1 ? "item" : "items"} ·{" "} - {formatWeight(totalWeight, unit)} · {formatPrice(totalCost)} + {formatWeight(totalWeight, unit)} · {formatPrice(totalCost, currency)} {!isUncategorized && (
diff --git a/src/client/components/ItemCard.tsx b/src/client/components/ItemCard.tsx index d11bd02..6c634e7 100644 --- a/src/client/components/ItemCard.tsx +++ b/src/client/components/ItemCard.tsx @@ -1,7 +1,9 @@ +import { useCurrency } from "../hooks/useCurrency"; import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; import { useUIStore } from "../stores/uiStore"; +import { ClassificationBadge } from "./ClassificationBadge"; interface ItemCardProps { id: number; @@ -13,6 +15,8 @@ interface ItemCardProps { imageFilename: string | null; productUrl?: string | null; onRemove?: () => void; + classification?: string; + onClassificationCycle?: () => void; } export function ItemCard({ @@ -25,8 +29,11 @@ export function ItemCard({ imageFilename, productUrl, onRemove, + classification, + onClassificationCycle, }: ItemCardProps) { const unit = useWeightUnit(); + const currency = useCurrency(); const openEditPanel = useUIStore((s) => s.openEditPanel); const openExternalLink = useUIStore((s) => s.openExternalLink); @@ -129,7 +136,7 @@ export function ItemCard({ )} {priceCents != null && ( - {formatPrice(priceCents)} + {formatPrice(priceCents, currency)} )} @@ -140,6 +147,12 @@ export function ItemCard({ />{" "} {categoryName} + {classification && onClassificationCycle && ( + + )}
diff --git a/src/client/components/ItemPicker.tsx b/src/client/components/ItemPicker.tsx index d1b4267..2a84493 100644 --- a/src/client/components/ItemPicker.tsx +++ b/src/client/components/ItemPicker.tsx @@ -1,4 +1,5 @@ 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"; @@ -22,6 +23,7 @@ export function ItemPicker({ const { data: items } = useItems(); const syncItems = useSyncSetupItems(setupId); const unit = useWeightUnit(); + const currency = useCurrency(); const [selectedIds, setSelectedIds] = useState>(new Set()); // Reset selected IDs when panel opens @@ -121,7 +123,7 @@ export function ItemPicker({ item.priceCents != null && " · "} {item.priceCents != null && - formatPrice(item.priceCents)} + formatPrice(item.priceCents, currency)} ))} diff --git a/src/client/components/SetupCard.tsx b/src/client/components/SetupCard.tsx index 8dd87da..7ca8440 100644 --- a/src/client/components/SetupCard.tsx +++ b/src/client/components/SetupCard.tsx @@ -1,4 +1,5 @@ import { Link } from "@tanstack/react-router"; +import { useCurrency } from "../hooks/useCurrency"; import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; @@ -18,6 +19,7 @@ export function SetupCard({ totalCost, }: SetupCardProps) { const unit = useWeightUnit(); + const currency = useCurrency(); return ( - {formatPrice(totalCost)} + {formatPrice(totalCost, currency)} diff --git a/src/client/components/ThreadCard.tsx b/src/client/components/ThreadCard.tsx index a109753..8f583ab 100644 --- a/src/client/components/ThreadCard.tsx +++ b/src/client/components/ThreadCard.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "@tanstack/react-router"; +import { useCurrency } from "../hooks/useCurrency"; import { formatPrice } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; @@ -22,10 +23,11 @@ function formatDate(iso: string): string { function formatPriceRange( min: number | null, max: number | null, + currency: Parameters[1], ): string | null { if (min == null && max == null) return null; - if (min === max) return formatPrice(min); - return `${formatPrice(min)} - ${formatPrice(max)}`; + if (min === max) return formatPrice(min, currency); + return `${formatPrice(min, currency)} - ${formatPrice(max, currency)}`; } export function ThreadCard({ @@ -40,9 +42,10 @@ export function ThreadCard({ categoryIcon, }: ThreadCardProps) { const navigate = useNavigate(); + const currency = useCurrency(); const isResolved = status === "resolved"; - const priceRange = formatPriceRange(minPriceCents, maxPriceCents); + const priceRange = formatPriceRange(minPriceCents, maxPriceCents, currency); return ( + ))} + + + +
+ +
+
+

Currency

+

+ Changes the currency symbol displayed. This does not convert + values. +

+
+
+ {CURRENCIES.map((c) => ( + + ))} +
+
+
+ + ); +} diff --git a/src/client/routes/setups/$setupId.tsx b/src/client/routes/setups/$setupId.tsx index fa06048..2602716 100644 --- a/src/client/routes/setups/$setupId.tsx +++ b/src/client/routes/setups/$setupId.tsx @@ -1,7 +1,6 @@ -import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { CategoryHeader } from "../../components/CategoryHeader"; -import { ClassificationBadge } from "../../components/ClassificationBadge"; import { ItemCard } from "../../components/ItemCard"; import { ItemPicker } from "../../components/ItemPicker"; import { WeightSummaryCard } from "../../components/WeightSummaryCard"; @@ -11,6 +10,7 @@ import { useSetup, useUpdateItemClassification, } from "../../hooks/useSetups"; +import { useCurrency } from "../../hooks/useCurrency"; import { useWeightUnit } from "../../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../../lib/formatters"; import { LucideIcon } from "../../lib/iconData"; @@ -22,6 +22,7 @@ export const Route = createFileRoute("/setups/$setupId")({ function SetupDetailPage() { const { setupId } = Route.useParams(); const unit = useWeightUnit(); + const currency = useCurrency(); const navigate = useNavigate(); const numericId = Number(setupId); const { data: setup, isLoading } = useSetup(numericId); @@ -107,9 +108,18 @@ function SetupDetailPage() { {/* Setup-specific sticky bar */}
-

- {setup.name} -

+
+ + ← + +

+ {setup.name} +

+
{itemCount}{" "} @@ -123,7 +133,7 @@ function SetupDetailPage() { - {formatPrice(totalCost)} + {formatPrice(totalCost, currency)} {" "} cost @@ -219,32 +229,27 @@ function SetupDetailPage() { />
{categoryItems.map((item) => ( -
- removeItem.mutate(item.id)} - /> -
- - updateClassification.mutate({ - itemId: item.id, - classification: nextClassification( - item.classification, - ), - }) - } - /> -
-
+ removeItem.mutate(item.id)} + classification={item.classification} + onClassificationCycle={() => + updateClassification.mutate({ + itemId: item.id, + classification: nextClassification( + item.classification, + ), + }) + } + /> ))}