// 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 });
})();