/* ===== 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() { 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 = { name: 'Ashraf Hossain', email: 'ashraf@sectionly.app', role: 'Frontend Developer', company: 'Sectionly', bio: 'Building beautiful frontends, one section at a time.', avatar: 'app/assets/ashraf.png', notif: { product: true, weekly: true, mentions: true, marketing: false }, twoFactor: false }; var saved = window.Store.get('profile'); return Object.assign(def, saved && typeof saved === 'object' ? saved : {}); }); const setProfile = useCallback((patch) => { setProfileState(prev => { var next = typeof patch === 'function' ? patch(prev) : Object.assign({}, prev, patch); window.Store.set('profile', next); return next; }); }, []); const searchRef = useRef(null); const [notifOpen, setNotifOpen] = useState(false); const notifRef = useRef(null); 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. */ window.Store.boot(function () { if (window.SectionStore && window.SectionStore.recompose) window.SectionStore.recompose(); ReactDOM.createRoot(document.getElementById('root')).render(); });