// app.jsx — INDIECASH navigation shell + Tweaks panel const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "dark": false, "accent": "emerald", "scenario": "couple", "hideBalance": false }/*EDITMODE-END*/; const ACCENT_PALETTES = [ ['#10b981', '#059669'], // emerald ['#0d9488', '#0f766e'], // teal ['#34d399', '#10b981'], // mint ['#16a34a', '#15803d'], // forest ]; const ACCENT_NAMES = ['emerald', 'teal', 'mint', 'forest']; function App() { const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS); const theme = useTheme(tw.dark, tw.accent); // Real auth state from Supabase const auth = (typeof useAuth !== 'undefined') ? useAuth() : { loading: false, authReady: true, user: null, profile: null, family: null, signIn: async()=>{}, signOut: async()=>{}, createFamily: async()=>{}, joinFamily: async()=>{} }; // Derive route from auth state const computedRoute = !auth.authReady ? 'loading' : !auth.user ? 'login' : !auth.profile?.family_id ? 'onboarding' : 'app'; const [route, setRoute] = React.useState('app'); // fallback for demo // Sync route with auth React.useEffect(() => { if (auth.authReady) setRoute(computedRoute); }, [computedRoute]); const [tab, setTab] = React.useState('home'); const [sheetOpen, setSheetOpen] = React.useState(false); const [sheetKind, setSheetKind] = React.useState('expense'); const [toast, setToast] = React.useState(null); const scenario = SCENARIOS[tw.scenario] || SCENARIOS.couple; const openSheet = (kind) => { setSheetKind(kind || 'expense'); setSheetOpen(true); }; const navigate = (target) => { if (['home','tx','report','more'].includes(target)) { setTab(target); } else { // sub-screen routes (budget/goals/debts) setTab(target); } }; const saveTx = async (entry) => { // Resolve "Hari ini" / "Kemarin" to ISO date var today = new Date(); var resolvedDate = (!entry.date || entry.date === 'Hari ini') ? today.toISOString().split('T')[0] : entry.date === 'Kemarin' ? new Date(today.setDate(today.getDate()-1)).toISOString().split('T')[0] : entry.date; entry = Object.assign({}, entry, { date: resolvedDate }); if (auth.user && auth.family && window.sbAddTransaction) { try { // Match category by label name (case-insensitive) // entry.cat could be string key ('makan_ex') or null var catKey = typeof entry.cat === 'string' ? entry.cat : ''; var catLabel = (CATEGORIES[catKey] && CATEGORIES[catKey].label) || ''; var cats = (await window.SB .from('categories').select('id,name') .or('family_id.is.null,family_id.eq.'+auth.family.id)).data || []; var cat = cats.find(function(c){ return (catLabel && c.name.toLowerCase() === catLabel.toLowerCase()) || (catKey && c.name.toLowerCase() === catKey.replace(/_/g,' ').toLowerCase()); }); var r = await sbAddTransaction(auth.family.id, auth.user.id, Object.assign({}, entry, { account_id: entry.account_id || null, category_id: cat ? cat.id : null, })); if (r.error) throw r.error; } catch(e) { setToast('Error: ' + (e.message||'Gagal simpan')); setTimeout(function(){ setToast(null); }, 3000); return; } } setToast((entry.kind === 'income' ? '✓ Pemasukan ' : '✓ Pengeluaran ') + fmtRp(entry.amount) + ' tersimpan'); setTimeout(function(){ setToast(null); }, 2200); window.dispatchEvent(new Event('tx-added')); }; // ─── Pick screen based on route + tab ───────────────────────── let screen; if (route === 'login') { screen = setRoute('onboarding')} />; } else if (route === 'onboarding') { screen = setRoute('app')} />; } else { screen = (() => { switch (tab) { case 'home': return setTweak('hideBalance', !tw.hideBalance)} onNavigate={navigate} onOpenSheet={openSheet} />; case 'tx': return ; case 'report': return ; case 'more': return { await auth.signOut(); setRoute('login'); }} />; case 'budget': return setTab('more')} theme={theme}> ; case 'goals': return setTab('more')} theme={theme}> ; case 'debts': return setTab('more')} theme={theme}> ; case 'fire': return setTab('more')} theme={theme}> ; case 'accounts': return setTab('more')} theme={theme}> ; case 'profile': return setTab('more')} theme={theme}> ; case 'security': return setTab('more')} theme={theme}> ; case 'export': return setTab('more')} theme={theme}> ; case 'help': return setTab('more')} theme={theme}> ; case 'receipt': return setTab('more')} theme={theme}> { setToast('Transaksi dari struk disimpan!'); setTimeout(() => setToast(null), 2200); }} /> ; default: return null; } })(); } const SUB_SCREENS = ['fire','accounts','profile','security','export','help','receipt','budget','goals','debts']; const showNav = route === 'app' && !['login','onboarding'].includes(route) && !SUB_SCREENS.includes(tab); const statusDark = (route === 'app') ? theme.statusBarDark : theme.statusBarDark; return (
{/* Status bar */}
{/* Scrollable screen */}
{screen}
{/* Bottom nav (only on app routes) */} {showNav && ( openSheet('picker')} theme={theme} accent={theme.accent} /> )} {/* FAB picker sheet */} {sheetKind === 'picker' && sheetOpen && ( setSheetOpen(false)} onSelect={function(k){ setSheetKind(k); }} theme={theme} accent={theme.accent} /> )} {/* Transaction form */} {!SUB_SCREENS.includes(tab) && sheetKind !== 'picker' && sheetKind !== 'transfer' && ( setSheetOpen(false)} onSave={saveTx} auth={auth} theme={theme} accent={theme.accent} scenario={scenario} defaultBy={(scenario.members && scenario.members[0] && scenario.members[0].id) || 'me'} /> )} {/* Transfer form */} {!SUB_SCREENS.includes(tab) && ( setSheetOpen(false)} onDone={function(){ window.dispatchEvent(new Event('tx-added')); }} auth={auth} theme={theme} accent={theme.accent} /> )} {/* Toast */} {toast && (
{toast}
)} {/* Tweaks panel */} setTweak('dark', v)} /> o.value).reduce((acc, k, i) => { acc.push(ACCENT_PALETTES[i]); return acc; }, [])} onChange={(palette) => { const i = ACCENT_PALETTES.findIndex( (p) => p[0] === palette[0] && p[1] === palette[1] ); setTweak('accent', ACCENT_NAMES[i] || 'emerald'); }} /> setTweak('scenario', v)} /> setTweak('hideBalance', v)} /> setRoute(v)} /> {route === 'app' && ( setTab(v)} /> )}
); } // Sub-screen wrapper — adds a back row above content function SubScreen({ title, onBack, theme, children }) { // The inner screens already render their own large title in 56px-top padding. // To keep visual consistency we just overlay a back button in the same zone. return (
{/* Pad the screen header right slightly so it doesn't sit under back btn */}
{children}
); } // Faux status bar — same look as iOS frame's, but lives inside the screen so // the dark/light icons can react to scenario/dark-mode. function FakeStatusBar({ dark }) { const c = dark ? '#fff' : '#0f172a'; return (
9:41
); } // Mount const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); Object.assign(window, { App, SubScreen, FakeStatusBar });