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:
@@ -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 && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 &&
|
||||||
" · "}
|
" · "}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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,18 +66,41 @@ 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}
|
||||||
{showStats && (
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
|
||||||
{displayStats.map((stat) => (
|
{UNITS.map((u) => (
|
||||||
<span key={stat.label}>
|
<button
|
||||||
<span className="font-medium text-gray-700">
|
key={u}
|
||||||
{stat.value}
|
type="button"
|
||||||
</span>{" "}
|
onClick={() =>
|
||||||
{stat.label}
|
updateSetting.mutate({
|
||||||
</span>
|
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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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) },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user