// /account.html — customer order history. Visually mirrors the storefront:
// dot-pulse status indicators, octagonal CornerCuts on cards, spotlight-style
// hero strip for the account header, and the same sec-head/sec-tag rhythm
// used on the catalog & FAQ sections.
const { useState, useEffect } = React;
function AccountApp() {
const { user, isAdmin } = window.useAuthState();
const ready = window.ExecAuth && window.ExecAuth.isReady();
const [orders, setOrders] = useState(null);
const [revealed, setRevealed] = useState({});
const [banner, setBanner] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const paid = params.get('paid');
if (paid === 'ok' || paid === 'cancel') {
setBanner(paid);
params.delete('paid'); params.delete('sid');
const qs = params.toString();
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : ''));
}
}, []);
useEffect(() => {
if (!user) { setOrders([]); return; }
let cancelled = false;
const load = () =>
fetch('/api/orders', { credentials: 'same-origin' })
.then((r) => r.ok ? r.json() : Promise.reject(r.status))
.then((d) => { if (!cancelled) setOrders(d.orders || []); })
.catch(() => { if (!cancelled) setOrders([]); });
load();
// Stripe webhook may land a beat after the redirect; second fetch catches it.
if (banner === 'ok') {
const t = setTimeout(load, 2500);
return () => { cancelled = true; clearTimeout(t); };
}
return () => { cancelled = true; };
}, [user, banner]);
const reveal = async (itemId) => {
try {
const res = await fetch(`/api/orders/${itemId}/reveal-key`, { method: 'POST', credentials: 'same-origin' });
const data = await res.json().catch(() => ({}));
if (!res.ok) { alert('Reveal failed: ' + (data.error || res.status)); return; }
setRevealed((r) => ({ ...r, [itemId]: data.key }));
} catch { alert('Reveal failed: network error'); }
};
if (!ready) {
return ;
}
if (!user) {
return (
);
}
const totalSpent = (orders || [])
.filter((o) => o.status === 'paid')
.reduce((s, o) => s + (o.amount_cents || 0), 0);
const pendingKeys = pendingKeyCount(orders);
return (
{banner && setBanner(null)}/>}
0}/>
// 01
ORDER HISTORY / {orders === null ? '…' : orders.length}
{orders === null && }
{orders !== null && orders.length === 0 && (
BROWSE CATALOG }
/>
)}
{orders !== null && orders.length > 0 && (
{orders.map((o) => (
))}
)}
);
}
// ---------- Order card ----------
function OrderCard({ order, revealed, onReveal }) {
const dotClass = order.status === 'paid' ? 'pulse' : '';
return (
{order.status.toUpperCase()}
ORDER · #{String(order.id).padStart(4, '0')}
{fmtDate(order.created_at)}
{window.fmt((order.amount_cents || 0) / 100)}
);
}
// ---------- Hero strip (spotlight-style) ----------
function HeroStrip({ user, isAdmin, children }) {
// Some admin sub-views (e.g. WEBSITE tab) reuse the stat slot without a
// logged-in user prop. Guard so passing null doesn't crash the entire
// panel; the identity block just renders as anonymous.
const name = user ? (user.global_name || user.username || '—') : 'ADMIN';
return (
ACCOUNT // AUTHENTICATED
{user ? (
{user.avatar_url ? (
) : (
{(name[0] || '?').toUpperCase()}
)}
{name}
@{user.username}
{isAdmin && ADMIN }
DISCORD · {user.discord_id}
) : (
{name}
{isAdmin && ADMIN }
)}
{children}
);
}
function StatColumn({ label, value, accent }) {
return (
);
}
// ---------- Page chrome (nav + main) ----------
function PageChrome({ user, isAdmin, currentTab, children }) {
const [navOpen, setNavOpen] = useState(false);
return (
);
}
function LoadingState() {
return (
LOADING…
);
}
function EmptyPanel({ art, line, sub, cta }) {
return (
{art}
{line}
{sub && {sub}
}
{cta && {cta}
}
);
}
function LoggedOutCTA() {
return (
⚿
LOGIN REQUIRED
Authenticate with Discord to view your orders.
window.ExecAuth.login()}>
LOG IN WITH DISCORD
);
}
function InlineBanner({ kind, onClose }) {
useEffect(() => {
if (kind !== 'ok') return;
const t = setTimeout(onClose, 8000);
return () => clearTimeout(t);
}, [kind, onClose]);
return (
{kind === 'ok'
? 'PAYMENT RECEIVED — your order is below'
: 'CHECKOUT CANCELLED — no charge was made'}
×
);
}
// ---------- helpers ----------
function fmtDate(epoch) {
if (!epoch) return '—';
const d = new Date(epoch * 1000);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
}
function pendingKeyCount(orders) {
if (!orders) return 0;
let n = 0;
for (const o of orders) {
if (o.status !== 'paid') continue;
for (const it of o.items) if (!it.has_key) n++;
}
return n;
}
// Expose shared shell pieces so admin.jsx can reuse them.
window.PageChrome = PageChrome;
window.HeroStrip = HeroStrip;
window.StatColumn = StatColumn;
window.LoadingState = LoadingState;
window.EmptyPanel = EmptyPanel;
window.LoggedOutCTA = LoggedOutCTA;
window.InlineBanner = InlineBanner;
window.fmtDate = fmtDate;
const __root = document.getElementById('account-root');
if (__root) ReactDOM.createRoot(__root).render( );