Built the Oracle Tab (#14)
This commit is contained in:
158
app/src/store/useCurrencyStore.ts
Normal file
158
app/src/store/useCurrencyStore.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// ── Currency config ───────────────────────────────────────────────────────────
|
||||
|
||||
export type CurrencyCode = 'USD' | 'AED' | 'INR';
|
||||
|
||||
export interface CurrencyOption {
|
||||
code: CurrencyCode;
|
||||
label: string;
|
||||
symbol: string;
|
||||
locale: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
export const CURRENCY_OPTIONS: CurrencyOption[] = [
|
||||
{ code: 'USD', label: 'US Dollar', symbol: '$', locale: 'en-US', flag: '🇺🇸' },
|
||||
{ code: 'AED', label: 'UAE Dirham', symbol: 'AED', locale: 'en-AE', flag: '🇦🇪' },
|
||||
{ code: 'INR', label: 'Indian Rupee', symbol: '₹', locale: 'en-IN', flag: '🇮🇳' },
|
||||
];
|
||||
|
||||
// ── Store ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CurrencyState {
|
||||
currency: CurrencyCode;
|
||||
setCurrency: (code: CurrencyCode) => void;
|
||||
|
||||
/** Format a numeric amount with the active currency */
|
||||
formatAmount: (amount: number, options?: { compact?: boolean }) => string;
|
||||
|
||||
/** Format a text string containing currency prefixes (e.g. AED 15M -> $ 15M) */
|
||||
formatText: (text: string) => string;
|
||||
|
||||
/** Active currency option object */
|
||||
option: () => CurrencyOption;
|
||||
}
|
||||
|
||||
export const useCurrencyStore = create<CurrencyState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
currency: 'USD',
|
||||
|
||||
setCurrency: (code) => set({ currency: code }),
|
||||
|
||||
option: () =>
|
||||
CURRENCY_OPTIONS.find((o) => o.code === get().currency) ?? CURRENCY_OPTIONS[0],
|
||||
|
||||
formatAmount: (amount, opts) => {
|
||||
const { currency, option } = get();
|
||||
const { locale } = option();
|
||||
|
||||
// Base assumption: Raw numbers in mock data are in AED.
|
||||
let convertedAmount = amount;
|
||||
if (currency === 'USD') convertedAmount = amount * 0.272; // AED -> USD
|
||||
if (currency === 'INR') convertedAmount = amount * 25.135112; // AED -> INR (0.272 * 92.4085)
|
||||
|
||||
if (opts?.compact) {
|
||||
// Compact notation: 1.5M, 450K, etc. — prefix with symbol
|
||||
const abs = Math.abs(convertedAmount);
|
||||
const sign = convertedAmount < 0 ? '-' : '';
|
||||
const sym = CURRENCY_OPTIONS.find((o) => o.code === currency)?.symbol ?? currency;
|
||||
|
||||
if (currency === 'INR' && abs >= 10_000_000) return `${sign}${sym} ${(abs / 10_000_000).toFixed(1)}Cr`;
|
||||
if (abs >= 1_000_000) return `${sign}${sym} ${(abs / 1_000_000).toFixed(1)}M`;
|
||||
if (abs >= 1_000) return `${sign}${sym} ${(abs / 1_000).toFixed(0)}K`;
|
||||
return `${sign}${sym} ${abs.toFixed(0)}`;
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(convertedAmount);
|
||||
},
|
||||
|
||||
formatText: (text) => {
|
||||
const { currency } = get();
|
||||
const sym = CURRENCY_OPTIONS.find((o) => o.code === currency)?.symbol ?? currency;
|
||||
|
||||
return text.replace(/(AED|INR|\$)\s*([\d\.\-]+)\s*([MKCr]*)\+?/g, (match, prefix, numRange, suffix) => {
|
||||
const parts = numRange.split('-');
|
||||
const convertedParts = parts.map((p: string) => {
|
||||
const parsedNum = parseFloat(p);
|
||||
if (isNaN(parsedNum)) return p;
|
||||
|
||||
// Step 1: Normalize everything to AED Base
|
||||
let baseAed = parsedNum;
|
||||
if (prefix === 'INR') baseAed = parsedNum / 25.135112;
|
||||
if (prefix === '$') baseAed = parsedNum / 0.272;
|
||||
|
||||
// Step 2: Handle suffixes (Cr -> M mapping)
|
||||
let multiplier = 1;
|
||||
|
||||
if (suffix === 'Cr') {
|
||||
if (currency !== 'INR') {
|
||||
multiplier = 10; // 1 Cr = 10M
|
||||
}
|
||||
} else if (suffix === 'M') {
|
||||
if (currency === 'INR') {
|
||||
multiplier = 0.1; // 10M = 1 Cr
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Convert from AED to Target Currency
|
||||
let targetNum = baseAed;
|
||||
if (currency === 'USD') targetNum = baseAed * 0.272;
|
||||
if (currency === 'INR') targetNum = baseAed * 25.135112;
|
||||
|
||||
targetNum = targetNum * multiplier;
|
||||
|
||||
return targetNum >= 10 ? targetNum.toFixed(0) : parseFloat(targetNum.toFixed(1)).toString();
|
||||
});
|
||||
|
||||
const hasPlus = match.includes('+');
|
||||
|
||||
// Determine final output suffix
|
||||
let outSuffix = suffix;
|
||||
if (suffix === 'Cr' && currency !== 'INR') outSuffix = 'M';
|
||||
if (suffix === 'M' && currency === 'INR') outSuffix = 'Cr';
|
||||
|
||||
return `${sym} ${convertedParts.join('-')}${outSuffix}${hasPlus ? '+' : ''}`;
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'pv-currency', // localStorage key
|
||||
partialize: (state) => ({ currency: state.currency }),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ── Convenience hook ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Use anywhere — returns stable primitives + function refs. No infinite loop. */
|
||||
export function useCurrency() {
|
||||
// Select each piece individually so zustand compares with Object.is on primitives
|
||||
const currency = useCurrencyStore((s) => s.currency);
|
||||
const setCurrency = useCurrencyStore((s) => s.setCurrency);
|
||||
const formatAmount = useCurrencyStore((s) => s.formatAmount);
|
||||
const formatText = useCurrencyStore((s) => s.formatText);
|
||||
|
||||
// Derive display values from the stable `currency` string (no selector object)
|
||||
const option = CURRENCY_OPTIONS.find((o) => o.code === currency) ?? CURRENCY_OPTIONS[0];
|
||||
const rate = currency === 'USD' ? 0.272 : currency === 'INR' ? 25.135112 : 1;
|
||||
|
||||
return {
|
||||
currency,
|
||||
symbol: option.symbol,
|
||||
flag: option.flag,
|
||||
label: option.label,
|
||||
option,
|
||||
rate,
|
||||
formatAmount,
|
||||
formatText,
|
||||
setCurrency,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user