/* ===== Main app: shell, routing, state, tweaks ===== */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#6243EB", "theme": "light", "density": "regular", "fontPair": "modern", "rail": false }/*EDITMODE-END*/; const FONT_PAIRS = { modern: { display: "'Inter'", ui: "'Inter'" }, geometric: { display: "'Inter'", ui: "'Inter'" }, playful: { display: "'Inter'", ui: "'Inter'" } }; const NAV_MAIN = [ { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' }, { id: 'browse', label: 'Library', icon: 'library' }, { id: 'collections', label: 'Collections', icon: 'collections' }, { id: 'favorites', label: 'Favorites', icon: 'heart' }, { id: 'manage', label: 'My Sections', icon: 'folder' } ]; function App({ user, onSignOut }) { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [route, setRoute] = useState({ view: 'dashboard' }); const [search, setSearch] = useState(''); const [favs, setFavs] = useState(() => { var v = window.Store.get('favs'); return Array.isArray(v) ? v : []; }); const [catsOpen, setCatsOpen] = useState(() => { var v = window.Store.get('catsOpen'); return v === undefined ? true : !!v; }); useEffect(() => { window.Store.set('catsOpen', catsOpen); }, [catsOpen]); const [profile, setProfileState] = useState(() => { var def = { role: 'Frontend Developer', company: 'Sectionly', bio: 'Building beautiful frontends, one section at a time.', notif: { product: true, weekly: true, mentions: true, marketing: false }, twoFactor: false }; var saved = window.Store.get('profile'); var base = Object.assign(def, saved && typeof saved === 'object' ? saved : {}); // Identity (name/email/avatar) is owned by the signed-in account. return Object.assign(base, { name: user.name, email: user.email, avatar: user.avatar || base.avatar || '', provider: user.provider, verified: user.verified }); }); const setProfile = useCallback((patch) => { setProfileState(prev => { var next = typeof patch === 'function' ? patch(prev) : Object.assign({}, prev, patch); window.Store.set('profile', next); // keep the account record (and other devices) in sync with identity edits if (patch && typeof patch === 'object' && (patch.name != null || patch.email != null || patch.avatar != null)) { window.Auth.updateAccount({ name: next.name, email: next.email, avatar: next.avatar }); } return next; }); }, []); // reflect external account changes (e.g. another device) into the sidebar useEffect(() => { setProfileState(prev => Object.assign({}, prev, { name: user.name, email: user.email, avatar: user.avatar || prev.avatar || '', provider: user.provider, verified: user.verified })); }, [user.name, user.email, user.avatar, user.verified]); const searchRef = useRef(null); const [notifOpen, setNotifOpen] = useState(false); const notifRef = useRef(null); const [userMenuOpen, setUserMenuOpen] = useState(false); const userMenuRef = useRef(null); useEffect(() => { if (!userMenuOpen) return; const onDoc = (e) => { if (userMenuRef.current && !userMenuRef.current.contains(e.target)) setUserMenuOpen(false); }; const onKey = (e) => { if (e.key === 'Escape') setUserMenuOpen(false); }; document.addEventListener('mousedown', onDoc); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); }; }, [userMenuOpen]); useEffect(() => { if (!notifOpen) return; const onDoc = (e) => { if (notifRef.current && !notifRef.current.contains(e.target)) setNotifOpen(false); }; const onKey = (e) => { if (e.key === 'Escape') setNotifOpen(false); }; document.addEventListener('mousedown', onDoc); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); }; }, [notifOpen]); const [version, setVersion] = useState(0); const refresh = useCallback(() => setVersion(v => v + 1), []); const [toast, setToast] = useState(null); const toastT = useRef(null); const notify = useCallback((msg, error) => { setToast({ msg: msg, error: !!error }); if (toastT.current) clearTimeout(toastT.current); toastT.current = setTimeout(() => setToast(null), 2400); }, []); useEffect(() => { window.Store.set('favs', favs); }, [favs]); const toggleFav = useCallback((id) => { setFavs(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); }, []); const nav = useCallback((r) => { if (r.view !== 'browse') setSearch(''); setRoute(r); const el = document.querySelector('.content'); if (el) el.scrollTop = 0; }, []); /* cmd+k focuses search */ useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); searchRef.current && searchRef.current.focus(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const onSearch = (v) => { setSearch(v); if (v && route.view !== 'browse') setRoute({ view: 'browse' }); }; const fp = FONT_PAIRS[t.fontPair] || FONT_PAIRS.modern; const rootStyle = { '--accent': t.accent, '--accent-press': 'color-mix(in srgb, ' + t.accent + ' 78%, #000)', '--font-display': fp.display + ", 'Inter', sans-serif", '--font-ui': fp.ui + ", -apple-system, system-ui, sans-serif" }; const cls = 'app theme-' + t.theme + ' density-' + t.density + (t.rail ? ' rail-collapsed' : ''); const activeNav = route.view === 'category' ? 'browse' : route.view; const activeCat = route.view === 'category' ? route.catId : null; let body; if (route.view === 'dashboard') body = ; else if (route.view === 'browse') body = ; else if (route.view === 'category') body = ; else if (route.view === 'collections') body = ; else if (route.view === 'favorites') body = ; else if (route.view === 'manage') body = ; else if (route.view === 'add') body = ; else if (route.view === 'settings') body = ; else if (route.view === 'section') { body = window.sectionById(route.sectionId) ? : ; } return (
{/* Sidebar */} {/* Main */}
onSearch(e.target.value)} placeholder="Search sections, tags, categories…" /> ⌘K
{notifOpen && (
Notifications

No notifications yet

You’re all caught up. New activity will show up here.

)}
{body}
{/* Tweaks */} setTweak('accent', v)} /> setTweak('fontPair', v)} /> setTweak('density', v)} /> setTweak('rail', v)} /> setTweak('theme', v)} />
); } /* Boot persistence (server when deployed, localStorage otherwise) THEN render. Gating the render on the async load means the very first paint already has the real data — no flash of defaults, no race with localStorage. */ function Root() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [user, setUser] = useState(() => window.Auth.currentUser()); useEffect(() => window.Auth.onChange(setUser), []); const onSignOut = useCallback(() => { window.Auth.signOut(); }, []); const fp = FONT_PAIRS[t.fontPair] || FONT_PAIRS.modern; const rootStyle = { '--accent': t.accent, '--accent-press': 'color-mix(in srgb, ' + t.accent + ' 78%, #000)', '--font-display': fp.display + ", 'Inter', sans-serif", '--font-ui': fp.ui + ", -apple-system, system-ui, sans-serif" }; if (!user) { return (
setTweak('theme', t.theme === 'light' ? 'dark' : 'light')} onAuthed={setUser} />
); } return ; } /* Boot persistence first, THEN auth (it reuses the detected server/local mode), THEN render — so the first paint already reflects the real session + data. */ window.Store.boot(function (mode) { if (window.SectionStore && window.SectionStore.recompose) window.SectionStore.recompose(); window.Auth.boot(mode, function () { ReactDOM.createRoot(document.getElementById('root')).render(); }); });