Built the Oracle Tab (#14)

This commit is contained in:
2026-04-11 19:35:45 +05:30
committed by Sagnik
parent 8e1ffe0e43
commit fb656d1443
54 changed files with 10651 additions and 818 deletions

View 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,
};
}