feat(07-02): add weight unit toggle and wire all formatWeight call sites

- Add segmented g/oz/lb/kg toggle to TotalsBar with settings persistence
- Pass unit parameter to all 8 formatWeight call sites across components and routes
- Import useWeightUnit hook in ItemCard, CandidateCard, CategoryHeader, SetupCard, ItemPicker, Dashboard, SetupDetail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 12:23:19 +01:00
parent 1b0b4d0368
commit faa437896f
8 changed files with 66 additions and 20 deletions

View File

@@ -1,3 +1,4 @@
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
@@ -27,6 +28,7 @@ export function CandidateCard({
threadId, threadId,
isActive, isActive,
}: CandidateCardProps) { }: CandidateCardProps) {
const unit = useWeightUnit();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
const openConfirmDeleteCandidate = useUIStore( const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate, (s) => s.openConfirmDeleteCandidate,
@@ -88,7 +90,7 @@ export function CandidateCard({
<div className="flex flex-wrap gap-1.5 mb-3"> <div className="flex flex-wrap gap-1.5 mb-3">
{weightGrams != null && ( {weightGrams != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
{formatWeight(weightGrams)} {formatWeight(weightGrams, unit)}
</span> </span>
)} )}
{priceCents != null && ( {priceCents != null && (

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories"; import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { IconPicker } from "./IconPicker"; import { IconPicker } from "./IconPicker";
@@ -21,6 +22,7 @@ export function CategoryHeader({
totalCost, totalCost,
itemCount, itemCount,
}: CategoryHeaderProps) { }: CategoryHeaderProps) {
const unit = useWeightUnit();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(name); const [editName, setEditName] = useState(name);
const [editIcon, setEditIcon] = useState(icon); const [editIcon, setEditIcon] = useState(icon);
@@ -85,7 +87,7 @@ export function CategoryHeader({
<h2 className="text-lg font-semibold text-gray-900">{name}</h2> <h2 className="text-lg font-semibold text-gray-900">{name}</h2>
<span className="text-sm text-gray-400"> <span className="text-sm text-gray-400">
{itemCount} {itemCount === 1 ? "item" : "items"} ·{" "} {itemCount} {itemCount === 1 ? "item" : "items"} ·{" "}
{formatWeight(totalWeight)} · {formatPrice(totalCost)} {formatWeight(totalWeight, unit)} · {formatPrice(totalCost)}
</span> </span>
{!isUncategorized && ( {!isUncategorized && (
<div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">

View File

@@ -1,3 +1,4 @@
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
@@ -25,6 +26,7 @@ export function ItemCard({
productUrl, productUrl,
onRemove, onRemove,
}: ItemCardProps) { }: ItemCardProps) {
const unit = useWeightUnit();
const openEditPanel = useUIStore((s) => s.openEditPanel); const openEditPanel = useUIStore((s) => s.openEditPanel);
const openExternalLink = useUIStore((s) => s.openExternalLink); const openExternalLink = useUIStore((s) => s.openExternalLink);
@@ -122,7 +124,7 @@ export function ItemCard({
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{weightGrams != null && ( {weightGrams != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
{formatWeight(weightGrams)} {formatWeight(weightGrams, unit)}
</span> </span>
)} )}
{priceCents != null && ( {priceCents != null && (

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useItems } from "../hooks/useItems"; import { useItems } from "../hooks/useItems";
import { useSyncSetupItems } from "../hooks/useSetups"; import { useSyncSetupItems } from "../hooks/useSetups";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { SlideOutPanel } from "./SlideOutPanel"; import { SlideOutPanel } from "./SlideOutPanel";
@@ -20,6 +21,7 @@ export function ItemPicker({
}: ItemPickerProps) { }: ItemPickerProps) {
const { data: items } = useItems(); const { data: items } = useItems();
const syncItems = useSyncSetupItems(setupId); const syncItems = useSyncSetupItems(setupId);
const unit = useWeightUnit();
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
// Reset selected IDs when panel opens // Reset selected IDs when panel opens
@@ -114,7 +116,7 @@ export function ItemPicker({
</span> </span>
<span className="text-xs text-gray-400 shrink-0"> <span className="text-xs text-gray-400 shrink-0">
{item.weightGrams != null && {item.weightGrams != null &&
formatWeight(item.weightGrams)} formatWeight(item.weightGrams, unit)}
{item.weightGrams != null && {item.weightGrams != null &&
item.priceCents != null && item.priceCents != null &&
" · "} " · "}

View File

@@ -1,4 +1,5 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
interface SetupCardProps { interface SetupCardProps {
@@ -16,6 +17,7 @@ export function SetupCard({
totalWeight, totalWeight,
totalCost, totalCost,
}: SetupCardProps) { }: SetupCardProps) {
const unit = useWeightUnit();
return ( return (
<Link <Link
to="/setups/$setupId" to="/setups/$setupId"
@@ -30,7 +32,7 @@ export function SetupCard({
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
{formatWeight(totalWeight)} {formatWeight(totalWeight, unit)}
</span> </span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{formatPrice(totalCost)} {formatPrice(totalCost)}

View File

@@ -1,8 +1,12 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { useUpdateSetting } from "../hooks/useSettings";
import { useTotals } from "../hooks/useTotals"; import { useTotals } from "../hooks/useTotals";
import { formatPrice, formatWeight } from "../lib/formatters"; import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight, type WeightUnit } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
interface TotalsBarProps { interface TotalsBarProps {
title?: string; title?: string;
stats?: Array<{ label: string; value: string }>; stats?: Array<{ label: string; value: string }>;
@@ -15,6 +19,8 @@ export function TotalsBar({
linkTo, linkTo,
}: TotalsBarProps) { }: TotalsBarProps) {
const { data } = useTotals(); const { data } = useTotals();
const unit = useWeightUnit();
const updateSetting = useUpdateSetting();
// When no stats provided, use global totals (backward compatible) // When no stats provided, use global totals (backward compatible)
const displayStats = const displayStats =
@@ -22,12 +28,15 @@ export function TotalsBar({
(data?.global (data?.global
? [ ? [
{ label: "items", value: String(data.global.itemCount) }, { label: "items", value: String(data.global.itemCount) },
{ label: "total", value: formatWeight(data.global.totalWeight) }, {
label: "total",
value: formatWeight(data.global.totalWeight, unit),
},
{ label: "spent", value: formatPrice(data.global.totalCost) }, { label: "spent", value: formatPrice(data.global.totalCost) },
] ]
: [ : [
{ label: "items", value: "0" }, { label: "items", value: "0" },
{ label: "total", value: formatWeight(null) }, { label: "total", value: formatWeight(null, unit) },
{ label: "spent", value: formatPrice(null) }, { label: "spent", value: formatPrice(null) },
]); ]);
@@ -57,6 +66,28 @@ export function TotalsBar({
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14"> <div className="flex items-center justify-between h-14">
{titleElement} {titleElement}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
{UNITS.map((u) => (
<button
key={u}
type="button"
onClick={() =>
updateSetting.mutate({
key: "weightUnit",
value: u,
})
}
className={`px-2 py-0.5 text-xs rounded-full transition-colors ${
unit === u
? "bg-white text-gray-700 shadow-sm font-medium"
: "text-gray-400 hover:text-gray-600"
}`}
>
{u}
</button>
))}
</div>
{showStats && ( {showStats && (
<div className="flex items-center gap-6 text-sm text-gray-500"> <div className="flex items-center gap-6 text-sm text-gray-500">
{displayStats.map((stat) => ( {displayStats.map((stat) => (
@@ -72,5 +103,6 @@ export function TotalsBar({
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -3,6 +3,7 @@ import { DashboardCard } from "../components/DashboardCard";
import { useSetups } from "../hooks/useSetups"; import { useSetups } from "../hooks/useSetups";
import { useThreads } from "../hooks/useThreads"; import { useThreads } from "../hooks/useThreads";
import { useTotals } from "../hooks/useTotals"; import { useTotals } from "../hooks/useTotals";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters"; import { formatPrice, formatWeight } from "../lib/formatters";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
@@ -13,6 +14,7 @@ function DashboardPage() {
const { data: totals } = useTotals(); const { data: totals } = useTotals();
const { data: threads } = useThreads(false); const { data: threads } = useThreads(false);
const { data: setups } = useSetups(); const { data: setups } = useSetups();
const unit = useWeightUnit();
const global = totals?.global; const global = totals?.global;
const activeThreadCount = threads?.length ?? 0; const activeThreadCount = threads?.length ?? 0;
@@ -29,7 +31,7 @@ function DashboardPage() {
{ label: "Items", value: String(global?.itemCount ?? 0) }, { label: "Items", value: String(global?.itemCount ?? 0) },
{ {
label: "Weight", label: "Weight",
value: formatWeight(global?.totalWeight ?? null), value: formatWeight(global?.totalWeight ?? null, unit),
}, },
{ label: "Cost", value: formatPrice(global?.totalCost ?? null) }, { label: "Cost", value: formatPrice(global?.totalCost ?? null) },
]} ]}

View File

@@ -8,6 +8,7 @@ import {
useRemoveSetupItem, useRemoveSetupItem,
useSetup, useSetup,
} from "../../hooks/useSetups"; } from "../../hooks/useSetups";
import { useWeightUnit } from "../../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../../lib/formatters"; import { formatPrice, formatWeight } from "../../lib/formatters";
import { LucideIcon } from "../../lib/iconData"; import { LucideIcon } from "../../lib/iconData";
@@ -17,6 +18,7 @@ export const Route = createFileRoute("/setups/$setupId")({
function SetupDetailPage() { function SetupDetailPage() {
const { setupId } = Route.useParams(); const { setupId } = Route.useParams();
const unit = useWeightUnit();
const navigate = useNavigate(); const navigate = useNavigate();
const numericId = Number(setupId); const numericId = Number(setupId);
const { data: setup, isLoading } = useSetup(numericId); const { data: setup, isLoading } = useSetup(numericId);
@@ -105,7 +107,7 @@ function SetupDetailPage() {
</span> </span>
<span> <span>
<span className="font-medium text-gray-700"> <span className="font-medium text-gray-700">
{formatWeight(totalWeight)} {formatWeight(totalWeight, unit)}
</span>{" "} </span>{" "}
total total
</span> </span>