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:
@@ -201,11 +201,42 @@ function ImportExportSection() {
|
||||
);
|
||||
}
|
||||
|
||||
const LOCALE_CURRENCY_MAP: Record<string, Currency> = {
|
||||
de: "EUR",
|
||||
fr: "EUR",
|
||||
es: "EUR",
|
||||
it: "EUR",
|
||||
nl: "EUR",
|
||||
pt: "EUR",
|
||||
en: "USD",
|
||||
ja: "JPY",
|
||||
};
|
||||
|
||||
function getSuggestedCurrency(): Currency | null {
|
||||
try {
|
||||
const lang = navigator.language;
|
||||
// Check full locale first (e.g., en-GB → GBP)
|
||||
if (lang.startsWith("en-GB")) return "GBP";
|
||||
if (lang.startsWith("en-AU")) return "AUD";
|
||||
if (lang.startsWith("en-CA") || lang.startsWith("fr-CA")) return "CAD";
|
||||
// Fall back to language prefix
|
||||
const prefix = lang.split("-")[0];
|
||||
return LOCALE_CURRENCY_MAP[prefix] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const unit = useWeightUnit();
|
||||
const currency = useCurrency();
|
||||
const { currency, showConversions } = useCurrency();
|
||||
const updateSetting = useUpdateSetting();
|
||||
const { data: auth } = useAuth();
|
||||
const [suggestionDismissed, setSuggestionDismissed] = useState(false);
|
||||
|
||||
const suggestedCurrency = getSuggestedCurrency();
|
||||
const showSuggestion =
|
||||
!suggestionDismissed && suggestedCurrency && suggestedCurrency !== currency;
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
@@ -219,6 +250,32 @@ function SettingsPage() {
|
||||
<h1 className="text-xl font-semibold text-gray-900">Settings</h1>
|
||||
</div>
|
||||
|
||||
{showSuggestion && (
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-xl px-4 py-3 mb-4 flex items-center justify-between">
|
||||
<span className="text-sm text-blue-700">
|
||||
Based on your location, we suggest{" "}
|
||||
{CURRENCIES.find((c) => c.value === suggestedCurrency)?.label ??
|
||||
suggestedCurrency}{" "}
|
||||
({suggestedCurrency})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateSetting.mutate({
|
||||
key: "currency",
|
||||
value: suggestedCurrency,
|
||||
});
|
||||
setSuggestionDismissed(true);
|
||||
}}
|
||||
className="text-sm font-medium text-blue-700 hover:text-blue-800 underline ml-3"
|
||||
>
|
||||
Use{" "}
|
||||
{CURRENCIES.find((c) => c.value === suggestedCurrency)?.label ??
|
||||
suggestedCurrency}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -254,10 +311,11 @@ function SettingsPage() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Currency</h3>
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
Market & Currency
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Changes the currency symbol displayed. This does not convert
|
||||
values.
|
||||
Sets your market region and currency for price display
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5">
|
||||
@@ -282,6 +340,37 @@ function SettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
Show Converted Prices
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Display approximate conversions when local price is not available
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateSetting.mutate({
|
||||
key: "showConversions",
|
||||
value: showConversions ? "false" : "true",
|
||||
})
|
||||
}
|
||||
className={`relative w-10 h-5 rounded-full transition-colors ${
|
||||
showConversions ? "bg-blue-500" : "bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${
|
||||
showConversions ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-5 space-y-6 mt-4">
|
||||
|
||||
Reference in New Issue
Block a user