// UI primitives + small helpers (function () { const { useState, useEffect, useRef, useContext, createContext } = React; // ---- Currency ---- // Reference rate (May 2026): 1 THB ≈ 615 LAK // Rate is user-configurable; lives in localStorage 'ciga-rate'. const DEFAULT_RATE = 615; function loadRate() { try { const v = parseFloat(localStorage.getItem('ciga-rate')); return Number.isFinite(v) && v > 0 ? v : DEFAULT_RATE; } catch (e) { return DEFAULT_RATE; } } const CURRENCY = { LAK: { symbol: '₭', label: 'กีบ', code: 'LAK' }, THB: { symbol: '฿', label: 'บาท', code: 'THB' }, }; const CurrencyContext = createContext({ currency: 'LAK', setCurrency: () => {}, rate: DEFAULT_RATE, setRate: () => {}, }); function useCurrency() { return useContext(CurrencyContext); } // Convert LAK amount to display currency. function convertLAK(v, currency, rate = DEFAULT_RATE) { if (currency === 'THB') return v / rate; return v; } function fmtMoney(v, currency = 'LAK', opts = {}, rate = DEFAULT_RATE) { const { compact = false, sign = false, decimals } = opts; const c = CURRENCY[currency] || CURRENCY.LAK; const conv = convertLAK(v, currency, rate); const signStr = sign && conv > 0 ? '+' : (conv < 0 ? '−' : ''); const abs = Math.abs(conv); let body; if (compact) { body = fmtK(abs, currency === 'THB' ? 1 : 2); } else if (decimals != null) { body = abs.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } else { body = Math.round(abs).toLocaleString('en-US'); } return signStr + c.symbol + body; } function moneySymbol(currency = 'LAK') { return (CURRENCY[currency] || CURRENCY.LAK).symbol; } // useMoney() — convenience hook returning ready-to-use formatters bound to current context. function useMoney() { const { currency, rate } = useCurrency(); return { currency, rate, sym: moneySymbol(currency), M: (v, opts) => fmtMoney(v, currency, opts, rate), Mk: (v) => fmtMoney(v, currency, { compact: true }, rate), }; } function CurrencyToggle() { const { currency, setCurrency } = useCurrency(); return (
{Object.entries(CURRENCY).map(([id, c]) => ( ))}
); } // ---- formatters ---- function fmtK(v, decimals = 2) { const n = Math.abs(v); if (n >= 1_000_000_000) return (v / 1_000_000_000).toFixed(decimals) + 'B'; if (n >= 1_000_000) return (v / 1_000_000).toFixed(decimals) + 'M'; if (n >= 1_000) return (v / 1_000).toFixed(decimals === 2 ? 1 : decimals) + 'K'; return Math.round(v).toString(); } function fmtInt(v) { return Math.round(v).toLocaleString('en-US'); } function fmtLAK(v, opts = {}) { const { compact = false, decimals = 0, sign = false } = opts; if (compact) return '₭' + fmtK(v, 2); const s = Math.round(v).toLocaleString('en-US'); return (sign && v > 0 ? '+' : '') + '₭' + s; } function fmtDate(d, fmt = 'short') { const yy = d.getFullYear() + 543; // BE const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); if (fmt === 'short') return `${dd}/${mm}/${String(yy).slice(2)}`; if (fmt === 'iso') return `${d.getFullYear()}-${mm}-${dd}`; if (fmt === 'long') { const months = ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.','ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.']; return `${dd} ${months[d.getMonth()]} ${String(yy).slice(2)}`; } return d.toString(); } function fmtTime(d) { return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); } // ---- glass card ---- function Card({ children, className = '', style = {}, glow = false, ...rest }) { return (
{children}
); } // ---- pill ---- function Pill({ children, color, active, onClick, mono }) { return ( ); } // ---- icons ---- function Icon({ name, size = 16, stroke = 1.5 }) { const props = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'dashboard': return ; case 'invest': return ; case 'wallet': return ; case 'box': return ; case 'arrows': return ; case 'chart': return ; case 'plus': return ; case 'search': return ; case 'bell': return ; case 'arrow-up': return ; case 'arrow-down': return ; case 'trash': return ; case 'edit': return ; case 'calendar': return ; case 'settings': return ; case 'users': return ; case 'percent': return ; case 'pie': return ; case 'x': return ; case 'filter': return ; case 'chevron-down': return ; case 'bolt': return ; case 'circle': return ; case 'dot': return ; case 'pulse': return ; default: return null; } } // ---- live clock ---- function LiveClock() { const [now, setNow] = useState(new Date()); useEffect(() => { const t = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(t); }, []); const days = ['อา','จ','อ','พ','พฤ','ศ','ส']; return (
{days[now.getDay()]} {fmtDate(now, 'long')} {String(now.getHours()).padStart(2, '0')}:{String(now.getMinutes()).padStart(2, '0')}:{String(now.getSeconds()).padStart(2, '0')} VTE
); } // ---- Ticker (Bloomberg-ish scrolling bar) ---- function Ticker({ items }) { return (
{[...items, ...items].map((it, i) => ( {it.label} {it.value} = 0 ? '#22d3a3' : '#ff5a78' }}> {it.delta >= 0 ? '▲' : '▼'} {Math.abs(it.delta).toFixed(2)}% ))}
); } Object.assign(window, { Card, Pill, Icon, LiveClock, Ticker, fmtK, fmtInt, fmtLAK, fmtMoney, moneySymbol, fmtDate, fmtTime, CurrencyContext, useCurrency, useMoney, CurrencyToggle, CURRENCY, loadRate, DEFAULT_RATE }); })();