// Top-level app shell + tweaks panel const { useState, useEffect } = React; // Bumped on every meaningful update so we can see what's live in the preview. // Convention: x.y.z — patch on tweaks, minor on new screens/sections, major on // a wholesale UX shift. const APP_VERSION = 'v1.10.0'; const APP_VERSION_DATE = '3 May 2026'; const VersionBadge = () => (
{APP_VERSION} · {APP_VERSION_DATE}
); const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light", "primary": "#2563eb", "radius": 25, "density": "default", "sparkStyle": "line", "sidebarExpanded": true }/*EDITMODE-END*/; const PRIMARY_PRESETS = [ { name: 'Blue', value: '#2563eb' }, { name: 'Indigo', value: '#4f46e5' }, { name: 'Violet', value: '#7c3aed' }, { name: 'Pink', value: '#db2777' }, { name: 'Emerald', value: '#059669' }, { name: 'Amber', value: '#d97706' }, { name: 'Slate', value: '#0f172a' }, ]; function App() { const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); // Auth mode: 'login' (picker) | 'client' (real client) | 'am' (AM portal). // When mode is 'am' and amClientId is set, AM has drilled into a client and // we render the client portal with an AM ribbon overlay. const [authMode, setAuthMode] = useState(() => { try { return localStorage.getItem('clicky_auth_mode') || 'login'; } catch (e) { return 'login'; } }); const [amClientId, setAmClientId] = useState(() => { try { return localStorage.getItem('clicky_am_client') || null; } catch (e) { return null; } }); // Which AM screen to land on after exiting client-view (set by AmRail). const [amScreenIntent, setAmScreenIntent] = useState('portfolio'); // AM client-setup overlay (gear icon in ribbon). const [amSetupOpen, setAmSetupOpen] = useState(false); // Sync window.CLIENT to the active brand synchronously so the first // render after a refresh already reads the right shortName/name. The // swap helper is idempotent — safe to call every render. if (window.swapActiveClient) { window.swapActiveClient(authMode === 'am' && amClientId ? amClientId : null); } const handleLogin = (mode) => { setAuthMode(mode); try { localStorage.setItem('clicky_auth_mode', mode); } catch(e){} }; const handleSignOut = () => { setAuthMode('login'); setAmClientId(null); try { localStorage.setItem('clicky_auth_mode', 'login'); localStorage.removeItem('clicky_am_client'); } catch(e){} }; const enterClientAsAm = (id) => { setAmClientId(id); try { localStorage.setItem('clicky_am_client', id); } catch(e){} }; const exitToPortfolio = () => { setAmClientId(null); try { localStorage.removeItem('clicky_am_client'); } catch(e){} }; // Hash-based routing — initial hash, if present, wins over the default. // Compatible with static http.server (no SPA rewrite required) and the // hash visibly changes as the user navigates between sections. const initialHashSection = (() => { try { const fromHash = (window.location.hash || '').replace(/^#\/?/, '').split('?')[0]; return fromHash || null; } catch (e) { return null; } })(); // User-chosen default dashboard ('classic' | 'ai-led'). Falls back to // the standard dashboard. Hash always wins over preference. const initialActiveSection = (() => { if (initialHashSection) return initialHashSection; try { const pref = localStorage.getItem('clicky_default_dashboard'); if (pref === 'ai-led') return 'dashboard-v2'; } catch (e) {} return 'dashboard'; })(); const [active, setActive] = useState(initialActiveSection); const [billingTab, setBillingTab] = useState(null); const goBilling = (tab) => { setBillingTab(tab || 'invoices'); setActive('billing'); }; const [contractDetailId, setContractDetailId] = useState(null); const openContractDetail = (id) => { setContractDetailId(id); setActive('contract-detail'); }; const [channelsExpanded, setChannelsExpanded] = useState(true); const [kpiDetailKey, setKpiDetailKey] = useState(null); const openKpiDetail = (key) => { setKpiDetailKey(key); setActive('kpi-detail'); }; const [maintenanceSiteId, setMaintenanceSiteId] = useState(null); const openMaintenance = (siteId) => { setMaintenanceSiteId(siteId || null); setActive('maintenance'); }; const [requestKpiOpen, setRequestKpiOpen] = useState(false); const openRequestKpi = () => setRequestKpiOpen(true); const [insiderArticleId, setInsiderArticleId] = useState(null); const openInsiderArticle = (id) => { setInsiderArticleId(id); setActive('insider'); }; const [searchQuery, setSearchQuery] = useState(''); // Ask ClickyAI lightbox — opens from any "Ask ClickyAI" rainbow button. const [askLightboxOpen, setAskLightboxOpen] = useState(false); const [askInitialQuery, setAskInitialQuery] = useState(null); const openAskLightbox = () => setAskLightboxOpen(true); const submitAskFromLightbox = (query) => { setAskInitialQuery(query); setAskLightboxOpen(false); setActive('ask'); }; // Allow the lightbox's "Open full Ask page" link (which lives outside React's // prop tree because it dispatches a window event) to drive the same flow. useEffect(() => { const handler = () => setActive('ask'); window.addEventListener('clicky:openAskFull', handler); return () => window.removeEventListener('clicky:openAskFull', handler); }, []); // Sync the active section to the URL hash — keep it visibly changing // as users browse. pushState fires no event so we don't loop with the // hashchange listener below; that listener only fires from real // navigation (back/forward, manual edit). useEffect(() => { if (authMode === 'login') return; if (authMode === 'am' && !amClientId) return; // AM portfolio owns its own nav const next = '#/' + active; if (window.location.hash !== next) { try { window.history.pushState({ active }, '', next); } catch (e) {} } }, [active, authMode, amClientId]); useEffect(() => { const handler = () => { const fromHash = (window.location.hash || '').replace(/^#\/?/, '').split('?')[0]; if (fromHash && fromHash !== active) setActive(fromHash); }; window.addEventListener('hashchange', handler); window.addEventListener('popstate', handler); return () => { window.removeEventListener('hashchange', handler); window.removeEventListener('popstate', handler); }; }, [active]); const onSearchJump = (target, opts) => { setSearchQuery(''); if (target === 'cpts' && opts?.kpi) { setKpiDetailKey(opts.kpi); setActive('kpi-detail'); return; } if (target === 'maintenance' && opts?.siteId) { setMaintenanceSiteId(opts.siteId); setActive('maintenance'); return; } if (target === 'billing' && opts?.tab) { goBilling(opts.tab); return; } if (target === 'insider' && opts?.articleId) { openInsiderArticle(opts.articleId); return; } setActive(target); }; // Apply tweaks to :root useEffect(() => { document.documentElement.setAttribute('data-theme', tweaks.theme); document.documentElement.style.setProperty('--primary', tweaks.primary); document.documentElement.style.setProperty('--radius', tweaks.radius + 'px'); // density mapping const dMap = { compact: 0.85, default: 1, comfortable: 1.15 }; document.documentElement.style.setProperty('--density', dMap[tweaks.density] || 1); // also persist quick theme + primary so initial paint matches try { localStorage.setItem('amplify_tweaks', JSON.stringify({ theme: tweaks.theme, primary: tweaks.primary })); } catch(e) {} }, [tweaks]); const toggleTheme = () => setTweak('theme', tweaks.theme === 'dark' ? 'light' : 'dark'); const screen = (() => { switch (active) { case 'dashboard': return ; case 'dashboard-v2': return ; case 'channels': return ; case 'paid-media': return ; case 'add-paid-media': return ; case 'add-seo': return ; case 'add-maintenance': return ; case 'google': return ; case 'meta': return ; case 'seo': return ; case 'reports': return ; case 'tickets': return ; case 'billing': return ; case 'contract-detail': return { setBillingTab('contracts'); setActive('billing'); }} setActive={setActive} />; case 'ask': return setAskInitialQuery(null)} />; case 'events': return ; case 'insider': return setInsiderArticleId(null)} />; case 'growth': return ; case 'files': return ; case 'kpi-detail':return setActive('dashboard')} setActive={setActive} />; case 'cpts': return ; case 'maintenance': return ; case 'team': return ; case 'opportunities': return ; case 'insights': return ; default: return null; } })(); const screenLabel = ({ 'dashboard-v2': '00 Dashboard (ClickyAI View)', dashboard: '01 Dashboard', channels: '02 My Ad Channels', 'paid-media': '02a Paid Media', 'add-paid-media': '02b Add Paid Media Channels', 'add-seo': '02c Add SEO', 'add-maintenance':'02d Add Maintenance', google: '02b Google Ads', meta: '02d Meta Ads', seo: '02c SEO', reports: '03 My Reports', tickets: '04 Support Tickets', billing: '05 Contracts & Billing', 'contract-detail': '05a Contract detail', ask: '06 ClickyAI™', events: '08 Events', insider: '08b Clicky Insider', growth: '09 Growth', files: '10 Files', 'kpi-detail': '11 KPI Detail', cpts: '12 All Performance Targets', maintenance: '13 Website Maintenance Pro', team: '14 Your Team', opportunities: '15 Growth Opportunities', insights: '16 Account Insights', })[active]; // Page titles in the browser tab — "Clicky | Current page | Enterprise // PRO Client Portal". Strips the leading numeric prefix from screenLabel // so the chrome reads naturally rather than "01 Dashboard". Login + AM // modes get their own title. useEffect(() => { const SUFFIX = 'Enterprise PRO Client Portal'; let pageName = 'Dashboard'; if (authMode === 'login') { pageName = 'Sign in'; } else if (authMode === 'am' && !amClientId) { pageName = 'Account Manager'; } else { pageName = (screenLabel || active || 'Dashboard').replace(/^\d+[a-z]*\s+/, ''); } document.title = `Clicky | ${pageName} | ${SUFFIX}`; }, [active, authMode, amClientId, screenLabel]); // Auth gates — short-circuit before rendering the client portal. if (authMode === 'login') { return <>; } if (authMode === 'am' && !amClientId) { return <>; } const isAmViewing = authMode === 'am' && amClientId; const goAmPortfolio = () => { setAmSetupOpen(false); setAmScreenIntent('portfolio'); exitToPortfolio(); }; const goAmThisWeek = () => { setAmSetupOpen(false); setAmScreenIntent('thisweek'); exitToPortfolio(); }; return (
{isAmViewing && ( )}
{isAmViewing && ( { setAmSetupOpen(false); enterClientAsAm(id); }} onOpenSetup={() => setAmSetupOpen(v => !v)} setupOpen={amSetupOpen} /> )}
{ if (amSetupOpen) setAmSetupOpen(false); setActive(id); }} channelsExpanded={channelsExpanded} setChannelsExpanded={setChannelsExpanded} density={tweaks.density} openKpiDetail={openKpiDetail} activeKpiKey={active === 'kpi-detail' ? kpiDetailKey : null} openMaintenance={openMaintenance} activeSiteId={active === 'maintenance' ? maintenanceSiteId : null} openRequestKpi={openRequestKpi} goBilling={goBilling} billingTab={active === 'billing' ? billingTab : null} />
setSearchQuery('')} openAskLightbox={openAskLightbox} setActive={setActive} /> {isAmViewing && amSetupOpen ? (
setAmSetupOpen(false)} />
) : (
{screen}
)}
{searchQuery && setSearchQuery('')} onJump={onSearchJump} />} {requestKpiOpen && setRequestKpiOpen(false)} />} {askLightboxOpen && setAskLightboxOpen(false)} onSubmit={submitAskFromLightbox} />} setTweak('theme', v)} />
Primary
{PRIMARY_PRESETS.map(p => (
setTweak('radius', v)} unit="px" /> setTweak('density', v)} /> setTweak('sparkStyle', v)} />
{[ { id: 'dashboard', label: 'Dashboard' }, { id: 'google', label: 'Google Ads' }, { id: 'reports', label: 'My Reports' }, { id: 'tickets', label: 'Support Tickets' }, { id: 'billing', label: 'Contracts & Billing' }, { id: 'ask', label: 'ClickyAI™' }, ].map(s => ( ))}
); } ReactDOM.createRoot(document.getElementById('root')).render();