// ═══════════════════════════════════════════════════════════
// StoreOS v2 — API-driven frontend
// All data fetched from /api/* endpoints
// ═══════════════════════════════════════════════════════════
const API = '/api';
const STORE_ID = 1;
// ── State ────────────────────────────────────────────────────────────────────
let STATE = {
store: null,
products: [],
orders: [],
bundles: [],
discounts:[],
flash: [],
analytics: null,
customers: [],
cart: {},
wishlist: new Set(),
activeCatStore: 'All',
activeCatDash: 'All',
flashTimer: null,
flashSecondsLeft: 0,
};
// ── API helpers ───────────────────────────────────────────────────────────────
async function apiFetch(path, opts = {}) {
try {
const res = await fetch(API + path, {
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
...opts,
});
const data = await res.json();
if (!res.ok) console.warn('API error', path, data);
return { ok: res.ok, status: res.status, data };
} catch (e) {
console.error('Fetch failed', path, e);
return { ok: false, status: 0, data: null };
}
}
const apiGet = (path) => apiFetch(path);
const apiPost = (path, body) => apiFetch(path, { method: 'POST', body: JSON.stringify(body) });
const apiPut = (path, body) => apiFetch(path, { method: 'PUT', body: JSON.stringify(body) });
const apiPatch= (path, body={}) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) });
// ── Skeleton / loading helpers ────────────────────────────────────────────────
function skeletonCards(n = 6, h = '130px') {
return Array(n).fill(0).map(() => `
`).join('');
}
function skeletonRows(n = 5) {
return Array(n).fill(0).map(() => `
|
|
`).join('');
}
function skeletonStat() {
return `
`;
}
function skeletonStoreProducts(n = 8) {
return Array(n).fill(0).map(() => `
`).join('');
}
// ── View router ───────────────────────────────────────────────────────────────
function showView(id) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo(0, 0);
}
function switchTab(t, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('lf').style.display = t === 'login' ? 'block' : 'none';
document.getElementById('sf2').style.display = t === 'signup' ? 'block' : 'none';
}
// ── Landing → Dashboard ───────────────────────────────────────────────────────
async function goToDash() {
showView('view-dashboard');
sp('overview', document.querySelector('.ni'));
// Show skeletons immediately
document.querySelectorAll('.sg .sv').forEach(el => el.innerHTML = skeletonStat());
if (document.getElementById('pg')) document.getElementById('pg').innerHTML = skeletonCards();
if (document.getElementById('ob')) document.getElementById('ob').innerHTML = skeletonRows();
// Parallel fetches
await Promise.all([
loadAnalytics(),
loadProducts(),
loadOrders(),
loadCustomers(),
loadBundles(),
loadDiscounts(),
loadFlash(),
loadStore(),
]);
renderHours();
}
function goToStore() {
showView('view-store');
// show skeletons on storefront immediately
const spg = document.getElementById('spg');
if (spg) spg.innerHTML = skeletonStoreProducts(8).replace(/var\(--bg[234]\)/g, '#f5f2ee');
loadStoreData();
}
function goToOb() { showView('view-onboarding'); }
// ── Store data loaders ────────────────────────────────────────────────────────
async function loadStore() {
const r = await apiGet(`/store?id=${STORE_ID}`);
if (!r.ok) return;
STATE.store = r.data;
// update sidebar store badge
const nameEl = document.querySelector('.sb-store .pn');
const urlEl = document.querySelector('.sb-store .pst');
if (nameEl) nameEl.textContent = r.data.name;
if (urlEl) urlEl.textContent = `storeos.ng/${r.data.slug}`;
}
async function loadAnalytics() {
const r = await apiGet(`/analytics?store_id=${STORE_ID}&days=14`);
if (!r.ok) return;
STATE.analytics = r.data;
renderOverviewStats(r.data);
renderChartBars(r.data.chart?.data ?? []);
renderCategoryBreakdown(r.data.categories ?? []);
renderTopProducts(r.data.top_products ?? []);
renderAnalyticsPanel(r.data);
}
async function loadProducts() {
const r = await apiGet(`/products?store_id=${STORE_ID}&active=1&per_page=50`);
if (!r.ok) return;
STATE.products = r.data.data ?? [];
renderProds();
}
async function loadOrders(filter = 'all') {
const statusParam = filter === 'all' ? '' : `&status=${filter}`;
const r = await apiGet(`/orders?store_id=${STORE_ID}${statusParam}`);
if (!r.ok) return;
STATE.orders = r.data.data ?? [];
STATE.orderCounts = r.data.counts ?? {};
renderOrders();
renderRecentOrders();
// update order badge in sidebar
const badge = document.querySelector('.ni .ni-badge');
if (badge) badge.textContent = STATE.orderCounts['new'] ?? 0;
}
async function loadCustomers() {
const r = await apiGet(`/customers?store_id=${STORE_ID}&per_page=20`);
if (!r.ok) return;
STATE.customers = r.data.data ?? [];
STATE.customerStats = r.data.stats ?? {};
renderCustomers();
}
async function loadBundles() {
const r = await apiGet(`/bundles?store_id=${STORE_ID}`);
if (!r.ok) return;
STATE.bundles = r.data.data ?? [];
renderBundlesDash();
}
async function loadDiscounts() {
const r = await apiGet(`/discounts?store_id=${STORE_ID}`);
if (!r.ok) return;
STATE.discounts = r.data.data ?? [];
renderDiscounts();
}
async function loadFlash() {
const r = await apiGet(`/discounts?type=flash&store_id=${STORE_ID}`);
if (!r.ok) return;
STATE.flash = r.data.data ?? [];
renderFlashDash();
// update live banner
const live = STATE.flash.find(f => f.is_live);
if (live) startFlashCountdown(live.seconds_left, live.product_name, live.discount_pct);
}
async function loadStoreData() {
await Promise.all([
loadProducts(),
loadBundles(),
loadFlash(),
loadStore(),
]);
renderStoreFront();
}
// ── Renderers: Dashboard ──────────────────────────────────────────────────────
function fmt(kobo) {
return '₦' + Math.round(kobo / 100).toLocaleString();
}
function renderOverviewStats(data) {
const t = data.overview?.today ?? {};
const m = data.overview?.mtd ?? {};
const sg = document.querySelector('#panel-overview .sg');
if (!sg) return;
sg.innerHTML = `
Revenue today
${fmt(t.revenue??0)}
${(t.revenue_change_pct??0)>=0?'↑':'↓'} ${Math.abs(t.revenue_change_pct??0)}% vs yesterday
Orders today
${t.orders??0}
↑ Live
Avg order value
${t.orders>0?fmt(Math.round((t.revenue??0)/(t.orders||1))):'—'}
↑ Today
Wishlist saves
${Math.floor((t.visits??0)*0.05)}
↑ High intent
`;
}
function renderChartBars(rows) {
const svg = document.querySelector('#panel-overview .cc svg');
if (!svg || !rows.length) return;
const max = Math.max(...rows.map(r => r.revenue), 1);
const W = 560, H = 140, barW = Math.floor(W / rows.length) - 4;
svg.innerHTML = `
` +
rows.map((r, i) => {
const barH = Math.max(4, Math.round((r.revenue / max) * (H - 10)));
const x = i * (barW + 4) + 4;
const y = H - barH;
const isRecent = i >= rows.length - 2;
return ``;
}).join('');
}
function renderCategoryBreakdown(cats) {
const el = document.querySelector('#panel-overview .cr .cc:last-child');
if (!el) return;
const colors = ['var(--accent)','var(--blue)','var(--green)','var(--red)','var(--purple)'];
el.innerHTML = `Top categories
` +
cats.slice(0, 5).map((c, i) => `
`).join('');
}
function renderTopProducts(prods) {
const el = document.querySelector('#panel-overview .cr .cc:first-child');
if (!el || !prods.length) return;
const max = Math.max(...prods.map(p => p.sold), 1);
const bottomHtml = `Top products today
` +
prods.slice(0, 3).map(p => `
${p.emoji} ${p.name}${p.sold} sold
`).join('');
// append below chart svg
const existing = el.querySelector('svg');
if (existing) existing.insertAdjacentHTML('afterend', bottomHtml);
}
function renderRecentOrders() {
const tbody = document.querySelector('#panel-overview tbody');
if (!tbody) return;
const SM = {new:'bb',preparing:'ba',delivery:'bb',delivered:'bg',cancelled:'br2'};
const SL = {new:'New',preparing:'Preparing',delivery:'Out for delivery',delivered:'Delivered',cancelled:'Cancelled'};
tbody.innerHTML = STATE.orders.slice(0, 5).map(o => `
| ${o.order_id} |
${o.customer_name ?? 'Guest'} |
${fmt(o.total)} |
${SL[o.status]??o.status} |
${o.status==='delivery'?``:``} |
`).join('') || '| No orders yet |
';
}
function renderProds() {
const grid = document.getElementById('pg');
if (!grid) return;
if (!STATE.products.length) { grid.innerHTML = 'No products found.
'; return; }
grid.innerHTML = STATE.products.map(p => `
${p.badge?`${p.badge}`:''}${p.emoji}
${p.name}
${fmt(p.price)}
Stock: ${p.stock} · ${p.category}
`).join('');
}
function renderOrders() {
const tbody = document.getElementById('ob');
if (!tbody) return;
const SM = {new:'bb',preparing:'ba',delivery:'bb',delivered:'bg',cancelled:'br2'};
const SL = {new:'New',preparing:'Preparing',delivery:'Out for delivery',delivered:'Delivered',cancelled:'Cancelled'};
tbody.innerHTML = STATE.orders.map(o => {
const act = o.status==='new'
? ``
: o.status==='preparing'
? ``
: o.status==='delivery'
? ``
: ``;
return `
| ${o.order_id} |
${o.customer_name??'Guest'} |
${o.items_summary??'—'} |
${fmt(o.total)} |
${SL[o.status]??o.status} |
${act} |
`;
}).join('') || '| No orders |
';
}
function renderCustomers() {
const tbody = document.querySelector('#panel-customers .tc table tbody');
if (!tbody) return;
const SM = {new:'bg',regular:'bb',vip:'ba'};
tbody.innerHTML = STATE.customers.map(c => `
| ${c.name} |
${c.phone??'—'} |
${c.order_count} |
${fmt(c.total_spent)} |
${c.segment.toUpperCase()} |
`).join('');
// update stats
const sg = document.querySelector('#panel-customers .sg');
if (sg && STATE.customerStats) {
const s = STATE.customerStats;
sg.innerHTML = `
Total customers
${s.total??0}
↑ Growing
Avg lifetime value
${fmt(s.avg_lifetime_value??0)}
↑ Strong
Repeat rate
${s.repeat_rate_pct??0}%
↑ Healthy
`;
}
}
function renderBundlesDash() {
const tbody = document.querySelector('#panel-bundles .tc table tbody');
if (!tbody) return;
tbody.innerHTML = STATE.bundles.map(b => `
| ${b.name} |
${b.items?.map(i=>`${i.emoji}${i.name}`).join(' + ')??'—'} |
${fmt(b.original_price)} |
${fmt(b.bundle_price)} |
${fmt(b.savings)} |
— |
${b.is_active?'Active':'Draft'} |
`).join('');
}
function renderDiscounts() {
const tbody = document.querySelector('#panel-discounts .tc table tbody');
if (!tbody) return;
tbody.innerHTML = STATE.discounts.map(d => `
| ${d.code} |
${d.type} |
${d.type==='percentage'?d.value+'%':d.type==='fixed'?fmt(d.value):'Free delivery'} |
${d.usage_count} / ${d.usage_limit??'∞'} |
${d.expires_at?d.expires_at.slice(0,10):'Never'} |
${d.is_active?'Active':'Inactive'} |
`).join('');
}
function renderFlashDash() {
const tbody = document.querySelector('#panel-flash .tc table tbody');
if (!tbody) return;
tbody.innerHTML = STATE.flash.map(f => `
| ${f.product_emoji} ${f.product_name} |
${f.discount_pct}% off |
${f.starts_at?.slice(11,16)??'—'} |
${f.ends_at?.slice(11,16)??'—'} |
${f.orders_count} |
${fmt(f.revenue)} |
${f.is_live?'Live now':'Ended'} |
`).join('');
}
function renderAnalyticsPanel(data) {
const sg = document.querySelector('#panel-analytics .sg');
if (!sg) return;
const mtd = data.overview?.mtd ?? {};
const ac = data.abandoned_carts ?? {};
sg.innerHTML = `
Monthly revenue
${fmt(mtd.revenue??0)}
${(mtd.revenue_change_pct??0)>=0?'↑':'↓'} ${Math.abs(mtd.revenue_change_pct??0)}% vs last month
Orders (MTD)
${mtd.orders??0}
↑ Growing
Abandoned carts
${ac.recovered??0} recovered
↑ ${fmt(ac.revenue_recovered??0)} recovered
Store visits
${(mtd.visits??0).toLocaleString()}
↑ Organic
`;
// traffic sources
const traf = document.querySelector('#panel-analytics .ag .cc:last-child');
if (traf) {
const sources = data.traffic_sources ?? [];
const maxV = Math.max(...sources.map(s => s.visits), 1);
traf.innerHTML = `Traffic sources
` +
sources.map(s => `
${s.source}${s.visits.toLocaleString()}
`).join('');
}
// reviews
const revEl = document.querySelector('#panel-analytics .ag .cc:nth-child(3)');
const rev = data.reviews ?? {};
if (revEl && rev.total_reviews) {
revEl.querySelector('.ct') && (revEl.querySelector('.ct').textContent = 'Reviews summary');
const ratingEl = revEl.querySelector('[style*="font-size: 36px"], [style*="font-size:36px"]');
if (ratingEl) ratingEl.innerHTML = `${rev.avg_rating} / 5 · ${rev.total_reviews} reviews`;
}
}
// ── Renderers: Storefront ─────────────────────────────────────────────────────
function renderStoreFront() {
const prods = STATE.products;
const bundles = STATE.bundles;
// Hero cards (first 4 products)
const hv = document.getElementById('hv');
if (hv) {
hv.innerHTML = prods.slice(0, 4).map(p => `
${p.emoji}
${p.name}
${fmt(p.price)}
${'★'.repeat(Math.round(p.rating??0))} (${p.review_count??0})
`).join('');
}
// Category pills
const cats = ['All', ...new Set(prods.map(p => p.category))];
const cpills = document.getElementById('cpills');
if (cpills) {
cpills.innerHTML = cats.map(c =>
`${c}
`
).join('');
}
renderStoreProds();
// Bundles section
const bg = document.querySelector('.bundle-grid');
if (bg && bundles.length) {
bg.innerHTML = bundles.map(b => `
${b.emojis}
${b.name}
${b.items?.map(i=>i.name).join(' + ')??b.description??''}
${fmt(b.original_price)}
${fmt(b.bundle_price)}
Save ${fmt(b.savings)}
`).join('');
}
// Flash banner
const live = STATE.flash.find(f => f.is_live);
if (live) startFlashCountdown(live.seconds_left, live.product_name, live.discount_pct);
}
function filterCat(cat, el) {
STATE.activeCatStore = cat;
document.querySelectorAll('.cpill').forEach(p => p.classList.remove('active'));
el.classList.add('active');
renderStoreProds();
}
function renderStoreProds() {
const filtered = STATE.activeCatStore === 'All'
? STATE.products
: STATE.products.filter(p => p.category === STATE.activeCatStore);
const grid = document.getElementById('spg');
if (!grid) return;
if (!filtered.length) {
grid.innerHTML = 'No products in this category.
';
return;
}
grid.innerHTML = filtered.map(p => `
${p.badge ? `
${p.badge}
` : ''}
${p.emoji}
${p.category}
${p.name}
${p.description??''}
${'★'.repeat(Math.floor(p.rating??0))}
${p.rating??'—'} (${p.review_count??0} reviews)
${fmt(p.price)}
`).join('');
}
// ── Flash countdown ───────────────────────────────────────────────────────────
function startFlashCountdown(seconds, productName, discountPct) {
STATE.flashSecondsLeft = seconds;
if (STATE.flashTimer) clearInterval(STATE.flashTimer);
const flashText = document.querySelector('.flash-banner .flash-text strong');
if (flashText) flashText.textContent = `${discountPct}% off ${productName}`;
STATE.flashTimer = setInterval(() => {
STATE.flashSecondsLeft = Math.max(0, STATE.flashSecondsLeft - 1);
const h = Math.floor(STATE.flashSecondsLeft / 3600);
const m = Math.floor((STATE.flashSecondsLeft % 3600) / 60);
const s = STATE.flashSecondsLeft % 60;
const fh = document.getElementById('fc-h');
const fm = document.getElementById('fc-m');
const fs = document.getElementById('fc-s');
if (fh) fh.textContent = String(h).padStart(2,'0');
if (fm) fm.textContent = String(m).padStart(2,'0');
if (fs) fs.textContent = String(s).padStart(2,'0');
if (STATE.flashSecondsLeft === 0) clearInterval(STATE.flashTimer);
}, 1000);
}
// ── Order actions ─────────────────────────────────────────────────────────────
async function acceptOrder(id) {
await apiPatch(`/orders?id=${id}&action=accept`);
await loadOrders();
toast('Order accepted — now preparing');
}
async function readyOrder(id) {
await apiPatch(`/orders?id=${id}&action=ready`);
await loadOrders();
toast('Marked as out for delivery');
}
async function fo(filter, btn) {
document.querySelectorAll('.fp').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
await loadOrders(filter);
}
// ── Product actions ───────────────────────────────────────────────────────────
async function hideProduct(id) {
await apiFetch(`/products?id=${id}`, { method: 'DELETE' });
STATE.products = STATE.products.filter(p => p.id !== id);
renderProds();
toast('Product hidden from store');
}
async function genAIDescForProduct(name, category) {
document.getElementById('ai-input').value = name;
document.getElementById('aim').classList.add('open');
await genAIDesc(name, category);
}
async function genAIDesc(name, category) {
const input = document.getElementById('ai-input');
const res = document.getElementById('ai-result');
const n = name || input?.value?.trim() || 'Product';
const cat = category || '';
res.style.display = 'block';
res.className = 'ai-result ai-typing';
res.textContent = 'Generating...';
const r = await apiPost('/ai-description', { product_name: n, category: cat, store_name: STATE.store?.name ?? 'our store' });
res.className = 'ai-result';
res.textContent = r.ok ? (r.data.description ?? 'Could not generate.') : 'Error generating description.';
}
// ── Flash sale launcher ───────────────────────────────────────────────────────
async function launchFlash() {
const prodSel = document.querySelector('#panel-flash select:first-of-type');
const discSel = document.querySelector('#panel-flash input[type="number"]');
const durSel = document.querySelector('#panel-flash select:last-of-type');
const pct = parseInt(discSel?.value ?? 25);
const dur = parseInt(durSel?.value ?? 2);
await apiPost('/discounts?type=flash&store_id=' + STORE_ID, {
discount_pct: pct, duration_hours: dur,
});
await loadFlash();
toast('Flash sale launched! Countdown banner live on your store.');
}
// ── Cart ──────────────────────────────────────────────────────────────────────
function addToCart(id) {
STATE.cart[id] = (STATE.cart[id] || 0) + 1;
updateCartUI();
}
function addBundle(ids) { ids.forEach(id => addToCart(id)); }
function removeFromCart(id) {
if (STATE.cart[id] > 1) STATE.cart[id]--;
else delete STATE.cart[id];
updateCartUI();
}
function updateCartUI() {
const total = Object.values(STATE.cart).reduce((a, b) => a + b, 0);
['cc3','cc4'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = total; });
const items = Object.entries(STATE.cart);
const listEl = document.getElementById('cil');
const footerEl = document.getElementById('cff');
if (!listEl) return;
if (!items.length) {
listEl.innerHTML = 'Your cart is empty.
Add something delicious!
';
if (footerEl) footerEl.style.display = 'none';
return;
}
if (footerEl) footerEl.style.display = 'block';
let sub = 0;
listEl.innerHTML = items.map(([id, qty]) => {
const p = STATE.products.find(x => x.id === +id);
if (!p) return '';
sub += p.price * qty;
return `
${p.emoji}
${p.name}
${fmt(p.price)}
${qty}
`;
}).join('');
const subEl = document.getElementById('csub');
const totEl = document.getElementById('ctot');
if (subEl) subEl.textContent = fmt(sub);
if (totEl) totEl.textContent = fmt(sub + 150000);
}
function toggleCart() {
document.getElementById('co').classList.toggle('open');
document.getElementById('cd').classList.toggle('open');
}
async function checkout() {
const items = Object.entries(STATE.cart).map(([id, qty]) => ({ product_id: +id, quantity: qty }));
if (!items.length) return;
const r = await apiPost('/orders?store_id=' + STORE_ID, {
items,
customer_name: 'Guest',
delivery_type: 'delivery',
});
if (r.ok) {
STATE.cart = {};
updateCartUI();
toggleCart();
toast('Order placed! You\'ll get a WhatsApp update shortly 🎉');
} else {
toast('Something went wrong. Please try again.');
}
}
function waCheckout() {
const items = Object.entries(STATE.cart).map(([id, qty]) => {
const p = STATE.products.find(x => x.id === +id);
return `${qty}× ${p?.name ?? id}`;
}).join(', ');
toast('Opening WhatsApp with: ' + items);
STATE.cart = {};
updateCartUI();
toggleCart();
}
function waOrder() { toast('Opening WhatsApp to send your order...'); }
function reorder() {
const first = STATE.products[0];
const second = STATE.products[6]; // chapman
if (first) addToCart(first.id);
if (second) addToCart(second.id);
toggleCart();
toast('Previous order added to cart!');
}
// ── Promo code ────────────────────────────────────────────────────────────────
async function applyPromo() {
const code = document.getElementById('promo-input')?.value?.trim();
if (!code) return;
const r = await apiGet(`/discounts?store_id=${STORE_ID}&action=validate&code=${encodeURIComponent(code)}`);
if (r.ok) {
toast('✓ Promo applied: ' + code);
} else {
toast('Code not found or expired. Try WELCOME20');
}
}
// ── Wishlist ──────────────────────────────────────────────────────────────────
function toggleWL(id, btn) {
if (STATE.wishlist.has(id)) {
STATE.wishlist.delete(id); btn.innerHTML = '♡'; btn.classList.remove('saved');
toast('Removed from wishlist');
} else {
STATE.wishlist.add(id); btn.innerHTML = '♥'; btn.classList.add('saved');
toast('Added to wishlist ♥');
}
const c = document.getElementById('wl-count');
if (c) c.textContent = STATE.wishlist.size > 0 ? `(${STATE.wishlist.size})` : '';
}
function showWishlist() {
showView('view-wishlist');
const wlc = document.getElementById('wl-content');
const hdr = document.getElementById('wl-header-count');
if (!STATE.wishlist.size) {
if (hdr) hdr.textContent = '0 items';
if (wlc) wlc.innerHTML = 'Your wishlist is empty.
Save items you love by tapping ♡ on any product.
';
return;
}
if (hdr) hdr.textContent = `${STATE.wishlist.size} saved items`;
const prods = STATE.products.filter(p => STATE.wishlist.has(p.id));
if (wlc) wlc.innerHTML = `${prods.map(p => `
${p.emoji}
${p.category}
${p.name}
${p.description??''}
${'★'.repeat(Math.floor(p.rating??0))}${p.rating} (${p.review_count})
${fmt(p.price)}
`).join('')}
`;
const c = document.getElementById('cc4');
if (c) c.textContent = Object.values(STATE.cart).reduce((a,b)=>a+b,0);
}
// ── Misc ──────────────────────────────────────────────────────────────────────
function renderHours() {
const grid = document.getElementById('hours-grid');
if (!grid) return;
const days = [['mon','Monday'],['tue','Tuesday'],['wed','Wednesday'],['thu','Thursday'],
['fri','Friday'],['sat','Saturday'],['sun','Sunday']];
const oh = STATE.store?.opening_hours ?? {};
grid.innerHTML = days.map(([key, label]) => {
const h = oh[key] ?? { open:'11:00', close:'22:00', enabled: true };
return ``;
}).join('');
}
function sp(name, el) {
document.querySelectorAll('.dp').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.ni').forEach(n => n.classList.remove('active'));
document.getElementById('panel-' + name)?.classList.add('active');
if (el) el.classList.add('active');
}
function selTheme(el, t) { document.querySelectorAll('#obs1 .theme-card').forEach(c => c.classList.remove('sel')); el.classList.add('sel'); }
function selTheme2(el) { document.querySelectorAll('#panel-themes .theme-card').forEach(c => c.classList.remove('sel')); el.classList.add('sel'); toast('Theme updated!'); }
function selEmoji(el) { document.querySelectorAll('.eopt').forEach(e => e.classList.remove('sel')); el.classList.add('sel'); }
function showTracker() { document.getElementById('otm').classList.add('open'); }
function showQR() { document.getElementById('qrm').classList.add('open'); }
function openAI() { document.getElementById('aim').classList.add('open'); }
function toast(msg) { const t=document.getElementById('toast-el'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3000); }
let obStep = 0;
function obGo(n) {
document.getElementById('obs'+obStep).style.display='none';
document.getElementById('od'+obStep).classList.remove('active');
document.getElementById('od'+obStep).classList.add('done');
obStep = n;
if (n < 4) { document.getElementById('obs'+n).style.display='block'; document.getElementById('od'+n).classList.add('active'); }
else goToDash();
}
// ── Shimmer animation (injected) ──────────────────────────────────────────────
const shimmerStyle = document.createElement('style');
shimmerStyle.textContent = `
@keyframes shimmer{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}
@keyframes storeShimmer{0%{opacity:1}50%{opacity:.6}100%{opacity:1}}`;
document.head.appendChild(shimmerStyle);
// ── Boot ──────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
// Show landing and immediately render skeleton UI behind it
showView('view-landing');
});