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 { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
@@ -27,6 +28,7 @@ export function CandidateCard({
threadId,
isActive,
}: CandidateCardProps) {
const unit = useWeightUnit();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate,
@@ -88,7 +90,7 @@ export function CandidateCard({
<div className="flex flex-wrap gap-1.5 mb-3">
{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">
{formatWeight(weightGrams)}
{formatWeight(weightGrams, unit)}
</span>
)}
{priceCents != null && (

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
import { IconPicker } from "./IconPicker";
@@ -21,6 +22,7 @@ export function CategoryHeader({
totalCost,
itemCount,
}: CategoryHeaderProps) {
const unit = useWeightUnit();
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(name);
const [editIcon, setEditIcon] = useState(icon);
@@ -85,7 +87,7 @@ export function CategoryHeader({
<h2 className="text-lg font-semibold text-gray-900">{name}</h2>
<span className="text-sm text-gray-400">
{itemCount} {itemCount === 1 ? "item" : "items"} ·{" "}
{formatWeight(totalWeight)} · {formatPrice(totalCost)}
{formatWeight(totalWeight, unit)} · {formatPrice(totalCost)}
</span>
{!isUncategorized && (
<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 { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
@@ -25,6 +26,7 @@ export function ItemCard({
productUrl,
onRemove,
}: ItemCardProps) {
const unit = useWeightUnit();
const openEditPanel = useUIStore((s) => s.openEditPanel);
const openExternalLink = useUIStore((s) => s.openExternalLink);
@@ -122,7 +124,7 @@ export function ItemCard({
<div className="flex flex-wrap gap-1.5">
{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">
{formatWeight(weightGrams)}
{formatWeight(weightGrams, unit)}
</span>
)}
{priceCents != null && (

View File

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

View File

@@ -1,4 +1,5 @@
import { Link } from "@tanstack/react-router";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
interface SetupCardProps {
@@ -16,6 +17,7 @@ export function SetupCard({
totalWeight,
totalCost,
}: SetupCardProps) {
const unit = useWeightUnit();
return (
<Link
to="/setups/$setupId"
@@ -30,7 +32,7 @@ export function SetupCard({
</div>
<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">
{formatWeight(totalWeight)}
{formatWeight(totalWeight, unit)}
</span>
<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)}

View File

@@ -1,8 +1,12 @@
import { Link } from "@tanstack/react-router";
import { useUpdateSetting } from "../hooks/useSettings";
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";
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
interface TotalsBarProps {
title?: string;
stats?: Array<{ label: string; value: string }>;
@@ -15,6 +19,8 @@ export function TotalsBar({
linkTo,
}: TotalsBarProps) {
const { data } = useTotals();
const unit = useWeightUnit();
const updateSetting = useUpdateSetting();
// When no stats provided, use global totals (backward compatible)
const displayStats =
@@ -22,12 +28,15 @@ export function TotalsBar({
(data?.global
? [
{ 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: "items", value: "0" },
{ label: "total", value: formatWeight(null) },
{ label: "total", value: formatWeight(null, unit) },
{ label: "spent", value: formatPrice(null) },
]);
@@ -57,18 +66,41 @@ export function TotalsBar({
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14">
{titleElement}
{showStats && (
<div className="flex items-center gap-6 text-sm text-gray-500">
{displayStats.map((stat) => (
<span key={stat.label}>
<span className="font-medium text-gray-700">
{stat.value}
</span>{" "}
{stat.label}
</span>
<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 && (
<div className="flex items-center gap-6 text-sm text-gray-500">
{displayStats.map((stat) => (
<span key={stat.label}>
<span className="font-medium text-gray-700">
{stat.value}
</span>{" "}
{stat.label}
</span>
))}
</div>
)}
</div>
</div>
</div>
</div>