feat(33-05): market/currency selector, dual price format, conversion toggle

- Add formatDualPrice() with ~prefix for approximate conversions (D-14)
- Evolve useCurrency() to return CurrencyContext with currency, market, showConversions
- Create useExchangeRates hook + convertClientPrice utility
- Redesign settings: Market & Currency selector, Show Converted Prices toggle
- Add locale-based auto-suggestion banner for first-time currency selection (D-13)
- Update useFormatters to destructure from new CurrencyContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 18:08:53 +02:00
parent d0bbf48bb5
commit 02fcae12f0
5 changed files with 185 additions and 12 deletions

View File

@@ -1,12 +1,42 @@
import type { Currency } from "../lib/formatters";
import { useSetting } from "./useSettings";
const VALID_CURRENCIES: Currency[] = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"];
const VALID_CURRENCIES: Currency[] = [
"USD",
"EUR",
"GBP",
"JPY",
"CAD",
"AUD",
];
export function useCurrency(): Currency {
const { data } = useSetting("currency");
if (data && VALID_CURRENCIES.includes(data as Currency)) {
return data as Currency;
}
return "USD";
const CURRENCY_MARKET_MAP: Record<string, string> = {
EUR: "EU",
USD: "US",
GBP: "UK",
JPY: "JP",
CAD: "CA",
AUD: "AU",
};
export interface CurrencyContext {
currency: Currency;
market: string;
showConversions: boolean;
}
export function useCurrency(): CurrencyContext {
const { data: currencyData } = useSetting("currency");
const { data: showConversionsData } = useSetting("showConversions");
const currency: Currency =
currencyData && VALID_CURRENCIES.includes(currencyData as Currency)
? (currencyData as Currency)
: "USD";
return {
currency,
market: CURRENCY_MARKET_MAP[currency] ?? currency,
showConversions: showConversionsData === "true",
};
}

View File

@@ -0,0 +1,34 @@
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";
interface ExchangeRates {
base: string;
date: string;
rates: Record<string, number>;
}
export function useExchangeRates() {
return useQuery({
queryKey: ["exchange-rates"],
queryFn: () => apiGet<ExchangeRates>("/api/exchange-rates"),
staleTime: 1000 * 60 * 60, // 1 hour client-side stale time
gcTime: 1000 * 60 * 60 * 24, // 24 hour garbage collection
refetchOnWindowFocus: false,
});
}
/**
* Convert price in cents from one currency to another using provided rates.
* All conversions go through EUR as the base currency.
*/
export function convertClientPrice(
cents: number,
from: string,
to: string,
rates: Record<string, number>,
): number {
if (from === to) return cents;
const fromRate = rates[from] ?? 1;
const toRate = rates[to] ?? 1;
return Math.round((cents / fromRate) * toRate);
}

View File

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