// Florio Analytics — Shell (sidebar + top bar + auth gate). Light theme. (function () { const I = window.HyperIcons; const { useState } = React; // Dashboard tabs (rebuilt to the new design) + operational tools (re-skinned). const NAV_DASH = [ { id: 'overview', label: 'Overview', icon: 'grid' }, { id: 'onboarding', label: 'Onboarding', icon: 'funnel' }, { id: 'gamification', label: 'Gamification', icon: 'trophy' }, { id: 'users', label: 'Users', icon: 'users' }, ]; const NAV_TOOLS = [ { id: 'notifications', label: 'Notifications', icon: 'bell' }, { id: 'traits', label: 'Traits', icon: 'layers' }, { id: 'labeling', label: 'Labeling', icon: 'target' }, { id: 'labels', label: 'Labels', icon: 'leaf' }, { id: 'gaps', label: 'Avatar Gaps', icon: 'search' }, { id: 'live', label: 'Live / TV', icon: 'monitor' }, { id: 'shop', label: 'Shop', icon: 'coin' }, { id: 'feedback', label: 'Feedback', icon: 'heart' }, ]; const SUBS = { overview: 'Product health across users, sessions and engagement.', onboarding: 'Step-by-step funnel from install to paid.', gamification: 'Streaks, levels, XP and the in-app economy.', users: 'At-risk & churned users: inactive, trial cancels, sub cancels.', notifications: 'Notification permissions and delivery.', traits: 'Plant trait extraction and avatar mapping.', labeling: 'Human review of scanned plants.', labels: 'Labelled plant dataset.', gaps: 'Avatar catalog coverage gaps.', live: 'Live activity, for the big screen.', shop: 'Cosmetics economy and purchases.', feedback: 'In-app feedback and ratings.', }; const TITLES = { overview: 'Overview', onboarding: 'Onboarding', gamification: 'Gamification', users: 'Users', notifications: 'Notifications', traits: 'Traits', labeling: 'Labeling', labels: 'Labels', gaps: 'Avatar Gaps', live: 'Live / TV', shop: 'Shop', feedback: 'Feedback', }; function NavItem({ item, active, onClick }) { const [h, setH] = useState(false); return React.createElement('button', { onClick, onMouseEnter: () => setH(true), onMouseLeave: () => setH(false), style: { display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '10px 14px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: active ? 'var(--app-accent)' : h ? 'var(--surface-sunken)' : 'transparent', color: active ? '#fff' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontWeight: active ? 600 : 500, fontSize: 14, transition: 'background var(--dur-fast), color var(--dur-fast)', textAlign: 'left', }, }, (I[item.icon] || I.grid)({ size: 18 }), item.label); } function SectionLabel({ children }) { return React.createElement('div', { style: { fontFamily: 'var(--font-mono-display)', fontSize: 10, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--text-muted)', padding: '14px 8px 8px' }, }, children); } function Sidebar({ active, onNav }) { return React.createElement('aside', { style: { width: 244, flexShrink: 0, height: '100%', boxSizing: 'border-box', background: 'var(--surface-card)', borderRight: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column', padding: '22px 16px', gap: 2, overflowY: 'auto', }, }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '4px 8px 14px' } }, React.createElement('img', { src: '../newDesign/ui_kits/plant-analytics/assets/logo.png', style: { width: 42, height: 42, borderRadius: 12, boxShadow: 'var(--shadow-sm)' }, onError: (e) => { e.target.style.display = 'none'; } }), React.createElement('div', { style: { display: 'flex', flexDirection: 'column', lineHeight: 1.1 } }, React.createElement('span', { style: { fontFamily: 'var(--font-sans)', fontWeight: 800, fontSize: 16, color: 'var(--text-primary)', letterSpacing: '-.01em' } }, 'Florio'), React.createElement('span', { style: { fontFamily: 'var(--font-mono-display)', fontSize: 9, letterSpacing: '.18em', textTransform: 'uppercase', color: 'var(--text-muted)', marginTop: 2 } }, 'Analytics'), ), ), React.createElement(SectionLabel, null, 'Dashboards'), ...NAV_DASH.map((n) => React.createElement(NavItem, { key: n.id, item: n, active: active === n.id, onClick: () => onNav(n.id) })), React.createElement(SectionLabel, null, 'Tools'), ...NAV_TOOLS.map((n) => React.createElement(NavItem, { key: n.id, item: n, active: active === n.id, onClick: () => onNav(n.id) })), React.createElement('div', { style: { marginTop: 'auto', paddingTop: 14 } }, React.createElement('button', { onClick: () => { try { window.FlorioAuth.signOut().finally(() => { sessionStorage.removeItem('florio_dash_auth'); location.reload(); }); } catch (e) { location.reload(); } }, style: { display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '10px 14px', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)', cursor: 'pointer', background: 'transparent', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontWeight: 500, fontSize: 14, textAlign: 'left' }, }, I.settings({ size: 18 }), 'Log out'), ), ); } const e = React.createElement; const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const fmtLocal = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const prettyDay = (s) => { const d = new Date(s + 'T00:00:00'); return isNaN(d.getTime()) ? s : `${d.getDate()} ${MON[d.getMonth()]}`; }; // Human label for the current range object ({ key } preset or custom). function rangeLabel(range) { if (range && range.key === 'custom') return range.label || (range.start + ' → ' + range.end); const key = (range && range.key) || range; return { Day: 'Today', Week: 'Last 7 days', Month: 'Last 30 days', Year: 'Last 12 months' }[key] || key; } function Segmented({ options, value, onChange }) { return e('div', { style: { display: 'inline-flex', background: 'var(--surface-sunken)', borderRadius: 999, padding: 3, gap: 2 } }, options.map((o) => e('button', { key: o, onClick: () => onChange(o), style: { border: 'none', cursor: 'pointer', padding: '7px 15px', borderRadius: 999, fontFamily: 'var(--font-ui-alt)', fontWeight: 600, fontSize: 12.5, background: value === o ? 'var(--surface-card)' : 'transparent', color: value === o ? 'var(--text-primary)' : 'var(--text-muted)', boxShadow: value === o ? 'var(--shadow-sm)' : 'none', transition: 'all var(--dur-fast)', }, }, o))); } // Pill that shows the active range + opens a custom From/To date picker. function RangeControl({ range, onRange }) { const [open, setOpen] = useState(false); const [from, setFrom] = useState(''); const [to, setTo] = useState(''); const isCustom = range && range.key === 'custom'; const openPanel = () => { const today = new Date(); const sd = new Date(); sd.setDate(sd.getDate() - 29); setFrom(isCustom ? range.start : fmtLocal(sd)); setTo(isCustom ? range.end : fmtLocal(today)); setOpen(true); }; const apply = () => { if (!from || !to) return; let s = from, en = to; if (s > en) { const t = s; s = en; en = t; } onRange({ key: 'custom', start: s, end: en, label: prettyDay(s) + ' – ' + prettyDay(en) }); setOpen(false); }; const field = (lab, val, setter) => e('label', { style: { display: 'flex', flexDirection: 'column', gap: 4, flex: 1 } }, e('span', { style: { fontFamily: 'var(--font-sans)', fontSize: 11, color: 'var(--text-muted)' } }, lab), e('input', { type: 'date', value: val, max: fmtLocal(new Date()), onChange: (ev) => setter(ev.target.value), style: { height: 38, borderRadius: 'var(--radius-md)', border: '1px solid var(--border-default)', padding: '0 10px', fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text-primary)', background: 'var(--surface-card)', outline: 'none' } })); return e('div', { style: { position: 'relative' } }, e('button', { onClick: () => (open ? setOpen(false) : openPanel()), style: { display: 'flex', alignItems: 'center', gap: 9, padding: '0 13px', height: 42, borderRadius: 'var(--radius-md)', border: '1px solid ' + (isCustom ? 'var(--app-accent, #8676FF)' : 'var(--border-subtle)'), background: 'var(--surface-card)', color: isCustom ? 'var(--app-accent, #8676FF)' : 'var(--text-muted)', cursor: 'pointer' }, }, I.calendar({ size: 17 }), e('span', { style: { fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: isCustom ? 700 : 500 } }, rangeLabel(range)), I.chevron ? I.chevron({ size: 14 }) : null, ), open ? e('div', { style: { position: 'absolute', top: 48, right: 0, zIndex: 50, background: 'var(--surface-card)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-card)', padding: 16, width: 300, display: 'flex', flexDirection: 'column', gap: 12 } }, e('div', { style: { fontFamily: 'var(--font-mono-display)', fontSize: 10, letterSpacing: '.12em', textTransform: 'uppercase', color: 'var(--text-muted)' } }, 'Custom date range'), e('div', { style: { display: 'flex', gap: 10 } }, field('From', from, setFrom), field('To', to, setTo)), e('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, e('button', { onClick: () => setOpen(false), style: { padding: '8px 14px', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-default)', background: 'var(--surface-card)', color: 'var(--text-muted)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600 } }, 'Cancel'), e('button', { onClick: apply, style: { padding: '8px 16px', borderRadius: 'var(--radius-md)', border: 'none', background: 'var(--app-accent, #8676FF)', color: '#fff', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 700 } }, 'Apply'), ), ) : null, ); } function TopBar({ title, sub, range, onRange }) { const activeKey = (range && range.key === 'custom') ? null : ((range && range.key) || range); return e('header', { style: { display: 'flex', alignItems: 'center', gap: 14, padding: '18px 28px', borderBottom: '1px solid var(--border-subtle)', background: 'var(--surface-card)' }, }, e('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } }, e('h1', { style: { margin: 0, fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 22, letterSpacing: '-.01em', color: 'var(--text-primary)' } }, title), e('span', { style: { fontFamily: 'var(--font-sans)', fontSize: 12.5, color: 'var(--text-muted)' } }, sub), ), e('div', { style: { flex: 1 } }), e(Segmented, { options: ['Day', 'Week', 'Month', 'Year'], value: activeKey, onChange: (o) => onRange({ key: o }) }), e(RangeControl, { range: range, onRange: onRange }), ); } // ── Loading splash ── function Splash() { return React.createElement('div', { style: { height: '100vh', display: 'grid', placeItems: 'center', background: 'var(--bg-canvas)' } }, React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 } }, React.createElement('div', { className: 'pa-spin', style: { width: 40, height: 40, borderRadius: '50%', border: '3px solid var(--border-default)', borderTopColor: 'var(--app-accent, #8676FF)' } }), React.createElement('span', { style: { fontFamily: 'var(--font-mono-display)', fontSize: 11, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--text-muted)' } }, 'Loading'), ), ); } // ── Password gate ── function Gate({ onAuthed }) { const [pw, setPw] = useState(''); const [err, setErr] = useState(false); const [busy, setBusy] = useState(false); const submit = async () => { if (!pw || busy) return; setBusy(true); setErr(false); try { await window.FlorioReady; await window.FlorioAuth.signIn(pw); sessionStorage.setItem('florio_dash_auth', '1'); onAuthed(); } catch (e) { setErr(true); setBusy(false); } }; return React.createElement('div', { style: { height: '100vh', display: 'grid', placeItems: 'center', background: 'var(--bg-canvas)' } }, React.createElement('div', { style: { width: 360, background: 'var(--surface-card)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-2xl)', boxShadow: 'var(--shadow-card)', padding: 32, display: 'flex', flexDirection: 'column', gap: 14 } }, React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, React.createElement('span', { style: { fontFamily: 'var(--font-sans)', fontWeight: 800, fontSize: 20, color: 'var(--text-primary)', letterSpacing: '-.01em' } }, 'Florio Analytics'), React.createElement('span', { style: { fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text-muted)' } }, 'Enter the dashboard password'), ), React.createElement('input', { type: 'password', value: pw, autoFocus: true, placeholder: 'Password', onChange: (e) => setPw(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') submit(); }, style: { height: 44, borderRadius: 'var(--radius-md)', border: '1px solid var(--border-default)', padding: '0 14px', fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text-primary)', outline: 'none', background: 'var(--surface-card)' }, }), err && React.createElement('span', { style: { fontFamily: 'var(--font-sans)', fontSize: 12.5, color: 'var(--negative)' } }, 'Incorrect password'), React.createElement('button', { onClick: submit, disabled: busy, style: { height: 44, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: 'var(--app-accent, #8676FF)', color: '#fff', fontFamily: 'var(--font-sans)', fontWeight: 600, fontSize: 14, opacity: busy ? 0.6 : 1 }, }, busy ? 'Checking…' : 'Enter'), ), ); } window.PAShell = { Sidebar, TopBar, SUBS, TITLES, Splash, Gate }; })();