// 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 */}
)}
);
}
function MenuItem({ icon, label, onClick, alert, sub }) {
return (
e.currentTarget.style.background = 'var(--bg-deep)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
{icon}
);
}
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 (
);
}
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 (
<>
{/* Account header */}
{(u.name || u.email || '?')[0].toUpperCase()}
{u.name || 'משתמש'}
{u.email}
{u.is_admin && (
אדמין
)}
ניווט
{items.map(it => (
))}
חשבון
{accountItems.map(it => (
))}
>
);
}
window.Sidebar = Sidebar;
window.CommandPalette = CommandPalette;
window.MobileTopbar = MobileTopbar;
window.BottomTabBar = BottomTabBar;
window.MoreSheet = MoreSheet;