// app.jsx — root: auth, routing, shared state, toasts, API integration const { useState, useEffect, useCallback } = React; const ACCENTS = { Blue: { c6: '#0a66ff', c7: '#0a52d6', c5: '#599bff', c1: '#d9e8ff', c0: '#eef5ff', rgb: '10,102,255' }, Indigo: { c6: '#4f46e5', c7: '#4338ca', c5: '#818cf8', c1: '#e0e7ff', c0: '#eef2ff', rgb: '79,70,229' }, Violet: { c6: '#7c3aed', c7: '#6d28d9', c5: '#a78bfa', c1: '#ede9fe', c0: '#f5f3ff', rgb: '124,58,237' }, Emerald: { c6: '#059669', c7: '#047857', c5: '#34d399', c1: '#d1fae5', c0: '#ecfdf5', rgb: '5,150,105' }, Coral: { c6: '#f4623a', c7: '#e14e29', c5: '#fb8a5c', c1: '#ffe2d5', c0: '#fff3ee', rgb: '244,98,58' }, }; const RADII = { Sharp: { xs: '4px', sm: '6px', r: '8px', lg: '10px', xl: '13px' }, Default: { xs: '7px', sm: '10px', r: '13px', lg: '17px', xl: '22px' }, Round: { xs: '10px', sm: '14px', r: '18px', lg: '24px', xl: '30px' }, }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "Light", "accent": "Blue", "radius": "Default", "density": "Comfortable", "railStyle": "Tinted" }/*EDITMODE-END*/; function Toasts({ items }) { const Icon = window.Icon; return (
{items.map(t => (
{t.msg}
))}
); } function App() { const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS); // ── Auth ───────────────────────────────────────────────────── const [authUser, setAuthUser] = useState(null); // null=loading, false=logged out, obj=authed const [authReady, setAuthReady] = useState(false); useEffect(() => { const token = localStorage.getItem('ims_token'); if (!token) { setAuthUser(false); setAuthReady(true); return; } window.API.me() .then(u => { setAuthUser(u); setBalance(u.balance); setAuthReady(true); }) .catch(() => { localStorage.removeItem('ims_token'); setAuthUser(false); setAuthReady(true); }); }, []); const handleLogin = useCallback((user) => { setAuthUser(user); setBalance(user.balance || 1000); window.API.getNotifs().then(ns => setNotifs(ns)).catch(() => {}); }, []); const handleLogout = useCallback(() => { localStorage.removeItem('ims_token'); setAuthUser(false); setRoute('home'); }, []); // ── Routing ────────────────────────────────────────────────── const [route, setRoute] = useState('home'); const [collapsed, setCollapsed] = useState(false); const [search, setSearch] = useState(''); const [activeIdea, setActiveIdea] = useState(null); const [activeUser, setActiveUser] = useState(null); const [prevRoute, setPrevRoute] = useState('home'); const [addOpen, setAddOpen] = useState(false); const [notifOpen, setNotifOpen] = useState(false); const [balance, setBalance] = useState(1000); const [lang, setLang] = useState('en'); const toggleLang = () => { setLang(l => { const next = l === 'en' ? 'ar' : 'en'; const html = document.documentElement; html.setAttribute('dir', next === 'ar' ? 'rtl' : 'ltr'); html.setAttribute('lang', next); if (next === 'ar') { html.style.fontFamily = "'Noto Sans Arabic', 'Segoe UI', sans-serif"; } else { html.style.fontFamily = ''; } return next; }); }; // ── Notifications ───────────────────────────────────────────── const [notifs, setNotifs] = useState([]); useEffect(() => { if (!authUser) return; window.API.getNotifs().then(ns => setNotifs(ns)).catch(() => {}); const iv = setInterval(() => window.API.getNotifs().then(ns => setNotifs(ns)).catch(() => {}), 30000); return () => clearInterval(iv); }, [!!authUser]); const unread = notifs.filter(n => n.unread).length; const [toasts, setToasts] = useState([]); const toast = useCallback((msg) => { const id = Date.now() + Math.random(); setToasts(ts => [...ts, { id, msg }]); setTimeout(() => setToasts(ts => ts.filter(x => x.id !== id)), 2600); }, []); // ── Votes (optimistic + API) ────────────────────────────────── const [votes, setVotes] = useState({}); const onVote = useCallback(async (id, dir) => { const prev = votes[id]; setVotes(v => ({ ...v, [id]: v[id] === dir ? undefined : dir })); try { const res = await window.API.voteIdea(id, dir); setVotes(v => ({ ...v, [id]: res.vote || undefined })); } catch (e) { setVotes(v => ({ ...v, [id]: prev })); toast('Vote failed: ' + e.message); } }, [votes, toast]); const go = useCallback((r) => { setPrevRoute(route); setRoute(r); document.querySelector('.content')?.scrollTo(0, 0); }, [route]); const openIdea = useCallback((idea) => { setActiveIdea(idea); setPrevRoute(route); setRoute('idea'); document.querySelector('.content')?.scrollTo(0, 0); }, [route]); const openUser = useCallback((user) => { setActiveUser(user); setPrevRoute(route); setRoute('user'); document.querySelector('.content')?.scrollTo(0, 0); }, [route]); const redeem = useCallback(async (award) => { try { const res = await window.API.redeem(award.id); setBalance(res.balance); toast(`Redeemed "${award.title}" · -${award.cost} pts`); } catch (e) { toast('Redemption failed: ' + e.message); } }, [toast]); const submitIdea = useCallback(async ({ title, description, category, tags, coverImage, extraImage, video }) => { try { await window.API.createIdea({ title, description, category, tags, coverImage, extraImage, video }); setAddOpen(false); window.API.me().then(u => setBalance(u.balance)).catch(() => {}); setRoute('myideas'); toast(`Idea "${title}" submitted · +150 pts`); } catch (e) { toast('Submit failed: ' + e.message); } }, [toast]); const onSearchSubmit = useCallback((val, enter) => { if (enter && val.trim()) { setRoute('search'); document.querySelector('.content')?.scrollTo(0, 0); } else if (val.trim() && route !== 'search') { setRoute('search'); } else if (!val.trim() && route === 'search') { setRoute('home'); } }, [route]); const readNotif = useCallback(async (id) => { setNotifs(ns => ns.map(n => n.id === id ? { ...n, unread: false } : n)); try { await window.API.readNotif(id); } catch {} }, []); const readAllNotifs = useCallback(async () => { setNotifs(ns => ns.map(n => ({ ...n, unread: false }))); try { await window.API.readAllNotifs(); } catch {} }, []); const saveProfile = useCallback(() => { go('profile'); toast('Profile saved'); }, [go, toast]); // ⌘K focuses search useEffect(() => { const h = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); document.querySelector('.search input')?.focus(); } }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, []); // apply tweaks → CSS vars useEffect(() => { const r = document.documentElement.style; document.body.classList.add('no-anim'); const a = ACCENTS[t.accent] || ACCENTS.Blue; r.setProperty('--accent', a.c6); r.setProperty('--accent-600', a.c6); r.setProperty('--accent-700', a.c7); r.setProperty('--accent-300', a.c5); r.setProperty('--accent-100', a.c1); r.setProperty('--accent-50', a.c0); r.setProperty('--accent-rgb', a.rgb); const rad = RADII[t.radius] || RADII.Default; r.setProperty('--r-xs', rad.xs); r.setProperty('--r-sm', rad.sm); r.setProperty('--r', rad.r); r.setProperty('--r-lg', rad.lg); r.setProperty('--r-xl', rad.xl); r.setProperty('--density', t.density === 'Compact' ? '0.6' : '1'); const dark = t.theme === 'Dark'; document.body.classList.toggle('dark', dark); r.setProperty('--accent-50', dark ? `rgba(${a.rgb},.16)` : a.c0); r.setProperty('--accent-100', dark ? `rgba(${a.rgb},.26)` : a.c1); r.setProperty('--accent-700', dark ? a.c5 : a.c7); requestAnimationFrame(() => requestAnimationFrame(() => document.body.classList.remove('no-anim'))); }, [t.accent, t.radius, t.density, t.theme]); const screen = () => { const listProps = { votes, onVote, openIdea }; switch (route) { case 'home': return ; case 'search': return ; case 'addIdea': return go(prevRoute === 'addIdea' ? 'home' : prevRoute)} onSubmit={submitIdea} />; case 'dashboard': return ; case 'myideas': return ; case 'favorite': return ; case 'profile': return go('editProfile')} openUser={openUser} />; case 'editProfile': return go('profile')} onSave={saveProfile} />; case 'settings': return ; case 'trade': return go('balanceDetail')} />; case 'balanceDetail': return go('trade')} />; case 'user': return go(prevRoute)} openIdea={openIdea} votes={votes} onVote={onVote} />; case 'idea': return go(prevRoute)} openUser={openUser} currentUser={authUser} />; default: return ; } }; // nav highlight: map sub-routes back to their owning nav section const navMap = { idea: prevRoute, user: prevRoute, editProfile: 'profile', balanceDetail: 'trade', search: 'home', addIdea: prevRoute }; const navRoute = navMap[route] || route; // ── Auth gate ───────────────────────────────────────────────── if (!authReady) return (
); if (!authUser) return ; return (
setCollapsed(c => !c)} search={search} setSearch={setSearch} balance={balance} onAdd={() => go('addIdea')} onNav={go} onSearchSubmit={onSearchSubmit} notifOpen={notifOpen} setNotifOpen={setNotifOpen} unread={unread} currentUser={authUser} lang={lang} toggleLang={toggleLang} />
{screen()}
setNotifOpen(false)} items={notifs} onRead={readNotif} onReadAll={readAllNotifs} /> setAddOpen(false)} onSubmit={submitIdea} /> setTweak('theme', v)} /> a.c6)} onChange={(hex) => setTweak('accent', Object.keys(ACCENTS).find(k => ACCENTS[k].c6 === hex) || 'Blue')} /> setTweak('radius', v)} /> setTweak('density', v)} /> setTweak('railStyle', v)} />
); } // alias fix window.Toasts = Toasts; ReactDOM.createRoot(document.getElementById('root')).render();