// rewards.jsx โ€” Tukar Poin: katalog voucher, redeem, kode, dompet warga. Global. function discountLabel(v) { if (!v) return ''; if (v.discount_type === 'amount') return '-Rp' + Math.round(v.discount_value || 0).toLocaleString('id-ID'); if (v.discount_type === 'percent') return '-' + (v.discount_value || 0) + '%'; return ''; } function fmtDateID(d) { if (!d) return ''; try { return new Date(d).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }); } catch (e) { return d; } } function copyText(t) { try { if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(t); } catch (e) {} try { var ta = document.createElement('textarea'); ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } catch (e) {} return Promise.resolve(); } // ---------- copy button (kode) ---------- function CopyBtn({ code, style, big }) { const [done, setDone] = React.useState(false); const onCopy = () => { copyText(code); setDone(true); setTimeout(() => setDone(false), 1600); }; return ( ); } // ---------- voucher card (photo-forward) ---------- function VoucherCard({ v, points, onRedeem }) { const dc = DIR_CATS.find(d => d.key === v.category) || DIR_CATS[0]; const afford = points >= v.points_cost; const kurang = v.points_cost - points; return ( ); } // ---------- redeem confirm sheet ---------- function RedeemSheet({ v, points, busy, err, onConfirm, onClose }) { const after = points - v.points_cost; const afford = after >= 0; return (
e.stopPropagation()} style={{ background: 'var(--surface)', borderTopLeftRadius: 'var(--r-lg)', borderTopRightRadius: 'var(--r-lg)', padding: '10px 20px', paddingBottom: 'max(22px, env(safe-area-inset-bottom))', boxShadow: 'var(--sh-3)', animation: 'gsSheetUp 280ms var(--ease)' }}>

Tukar voucher?

{v.title}{v.place_name ? ' ยท ' + v.place_name : ''}

Saldo sekarang {points.toLocaleString('id-ID')} Sesudah {after.toLocaleString('id-ID')} -{v.points_cost}
{err &&
{err}
}
); } // ---------- redeem success (kode besar) ---------- function RedeemSuccess({ result, onClose }) { return (
๐ŸŽ

Voucher ditukar!

{result.title}{result.place_name ? ' ยท ' + result.place_name : ''}

KODE VOUCHER
{result.code}
Tunjukkan kode ini ke kasir
Sisa poinmu: {(result.sisa_poin || 0).toLocaleString('id-ID')}
); } // ---------- wallet row (Voucherku) ---------- function WalletRow({ r }) { const active = r.status === 'active'; return (
{active ? '๐ŸŽŸ๏ธ' : (r.status === 'used' ? 'โœ…' : 'โŒ›')} {r.title} {r.place_name || 'gs.town'} ยท {discountLabel(r) || (r.points_cost + ' poin')} {active ? {r.code} : {r.code}} {active ? : {r.status === 'used' ? 'DIPAKAI' : 'HANGUS'}}
); } // ---------- rewards screen (list + wallet) ---------- function RewardsScreen({ profile, authed, onRequireAuth, onClose }) { const [tab, setTab] = React.useState('katalog'); const [rows, setRows] = React.useState(null); const [notReady, setNotReady] = React.useState(false); const [wallet, setWallet] = React.useState(null); const [sel, setSel] = React.useState(null); // voucher yang mau ditukar const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(''); const [success, setSuccess] = React.useState(null); // hasil redeem const [points, setPoints] = React.useState((profile && profile.points) || 0); const loadCatalog = React.useCallback(() => { setRows(null); gsData.loadVouchers().then(r => { if (r.notReady) { setNotReady(true); setRows([]); } else { setNotReady(false); setRows(r.rows); } }); }, []); const loadWallet = React.useCallback(() => { setWallet(null); gsData.myWallet().then(w => { if (w.notReady) { setNotReady(true); setWallet({ points: points, redemptions: [] }); } else { setWallet(w); setPoints(w.points); } }); }, [points]); React.useEffect(() => { loadCatalog(); }, []); React.useEffect(() => { if (authed) gsData.myWallet().then(w => { if (!w.notReady) setPoints(w.points); }); }, [authed]); React.useEffect(() => { if (tab === 'dompet' && wallet === null) loadWallet(); }, [tab]); const onRedeem = (v) => { if (!authed) { onRequireAuth && onRequireAuth(); return; } setErr(''); setSel(v); }; const confirmRedeem = async () => { setBusy(true); setErr(''); const res = await gsData.redeemVoucher(sel.id); setBusy(false); if (res.notReady) { setErr('Fitur tukar poin segera aktif.'); return; } if (res.error) { setErr(res.error); return; } setPoints(res.data.sisa_poin); setSel(null); setSuccess(res.data); setWallet(null); // paksa reload dompet nanti loadCatalog(); // sisa kuota bisa berubah }; const closeSuccess = () => { setSuccess(null); setTab('dompet'); }; const empty = rows && rows.length === 0 && !notReady; return (
{/* header */}

Tukar Poin

{points.toLocaleString('id-ID')}
{[['katalog', 'Voucher'], ['dompet', 'Voucherku']].map(([k, l]) => ( ))}
{tab === 'katalog' ? ( rows === null ? (
{[0, 1, 2].map(i => )}
) : notReady ? ( ) : empty ? ( ) : (
{rows.map(v => )}
) ) : ( !authed ? ( onRequireAuth && onRequireAuth()} /> ) : wallet === null ? (
{[0, 1, 2].map(i => )}
) : wallet.redemptions.length === 0 ? ( setTab('katalog')} /> ) : ( (() => { const act = wallet.redemptions.filter(r => r.status === 'active'); const hist = wallet.redemptions.filter(r => r.status !== 'active'); return (
{act.length > 0 && (
AKTIF ยท {act.length}
{act.map(r => )}
)} {hist.length > 0 && (
RIWAYAT
{hist.map(r => )}
)}
); })() ) )}
{sel && { if (!busy) { setSel(null); setErr(''); } }} />} {success && }
); } Object.assign(window, { RewardsScreen, VoucherCard, RedeemSheet, RedeemSuccess, WalletRow, CopyBtn, discountLabel });