refactor: add useFormatters hook to reduce boilerplate across 14 components

Created useFormatters() combining useWeightUnit + useCurrency + formatWeight/formatPrice
into a single hook returning weight(grams) and price(cents) bound functions plus
raw unit and currency values. Updated all 14 consumer files to use the new hook,
removing the repeated 4-import + 2-hook-call pattern from each.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 15:49:16 +02:00
parent 5308991123
commit e9d33e59e9
14 changed files with 78 additions and 100 deletions

View File

@@ -1,6 +1,4 @@
import { useCurrency } from "../hooks/useCurrency";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { RankBadge } from "./CandidateListItem";
@@ -41,8 +39,7 @@ export function CandidateCard({
cons,
rank,
}: CandidateCardProps) {
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate,
@@ -165,12 +162,12 @@ export function CandidateCard({
{rank != null && <RankBadge rank={rank} />}
{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, unit)}
{weight(weightGrams)}
</span>
)}
{priceCents != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{formatPrice(priceCents, currency)}
{price(priceCents)}
</span>
)}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">

View File

@@ -1,7 +1,5 @@
import { Reorder, useDragControls } from "framer-motion";
import { useCurrency } from "../hooks/useCurrency";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { StatusBadge } from "./StatusBadge";
@@ -53,8 +51,7 @@ export function CandidateListItem({
onStatusChange,
}: CandidateListItemProps) {
const controls = useDragControls();
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate,
@@ -113,12 +110,12 @@ export function CandidateListItem({
<div className="flex flex-wrap gap-1.5 mt-1">
{candidate.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(candidate.weightGrams, unit)}
{weight(candidate.weightGrams)}
</span>
)}
{candidate.priceCents != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{formatPrice(candidate.priceCents, currency)}
{price(candidate.priceCents)}
</span>
)}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">

View File

@@ -1,8 +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 { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
import { IconPicker } from "./IconPicker";
@@ -23,8 +21,7 @@ export function CategoryHeader({
totalCost,
itemCount,
}: CategoryHeaderProps) {
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(name);
const [editIcon, setEditIcon] = useState(icon);
@@ -88,8 +85,8 @@ export function CategoryHeader({
<LucideIcon name={icon} size={22} className="text-gray-500" />
<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, unit)} · {formatPrice(totalCost, currency)}
{itemCount} {itemCount === 1 ? "item" : "items"} · {weight(totalWeight)}{" "}
· {price(totalCost)}
</span>
{!isUncategorized && (
<div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">

View File

@@ -1,10 +1,8 @@
import { useMemo, useState } from "react";
import { useCategories } from "../hooks/useCategories";
import { useCurrency } from "../hooks/useCurrency";
import { useFormatters } from "../hooks/useFormatters";
import { useItems } from "../hooks/useItems";
import { useTotals } from "../hooks/useTotals";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { CategoryFilterDropdown } from "./CategoryFilterDropdown";
@@ -15,8 +13,7 @@ export function CollectionView() {
const { data: items, isLoading: itemsLoading } = useItems();
const { data: totals } = useTotals();
const { data: categories } = useCategories();
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const openAddPanel = useUIStore((s) => s.openAddPanel);
const [searchText, setSearchText] = useState("");
@@ -148,7 +145,7 @@ export function CollectionView() {
<LucideIcon name="weight" size={14} className="text-gray-400" />
<span className="text-xs text-gray-500">Total Weight</span>
<span className="text-sm font-semibold text-gray-900">
{formatWeight(totals.global.totalWeight, unit)}
{weight(totals.global.totalWeight)}
</span>
</div>
<div className="flex flex-col items-center gap-1">
@@ -159,7 +156,7 @@ export function CollectionView() {
/>
<span className="text-xs text-gray-500">Total Spent</span>
<span className="text-sm font-semibold text-gray-900">
{formatPrice(totals.global.totalCost, currency)}
{price(totals.global.totalCost)}
</span>
</div>
</div>

View File

@@ -1,7 +1,5 @@
import { useMemo } from "react";
import { useCurrency } from "../hooks/useCurrency";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { RankBadge } from "./CandidateListItem";
@@ -40,8 +38,7 @@ export function ComparisonTable({
candidates,
resolvedCandidateId,
}: ComparisonTableProps) {
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const openExternalLink = useUIStore((s) => s.openExternalLink);
const { bestWeightId, bestPriceId, weightDeltas, priceDeltas } =
@@ -62,8 +59,7 @@ export function ComparisonTable({
weightDeltas[c.id] = null;
} else {
const delta = c.weightGrams - minWeight;
weightDeltas[c.id] =
delta === 0 ? null : `+${formatWeight(delta, unit)}`;
weightDeltas[c.id] = delta === 0 ? null : `+${weight(delta)}`;
}
}
} else {
@@ -88,8 +84,7 @@ export function ComparisonTable({
priceDeltas[c.id] = null;
} else {
const delta = c.priceCents - minPrice;
priceDeltas[c.id] =
delta === 0 ? null : `+${formatPrice(delta, currency)}`;
priceDeltas[c.id] = delta === 0 ? null : `+${price(delta)}`;
}
}
} else {
@@ -99,7 +94,7 @@ export function ComparisonTable({
}
return { bestWeightId, bestPriceId, weightDeltas, priceDeltas };
}, [candidates, unit, currency]);
}, [candidates, weight, price]);
const ATTRIBUTE_ROWS: Array<{
key: string;
@@ -155,7 +150,7 @@ export function ComparisonTable({
return (
<div>
<span className="font-medium text-gray-900">
{formatWeight(c.weightGrams, unit)}
{weight(c.weightGrams)}
</span>
{!isBest && delta && (
<div className="text-xs text-gray-400">{delta}</div>
@@ -181,7 +176,7 @@ export function ComparisonTable({
return (
<div>
<span className="font-medium text-gray-900">
{formatPrice(c.priceCents, currency)}
{price(c.priceCents)}
</span>
{!isBest && delta && (
<div className="text-xs text-gray-400">{delta}</div>

View File

@@ -1,6 +1,4 @@
import { useCurrency } from "../hooks/useCurrency";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { ClassificationBadge } from "./ClassificationBadge";
@@ -32,8 +30,7 @@ export function ItemCard({
classification,
onClassificationCycle,
}: ItemCardProps) {
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const openEditPanel = useUIStore((s) => s.openEditPanel);
const openExternalLink = useUIStore((s) => s.openExternalLink);
@@ -131,12 +128,12 @@ 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, unit)}
{weight(weightGrams)}
</span>
)}
{priceCents != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{formatPrice(priceCents, currency)}
{price(priceCents)}
</span>
)}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">

View File

@@ -1,9 +1,7 @@
import { useEffect, useState } from "react";
import { useCurrency } from "../hooks/useCurrency";
import { useFormatters } from "../hooks/useFormatters";
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";
@@ -22,8 +20,7 @@ export function ItemPicker({
}: ItemPickerProps) {
const { data: items } = useItems();
const syncItems = useSyncSetupItems(setupId);
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
// Reset selected IDs when panel opens
@@ -117,13 +114,11 @@ export function ItemPicker({
{item.name}
</span>
<span className="text-xs text-gray-400 shrink-0">
{item.weightGrams != null &&
formatWeight(item.weightGrams, unit)}
{item.weightGrams != null && weight(item.weightGrams)}
{item.weightGrams != null &&
item.priceCents != null &&
" · "}
{item.priceCents != null &&
formatPrice(item.priceCents, currency)}
{item.priceCents != null && price(item.priceCents)}
</span>
</label>
))}

View File

@@ -1,7 +1,5 @@
import { Link } from "@tanstack/react-router";
import { useCurrency } from "../hooks/useCurrency";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
interface SetupCardProps {
id: number;
@@ -18,8 +16,7 @@ export function SetupCard({
totalWeight,
totalCost,
}: SetupCardProps) {
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
return (
<Link
to="/setups/$setupId"
@@ -34,10 +31,10 @@ 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, unit)}
{weight(totalWeight)}
</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, currency)}
{price(totalCost)}
</span>
</div>
</Link>

View File

@@ -1,6 +1,5 @@
import { useNavigate } from "@tanstack/react-router";
import { useCurrency } from "../hooks/useCurrency";
import { formatPrice } from "../lib/formatters";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
interface ThreadCardProps {
@@ -20,16 +19,6 @@ function formatDate(iso: string): string {
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function formatPriceRange(
min: number | null,
max: number | null,
currency: Parameters<typeof formatPrice>[1],
): string | null {
if (min == null && max == null) return null;
if (min === max) return formatPrice(min, currency);
return `${formatPrice(min, currency)} - ${formatPrice(max, currency)}`;
}
export function ThreadCard({
id,
name,
@@ -42,10 +31,19 @@ export function ThreadCard({
categoryIcon,
}: ThreadCardProps) {
const navigate = useNavigate();
const currency = useCurrency();
const { price } = useFormatters();
function formatPriceRange(
min: number | null,
max: number | null,
): string | null {
if (min == null && max == null) return null;
if (min === max) return price(min);
return `${price(min)} - ${price(max)}`;
}
const isResolved = status === "resolved";
const priceRange = formatPriceRange(minPriceCents, maxPriceCents, currency);
const priceRange = formatPriceRange(minPriceCents, maxPriceCents);
return (
<button

View File

@@ -1,9 +1,9 @@
import { Link } from "@tanstack/react-router";
import { useAuth, useLogout } from "../hooks/useAuth";
import { useFormatters } from "../hooks/useFormatters";
import { useUpdateSetting } from "../hooks/useSettings";
import { useTotals } from "../hooks/useTotals";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight, type WeightUnit } from "../lib/formatters";
import type { WeightUnit } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
@@ -23,7 +23,7 @@ export function TotalsBar({
const { data: auth } = useAuth();
const logout = useLogout();
const isAuthenticated = !!auth?.user;
const unit = useWeightUnit();
const { weight, price, unit } = useFormatters();
const updateSetting = useUpdateSetting();
// When no stats provided, use global totals (backward compatible)
@@ -34,14 +34,14 @@ export function TotalsBar({
{ label: "items", value: String(data.global.itemCount) },
{
label: "total",
value: formatWeight(data.global.totalWeight, unit),
value: weight(data.global.totalWeight),
},
{ label: "spent", value: formatPrice(data.global.totalCost) },
{ label: "spent", value: price(data.global.totalCost) },
]
: [
{ label: "items", value: "0" },
{ label: "total", value: formatWeight(null, unit) },
{ label: "spent", value: formatPrice(null) },
{ label: "total", value: weight(null) },
{ label: "spent", value: price(null) },
]);
const titleContent = (

View File

@@ -7,8 +7,8 @@ import {
ResponsiveContainer,
Tooltip,
} from "recharts";
import { useFormatters } from "../hooks/useFormatters";
import type { SetupItemWithCategory } from "../hooks/useSetups";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatWeight, type WeightUnit } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
@@ -143,7 +143,7 @@ function LegendRow({
}
export function WeightSummaryCard({ items }: WeightSummaryCardProps) {
const unit = useWeightUnit();
const { unit } = useFormatters();
const [viewMode, setViewMode] = useState<ViewMode>("category");
const baseWeight = items.reduce(

View File

@@ -0,0 +1,14 @@
import { formatPrice, formatWeight } from "../lib/formatters";
import { useCurrency } from "./useCurrency";
import { useWeightUnit } from "./useWeightUnit";
export function useFormatters() {
const unit = useWeightUnit();
const currency = useCurrency();
return {
weight: (grams: number | null) => formatWeight(grams, unit),
price: (cents: number | null) => formatPrice(cents, currency),
unit,
currency,
};
}

View File

@@ -1,11 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { DashboardCard } from "../components/DashboardCard";
import { useCurrency } from "../hooks/useCurrency";
import { useFormatters } from "../hooks/useFormatters";
import { useSetups } from "../hooks/useSetups";
import { useThreads } from "../hooks/useThreads";
import { useTotals } from "../hooks/useTotals";
import { useWeightUnit } from "../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../lib/formatters";
export const Route = createFileRoute("/")({
component: DashboardPage,
@@ -15,8 +13,7 @@ function DashboardPage() {
const { data: totals } = useTotals();
const { data: threads } = useThreads(false);
const { data: setups } = useSetups();
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const global = totals?.global;
const activeThreadCount = threads?.length ?? 0;
@@ -33,11 +30,11 @@ function DashboardPage() {
{ label: "Items", value: String(global?.itemCount ?? 0) },
{
label: "Weight",
value: formatWeight(global?.totalWeight ?? null, unit),
value: weight(global?.totalWeight ?? null),
},
{
label: "Cost",
value: formatPrice(global?.totalCost ?? null, currency),
value: price(global?.totalCost ?? null),
},
]}
emptyText="Get started"

View File

@@ -4,15 +4,13 @@ import { CategoryHeader } from "../../components/CategoryHeader";
import { ItemCard } from "../../components/ItemCard";
import { ItemPicker } from "../../components/ItemPicker";
import { WeightSummaryCard } from "../../components/WeightSummaryCard";
import { useCurrency } from "../../hooks/useCurrency";
import { useFormatters } from "../../hooks/useFormatters";
import {
useDeleteSetup,
useRemoveSetupItem,
useSetup,
useUpdateItemClassification,
} from "../../hooks/useSetups";
import { useWeightUnit } from "../../hooks/useWeightUnit";
import { formatPrice, formatWeight } from "../../lib/formatters";
import { LucideIcon } from "../../lib/iconData";
export const Route = createFileRoute("/setups/$setupId")({
@@ -21,8 +19,7 @@ export const Route = createFileRoute("/setups/$setupId")({
function SetupDetailPage() {
const { setupId } = Route.useParams();
const unit = useWeightUnit();
const currency = useCurrency();
const { weight, price } = useFormatters();
const navigate = useNavigate();
const numericId = Number(setupId);
const { data: setup, isLoading } = useSetup(numericId);
@@ -127,13 +124,13 @@ function SetupDetailPage() {
</span>
<span>
<span className="font-medium text-gray-700">
{formatWeight(totalWeight, unit)}
{weight(totalWeight)}
</span>{" "}
total
</span>
<span>
<span className="font-medium text-gray-700">
{formatPrice(totalCost, currency)}
{price(totalCost)}
</span>{" "}
cost
</span>