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:
@@ -1,6 +1,4 @@
|
|||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
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";
|
||||||
import { RankBadge } from "./CandidateListItem";
|
import { RankBadge } from "./CandidateListItem";
|
||||||
@@ -41,8 +39,7 @@ export function CandidateCard({
|
|||||||
cons,
|
cons,
|
||||||
rank,
|
rank,
|
||||||
}: CandidateCardProps) {
|
}: CandidateCardProps) {
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
||||||
const openConfirmDeleteCandidate = useUIStore(
|
const openConfirmDeleteCandidate = useUIStore(
|
||||||
(s) => s.openConfirmDeleteCandidate,
|
(s) => s.openConfirmDeleteCandidate,
|
||||||
@@ -165,12 +162,12 @@ export function CandidateCard({
|
|||||||
{rank != null && <RankBadge rank={rank} />}
|
{rank != null && <RankBadge rank={rank} />}
|
||||||
{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, unit)}
|
{weight(weightGrams)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{priceCents != null && (
|
{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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Reorder, useDragControls } from "framer-motion";
|
import { Reorder, useDragControls } from "framer-motion";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
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";
|
||||||
import { StatusBadge } from "./StatusBadge";
|
import { StatusBadge } from "./StatusBadge";
|
||||||
@@ -53,8 +51,7 @@ export function CandidateListItem({
|
|||||||
onStatusChange,
|
onStatusChange,
|
||||||
}: CandidateListItemProps) {
|
}: CandidateListItemProps) {
|
||||||
const controls = useDragControls();
|
const controls = useDragControls();
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
||||||
const openConfirmDeleteCandidate = useUIStore(
|
const openConfirmDeleteCandidate = useUIStore(
|
||||||
(s) => s.openConfirmDeleteCandidate,
|
(s) => s.openConfirmDeleteCandidate,
|
||||||
@@ -113,12 +110,12 @@ export function CandidateListItem({
|
|||||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
{candidate.weightGrams != null && (
|
{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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{candidate.priceCents != null && (
|
{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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories";
|
import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
import { formatPrice, formatWeight } from "../lib/formatters";
|
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
import { IconPicker } from "./IconPicker";
|
import { IconPicker } from "./IconPicker";
|
||||||
|
|
||||||
@@ -23,8 +21,7 @@ export function CategoryHeader({
|
|||||||
totalCost,
|
totalCost,
|
||||||
itemCount,
|
itemCount,
|
||||||
}: CategoryHeaderProps) {
|
}: CategoryHeaderProps) {
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
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);
|
||||||
@@ -88,8 +85,8 @@ export function CategoryHeader({
|
|||||||
<LucideIcon name={icon} size={22} className="text-gray-500" />
|
<LucideIcon name={icon} size={22} className="text-gray-500" />
|
||||||
<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"} · {weight(totalWeight)}{" "}
|
||||||
{formatWeight(totalWeight, unit)} · {formatPrice(totalCost, currency)}
|
· {price(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,10 +1,8 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useCategories } from "../hooks/useCategories";
|
import { useCategories } from "../hooks/useCategories";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useItems } from "../hooks/useItems";
|
import { useItems } from "../hooks/useItems";
|
||||||
import { useTotals } from "../hooks/useTotals";
|
import { useTotals } from "../hooks/useTotals";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
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";
|
||||||
import { CategoryFilterDropdown } from "./CategoryFilterDropdown";
|
import { CategoryFilterDropdown } from "./CategoryFilterDropdown";
|
||||||
@@ -15,8 +13,7 @@ export function CollectionView() {
|
|||||||
const { data: items, isLoading: itemsLoading } = useItems();
|
const { data: items, isLoading: itemsLoading } = useItems();
|
||||||
const { data: totals } = useTotals();
|
const { data: totals } = useTotals();
|
||||||
const { data: categories } = useCategories();
|
const { data: categories } = useCategories();
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
const openAddPanel = useUIStore((s) => s.openAddPanel);
|
const openAddPanel = useUIStore((s) => s.openAddPanel);
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
@@ -148,7 +145,7 @@ export function CollectionView() {
|
|||||||
<LucideIcon name="weight" size={14} className="text-gray-400" />
|
<LucideIcon name="weight" size={14} className="text-gray-400" />
|
||||||
<span className="text-xs text-gray-500">Total Weight</span>
|
<span className="text-xs text-gray-500">Total Weight</span>
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{formatWeight(totals.global.totalWeight, unit)}
|
{weight(totals.global.totalWeight)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<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-xs text-gray-500">Total Spent</span>
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{formatPrice(totals.global.totalCost, currency)}
|
{price(totals.global.totalCost)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
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";
|
||||||
import { RankBadge } from "./CandidateListItem";
|
import { RankBadge } from "./CandidateListItem";
|
||||||
@@ -40,8 +38,7 @@ export function ComparisonTable({
|
|||||||
candidates,
|
candidates,
|
||||||
resolvedCandidateId,
|
resolvedCandidateId,
|
||||||
}: ComparisonTableProps) {
|
}: ComparisonTableProps) {
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
||||||
|
|
||||||
const { bestWeightId, bestPriceId, weightDeltas, priceDeltas } =
|
const { bestWeightId, bestPriceId, weightDeltas, priceDeltas } =
|
||||||
@@ -62,8 +59,7 @@ export function ComparisonTable({
|
|||||||
weightDeltas[c.id] = null;
|
weightDeltas[c.id] = null;
|
||||||
} else {
|
} else {
|
||||||
const delta = c.weightGrams - minWeight;
|
const delta = c.weightGrams - minWeight;
|
||||||
weightDeltas[c.id] =
|
weightDeltas[c.id] = delta === 0 ? null : `+${weight(delta)}`;
|
||||||
delta === 0 ? null : `+${formatWeight(delta, unit)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -88,8 +84,7 @@ export function ComparisonTable({
|
|||||||
priceDeltas[c.id] = null;
|
priceDeltas[c.id] = null;
|
||||||
} else {
|
} else {
|
||||||
const delta = c.priceCents - minPrice;
|
const delta = c.priceCents - minPrice;
|
||||||
priceDeltas[c.id] =
|
priceDeltas[c.id] = delta === 0 ? null : `+${price(delta)}`;
|
||||||
delta === 0 ? null : `+${formatPrice(delta, currency)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -99,7 +94,7 @@ export function ComparisonTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { bestWeightId, bestPriceId, weightDeltas, priceDeltas };
|
return { bestWeightId, bestPriceId, weightDeltas, priceDeltas };
|
||||||
}, [candidates, unit, currency]);
|
}, [candidates, weight, price]);
|
||||||
|
|
||||||
const ATTRIBUTE_ROWS: Array<{
|
const ATTRIBUTE_ROWS: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
@@ -155,7 +150,7 @@ export function ComparisonTable({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900">
|
||||||
{formatWeight(c.weightGrams, unit)}
|
{weight(c.weightGrams)}
|
||||||
</span>
|
</span>
|
||||||
{!isBest && delta && (
|
{!isBest && delta && (
|
||||||
<div className="text-xs text-gray-400">{delta}</div>
|
<div className="text-xs text-gray-400">{delta}</div>
|
||||||
@@ -181,7 +176,7 @@ export function ComparisonTable({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900">
|
||||||
{formatPrice(c.priceCents, currency)}
|
{price(c.priceCents)}
|
||||||
</span>
|
</span>
|
||||||
{!isBest && delta && (
|
{!isBest && delta && (
|
||||||
<div className="text-xs text-gray-400">{delta}</div>
|
<div className="text-xs text-gray-400">{delta}</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
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";
|
||||||
import { ClassificationBadge } from "./ClassificationBadge";
|
import { ClassificationBadge } from "./ClassificationBadge";
|
||||||
@@ -32,8 +30,7 @@ export function ItemCard({
|
|||||||
classification,
|
classification,
|
||||||
onClassificationCycle,
|
onClassificationCycle,
|
||||||
}: ItemCardProps) {
|
}: ItemCardProps) {
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
const openEditPanel = useUIStore((s) => s.openEditPanel);
|
const openEditPanel = useUIStore((s) => s.openEditPanel);
|
||||||
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
||||||
|
|
||||||
@@ -131,12 +128,12 @@ 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, unit)}
|
{weight(weightGrams)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{priceCents != null && (
|
{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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
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 { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
import { SlideOutPanel } from "./SlideOutPanel";
|
import { SlideOutPanel } from "./SlideOutPanel";
|
||||||
|
|
||||||
@@ -22,8 +20,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 { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
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
|
||||||
@@ -117,13 +114,11 @@ export function ItemPicker({
|
|||||||
{item.name}
|
{item.name}
|
||||||
</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 && weight(item.weightGrams)}
|
||||||
formatWeight(item.weightGrams, unit)}
|
|
||||||
{item.weightGrams != null &&
|
{item.weightGrams != null &&
|
||||||
item.priceCents != null &&
|
item.priceCents != null &&
|
||||||
" · "}
|
" · "}
|
||||||
{item.priceCents != null &&
|
{item.priceCents != null && price(item.priceCents)}
|
||||||
formatPrice(item.priceCents, currency)}
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
import { formatPrice, formatWeight } from "../lib/formatters";
|
|
||||||
|
|
||||||
interface SetupCardProps {
|
interface SetupCardProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -18,8 +16,7 @@ export function SetupCard({
|
|||||||
totalWeight,
|
totalWeight,
|
||||||
totalCost,
|
totalCost,
|
||||||
}: SetupCardProps) {
|
}: SetupCardProps) {
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to="/setups/$setupId"
|
to="/setups/$setupId"
|
||||||
@@ -34,10 +31,10 @@ 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, unit)}
|
{weight(totalWeight)}
|
||||||
</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, currency)}
|
{price(totalCost)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { formatPrice } from "../lib/formatters";
|
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
|
|
||||||
interface ThreadCardProps {
|
interface ThreadCardProps {
|
||||||
@@ -20,16 +19,6 @@ function formatDate(iso: string): string {
|
|||||||
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
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({
|
export function ThreadCard({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -42,10 +31,19 @@ export function ThreadCard({
|
|||||||
categoryIcon,
|
categoryIcon,
|
||||||
}: ThreadCardProps) {
|
}: ThreadCardProps) {
|
||||||
const navigate = useNavigate();
|
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 isResolved = status === "resolved";
|
||||||
const priceRange = formatPriceRange(minPriceCents, maxPriceCents, currency);
|
const priceRange = formatPriceRange(minPriceCents, maxPriceCents);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useAuth, useLogout } from "../hooks/useAuth";
|
import { useAuth, useLogout } from "../hooks/useAuth";
|
||||||
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useUpdateSetting } from "../hooks/useSettings";
|
import { useUpdateSetting } from "../hooks/useSettings";
|
||||||
import { useTotals } from "../hooks/useTotals";
|
import { useTotals } from "../hooks/useTotals";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
import type { WeightUnit } from "../lib/formatters";
|
||||||
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"];
|
const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
|
||||||
@@ -23,7 +23,7 @@ export function TotalsBar({
|
|||||||
const { data: auth } = useAuth();
|
const { data: auth } = useAuth();
|
||||||
const logout = useLogout();
|
const logout = useLogout();
|
||||||
const isAuthenticated = !!auth?.user;
|
const isAuthenticated = !!auth?.user;
|
||||||
const unit = useWeightUnit();
|
const { weight, price, unit } = useFormatters();
|
||||||
const updateSetting = useUpdateSetting();
|
const updateSetting = useUpdateSetting();
|
||||||
|
|
||||||
// When no stats provided, use global totals (backward compatible)
|
// When no stats provided, use global totals (backward compatible)
|
||||||
@@ -34,14 +34,14 @@ export function TotalsBar({
|
|||||||
{ label: "items", value: String(data.global.itemCount) },
|
{ label: "items", value: String(data.global.itemCount) },
|
||||||
{
|
{
|
||||||
label: "total",
|
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: "items", value: "0" },
|
||||||
{ label: "total", value: formatWeight(null, unit) },
|
{ label: "total", value: weight(null) },
|
||||||
{ label: "spent", value: formatPrice(null) },
|
{ label: "spent", value: price(null) },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const titleContent = (
|
const titleContent = (
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import type { SetupItemWithCategory } from "../hooks/useSetups";
|
import type { SetupItemWithCategory } from "../hooks/useSetups";
|
||||||
import { useWeightUnit } from "../hooks/useWeightUnit";
|
|
||||||
import { formatWeight, type WeightUnit } from "../lib/formatters";
|
import { formatWeight, type WeightUnit } from "../lib/formatters";
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ function LegendRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WeightSummaryCard({ items }: WeightSummaryCardProps) {
|
export function WeightSummaryCard({ items }: WeightSummaryCardProps) {
|
||||||
const unit = useWeightUnit();
|
const { unit } = useFormatters();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("category");
|
const [viewMode, setViewMode] = useState<ViewMode>("category");
|
||||||
|
|
||||||
const baseWeight = items.reduce(
|
const baseWeight = items.reduce(
|
||||||
|
|||||||
14
src/client/hooks/useFormatters.ts
Normal file
14
src/client/hooks/useFormatters.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { DashboardCard } from "../components/DashboardCard";
|
import { DashboardCard } from "../components/DashboardCard";
|
||||||
import { useCurrency } from "../hooks/useCurrency";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
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";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: DashboardPage,
|
component: DashboardPage,
|
||||||
@@ -15,8 +13,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 { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
|
|
||||||
const global = totals?.global;
|
const global = totals?.global;
|
||||||
const activeThreadCount = threads?.length ?? 0;
|
const activeThreadCount = threads?.length ?? 0;
|
||||||
@@ -33,11 +30,11 @@ 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, unit),
|
value: weight(global?.totalWeight ?? null),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Cost",
|
label: "Cost",
|
||||||
value: formatPrice(global?.totalCost ?? null, currency),
|
value: price(global?.totalCost ?? null),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
emptyText="Get started"
|
emptyText="Get started"
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import { CategoryHeader } from "../../components/CategoryHeader";
|
|||||||
import { ItemCard } from "../../components/ItemCard";
|
import { ItemCard } from "../../components/ItemCard";
|
||||||
import { ItemPicker } from "../../components/ItemPicker";
|
import { ItemPicker } from "../../components/ItemPicker";
|
||||||
import { WeightSummaryCard } from "../../components/WeightSummaryCard";
|
import { WeightSummaryCard } from "../../components/WeightSummaryCard";
|
||||||
import { useCurrency } from "../../hooks/useCurrency";
|
import { useFormatters } from "../../hooks/useFormatters";
|
||||||
import {
|
import {
|
||||||
useDeleteSetup,
|
useDeleteSetup,
|
||||||
useRemoveSetupItem,
|
useRemoveSetupItem,
|
||||||
useSetup,
|
useSetup,
|
||||||
useUpdateItemClassification,
|
useUpdateItemClassification,
|
||||||
} from "../../hooks/useSetups";
|
} from "../../hooks/useSetups";
|
||||||
import { useWeightUnit } from "../../hooks/useWeightUnit";
|
|
||||||
import { formatPrice, formatWeight } from "../../lib/formatters";
|
|
||||||
import { LucideIcon } from "../../lib/iconData";
|
import { LucideIcon } from "../../lib/iconData";
|
||||||
|
|
||||||
export const Route = createFileRoute("/setups/$setupId")({
|
export const Route = createFileRoute("/setups/$setupId")({
|
||||||
@@ -21,8 +19,7 @@ export const Route = createFileRoute("/setups/$setupId")({
|
|||||||
|
|
||||||
function SetupDetailPage() {
|
function SetupDetailPage() {
|
||||||
const { setupId } = Route.useParams();
|
const { setupId } = Route.useParams();
|
||||||
const unit = useWeightUnit();
|
const { weight, price } = useFormatters();
|
||||||
const currency = useCurrency();
|
|
||||||
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);
|
||||||
@@ -127,13 +124,13 @@ function SetupDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-700">
|
<span className="font-medium text-gray-700">
|
||||||
{formatWeight(totalWeight, unit)}
|
{weight(totalWeight)}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
total
|
total
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-700">
|
<span className="font-medium text-gray-700">
|
||||||
{formatPrice(totalCost, currency)}
|
{price(totalCost)}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
cost
|
cost
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user