// App shell: sidebar nav for desktop. On mobile (≤1024px) the entire // .app-sidebar element is hidden via CSS — mobile uses the bottom tab bar // + bottom sheet pattern instead (see BottomTabBar / MoreSheet below). function Sidebar({ page, onPage }) { const { setCmdOpen } = useApp(); const isAdmin = window.APP_USER?.is_admin; const items1 = [ { id: 'dashboard', icon: '◉', label: 'סקירה', shortcut: 'G D' }, { id: 'txns', icon: '≡', label: 'עסקאות', count: D.current?.txns?.length || 0, shortcut: 'G T' }, { id: 'trends', icon: '⌃', label: 'מגמות', shortcut: 'G R' }, { id: 'ai', icon: '✦', label: 'דוח Cai', shortcut: 'G A' }, { id: 'savings', icon: '↓', label: 'איפה לחסוך', count: D.savings?.items?.length || 0, shortcut: 'G S' }, ]; const items2 = [ { id: 'budgets', icon: '◐', label: 'תקציבים' }, { id: 'subs', icon: '↻', label: 'מנויים', count: D.subscriptions?.length || 0 }, { id: 'goals', icon: '★', label: 'יעדים' }, { id: 'settings', icon: '⚙', label: 'הגדרות' }, ]; if (isAdmin) { items2.push({ id: 'admin', icon: '⚒', label: 'ניהול' }); } return ( ); } function UserCard() { const { setPage } = useApp(); const u = window.APP_USER || {}; const initial = (u.name || u.email || '?').trim()[0].toUpperCase(); const accountCount = (D.accounts || []).length; const [open, setOpen] = useState(false); const ref = useRef(null); // Close dropdown when clicking outside useEffect(() => { if (!open) return; const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onClick); return () => document.removeEventListener('mousedown', onClick); }, [open]); const goTo = (pageId) => { setPage(pageId); setOpen(false); }; const logout = async () => { setOpen(false); if (!confirm('להתנתק מהחשבון?')) return; if (window.APP_LOGOUT) await window.APP_LOGOUT(); else window.location.href = '/login'; }; return (
setOpen(o => !o)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 6px', borderTop: '0.5px solid var(--rule)', paddingTop: 12, cursor: 'pointer', transition: 'background .14s', borderRadius: 8, background: open ? 'var(--surface)' : 'transparent', }} title="לחץ לתפריט" >
{initial}
{u.name || 'משתמש'}
{accountCount} חשבונות
{open && (
{/* Header */}
{u.name}
{u.email}
goTo('profile')} /> {u.is_admin && goTo('admin')} />} goTo('settings')} />
)}
); } function MenuItem({ icon, label, onClick, alert, sub }) { return (
e.currentTarget.style.background = 'var(--bg-deep)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} > {icon}
{label}
{sub &&
{sub}
}
); } function NavGroup({ label, items, active, onSelect }) { return (
{label}
{items.map(it => { const on = it.id === active; return (
onSelect(it.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 10px', borderRadius: 8, cursor: 'pointer', background: on ? 'var(--surface)' : 'transparent', boxShadow: on ? 'var(--shadow-sm)' : 'none', fontSize: 13, fontWeight: on ? 600 : 500, color: on ? 'var(--ink)' : 'var(--ink-2)', transition: 'background .14s', }}> {it.icon} {it.label} {it.count != null && ( {it.count} )}
); })}
); } function ThemeToggle() { const { theme, setTheme } = useApp(); return (
{[['light','☀','בהיר'],['dark','☾','כהה']].map(([k, ic, l]) => ( ))}
); } // Command Palette function CommandPalette() { const { cmdOpen, setCmdOpen, setPage } = useApp(); const [q, setQ] = useState(''); const [idx, setIdx] = useState(0); const inputRef = useRef(); useEffect(() => { if (cmdOpen) setTimeout(() => inputRef.current?.focus(), 30); else { setQ(''); setIdx(0); } }, [cmdOpen]); const allItems = useMemo(() => [ { type: 'page', id: 'dashboard', label: 'סקירה', sub: 'דשבורד ראשי', icon: '◉' }, { type: 'page', id: 'txns', label: 'עסקאות', sub: 'כל החיובים', icon: '≡' }, { type: 'page', id: 'trends', label: 'מגמות', sub: 'השוואת חודשים', icon: '⌃' }, { type: 'page', id: 'ai', label: 'דוח Cai', sub: 'תובנות חודשיות', icon: '✦' }, { type: 'page', id: 'savings', label: 'איפה לחסוך', sub: 'המלצות חיסכון מבוססות נתונים', icon: '↓' }, { type: 'page', id: 'budgets', label: 'תקציבים', icon: '◐' }, { type: 'page', id: 'subs', label: 'מנויים', icon: '↻' }, { type: 'page', id: 'goals', label: 'יעדים', icon: '★' }, { type: 'page', id: 'settings', label: 'הגדרות', icon: '⚙' }, ...D.current.txns.slice(0, 30).map(t => ({ type: 'txn', id: t.id, label: t.description, sub: `${fmtDate(t.date)} · ${t.category} · ${fmtILS(t.amount)}`, icon: D.categories[t.category]?.icon || '·', })), ], []); const filtered = useMemo(() => { if (!q) return allItems.slice(0, 8); const lq = q.toLowerCase(); return allItems.filter(i => i.label.toLowerCase().includes(lq) || (i.sub||'').toLowerCase().includes(lq)).slice(0, 12); }, [q, allItems]); useEffect(() => { setIdx(0); }, [q]); useEffect(() => { if (!cmdOpen) return; const onKey = (e) => { if (e.key === 'Escape') setCmdOpen(false); if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(filtered.length-1, i+1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(0, i-1)); } if (e.key === 'Enter') { const it = filtered[idx]; if (it?.type === 'page') { setPage(it.id); setCmdOpen(false); } else if (it?.type === 'txn') { setCmdOpen(false); setPage('txns'); } } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [cmdOpen, filtered, idx]); if (!cmdOpen) return null; return (
setCmdOpen(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)', zIndex: 100, display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: '12vh', animation: 'fade-in .15s', }}>
e.stopPropagation()} className="slide-up" style={{ width: 540, maxWidth: '90%', background: 'var(--surface)', borderRadius: 16, boxShadow: 'var(--shadow-lg)', overflow: 'hidden', }}>
setQ(e.target.value)} placeholder="חפש עסקאות, עמודים, פעולות…" style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 14 }} /> ESC
{filtered.length === 0 && (
אין תוצאות
)} {filtered.map((it, i) => (
setIdx(i)} onClick={() => { if (it.type === 'page') { setPage(it.id); setCmdOpen(false); } }} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', borderRadius: 8, cursor: 'pointer', background: i === idx ? 'var(--bg-deep)' : 'transparent', }}> {it.icon}
{it.label}
{it.sub &&
{it.sub}
}
{it.type === 'page' ? 'עמוד' : 'עסקה'}
))}
↑↓ ניווט↵ בחירהESC סגירה
); } // ─── Mobile chrome (iOS Human-Interface-style) ─────────────────────── // // Pattern: // • Sticky glass top bar with the page title + a single trailing // action (search). No hamburger — primary nav lives in the bottom // tab bar instead. // • Bottom tab bar with the 5 most-used destinations. // • A "More" tab opens a bottom-sheet listing the rest, the user // account, and Logout. // // All CSS lives in styles.css (mobile @media query). These components // just render the structure and toggle classes. const PAGE_LABELS = { dashboard: 'סקירה', txns: 'עסקאות', trends: 'מגמות', ai: 'דוח Cai', savings: 'איפה לחסוך', budgets: 'תקציבים', subs: 'מנויים', goals: 'יעדים', settings: 'הגדרות', profile: 'החשבון שלי', admin: 'ניהול', }; // Primary nav for the bottom tab bar — pick the 5 most-used destinations. // Order matters (RTL means rightmost is first). function getPrimaryTabs() { return [ { id: 'dashboard', icon: '◉', label: 'סקירה' }, { id: 'txns', icon: '≡', label: 'עסקאות' }, { id: 'ai', icon: '✦', label: 'Cai' }, { id: 'subs', icon: '↻', label: 'מנויים', count: D.subscriptions?.length || 0 }, { id: '__more', icon: '⋯', label: 'עוד' }, ]; } function MobileTopbar({ page, onSearch }) { return (
{PAGE_LABELS[page] || 'CliBank'}
); } function BottomTabBar({ page, onPick }) { const tabs = getPrimaryTabs(); return ( ); } // Bottom sheet for "More" destinations + account + logout. function MoreSheet({ open, onClose, onPick, page }) { const isAdmin = window.APP_USER?.is_admin; const u = window.APP_USER || {}; // Click backdrop = close const onBackdrop = (e) => { if (e.target === e.currentTarget) onClose(); }; // Lock body scroll when sheet open useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [open]); const items = [ { id: 'trends', icon: '⌃', label: 'מגמות' }, { id: 'savings', icon: '↓', label: 'איפה לחסוך', count: D.savings?.items?.length || 0 }, { id: 'budgets', icon: '◐', label: 'תקציבים' }, { id: 'goals', icon: '★', label: 'יעדים' }, { id: 'settings', icon: '⚙', label: 'הגדרות' }, ]; const accountItems = [ { id: 'profile', icon: '👤', label: 'החשבון שלי' }, ]; if (isAdmin) accountItems.push({ id: 'admin', icon: '⚒', label: 'ניהול' }); const handlePick = (id) => { onPick(id); onClose(); }; const handleLogout = async () => { onClose(); if (!confirm('להתנתק מהחשבון?')) return; if (window.APP_LOGOUT) await window.APP_LOGOUT(); else window.location.href = '/login'; }; return ( <>