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:
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
34
src/client/hooks/useExchangeRates.ts
Normal file
34
src/client/hooks/useExchangeRates.ts
Normal 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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user