2026-04-26 18:44:59 +09:00
|
|
|
'use client';
|
|
|
|
|
|
2026-03-27 14:50:34 +09:00
|
|
|
import Link from 'next/link';
|
2026-04-26 18:44:59 +09:00
|
|
|
import { usePathname } from 'next/navigation';
|
|
|
|
|
import { Icon } from '@/components/ui/Icon';
|
|
|
|
|
import type { IconName } from '@/components/ui/Icon';
|
|
|
|
|
import { Avatar } from '@/components/ui/Avatar';
|
|
|
|
|
import { useApi } from '@/lib/useApi';
|
|
|
|
|
|
|
|
|
|
interface NavItem {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
href: string;
|
|
|
|
|
icon: IconName;
|
|
|
|
|
section?: string;
|
|
|
|
|
badge?: number;
|
|
|
|
|
}
|
2026-03-27 14:50:34 +09:00
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
const NAV: NavItem[] = [
|
|
|
|
|
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'dashboard' },
|
|
|
|
|
{
|
|
|
|
|
id: 'cabinet',
|
|
|
|
|
label: 'Cabinet',
|
|
|
|
|
href: '/medicines/cabinet',
|
|
|
|
|
icon: 'cabinet',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'schedule',
|
|
|
|
|
label: 'Schedule & Log',
|
|
|
|
|
href: '/medicines/schedule',
|
|
|
|
|
icon: 'clock',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'regimens',
|
|
|
|
|
label: 'Regimens',
|
|
|
|
|
href: '/medicines/regimens',
|
|
|
|
|
icon: 'list',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'organizer',
|
|
|
|
|
label: 'Pill Organizer',
|
|
|
|
|
href: '/medicines/organizer',
|
|
|
|
|
icon: 'calendar',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'library',
|
|
|
|
|
label: 'Library',
|
|
|
|
|
href: '/medicines/library',
|
|
|
|
|
icon: 'pill',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'refills',
|
|
|
|
|
label: 'Shopping list',
|
|
|
|
|
href: '/refills',
|
|
|
|
|
icon: 'refresh',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{ id: 'purchases', label: 'Purchases', href: '/purchases', icon: 'truck', section: 'Medicines' },
|
|
|
|
|
{ id: 'prices', label: 'Prices', href: '/medicine-prices', icon: 'tag', section: 'Medicines' },
|
|
|
|
|
{ id: 'stores', label: 'Stores', href: '/stores', icon: 'store', section: 'Medicines' },
|
|
|
|
|
{
|
|
|
|
|
id: 'activity',
|
|
|
|
|
label: 'Activity & Spend',
|
|
|
|
|
href: '/medicines/activity',
|
|
|
|
|
icon: 'trend',
|
|
|
|
|
section: 'Medicines',
|
|
|
|
|
},
|
|
|
|
|
{ id: 'settings', label: 'Settings', href: '/settings', icon: 'settings' },
|
2026-05-14 18:38:50 +09:00
|
|
|
{
|
|
|
|
|
id: 'meal-plans',
|
|
|
|
|
label: 'Meal Planner',
|
|
|
|
|
href: '/meal-plans',
|
|
|
|
|
icon: 'calendar',
|
|
|
|
|
section: 'Food',
|
|
|
|
|
},
|
2026-05-14 14:47:23 +09:00
|
|
|
{
|
|
|
|
|
id: 'recipes',
|
|
|
|
|
label: 'Recipes',
|
|
|
|
|
href: '/recipes',
|
|
|
|
|
icon: 'list',
|
|
|
|
|
section: 'Food',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pantry',
|
|
|
|
|
label: 'Pantry',
|
|
|
|
|
href: '/pantry',
|
|
|
|
|
icon: 'fridge',
|
|
|
|
|
section: 'Food',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'products',
|
|
|
|
|
label: 'Product Library',
|
|
|
|
|
href: '/products',
|
|
|
|
|
icon: 'list',
|
|
|
|
|
section: 'Food',
|
|
|
|
|
},
|
2026-03-27 14:50:34 +09:00
|
|
|
];
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
function groupNav(items: NavItem[]): [string, NavItem[]][] {
|
|
|
|
|
const map = new Map<string, NavItem[]>();
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const key = item.section ?? '__root__';
|
|
|
|
|
if (!map.has(key)) map.set(key, []);
|
|
|
|
|
map.get(key)!.push(item);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(map.entries());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:50:34 +09:00
|
|
|
export function Sidebar() {
|
2026-04-26 18:44:59 +09:00
|
|
|
const pathname = usePathname();
|
|
|
|
|
const { profile } = useApi();
|
|
|
|
|
const displayName = profile?.displayName ?? 'User';
|
|
|
|
|
const groups = groupNav(NAV);
|
|
|
|
|
|
2026-03-27 14:50:34 +09:00
|
|
|
return (
|
2026-04-26 18:44:59 +09:00
|
|
|
<aside
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--bg-elev)',
|
|
|
|
|
borderRight: '1px solid var(--border)',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
position: 'sticky',
|
|
|
|
|
top: 0,
|
|
|
|
|
height: '100vh',
|
|
|
|
|
width: 248,
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Brand */}
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
gap: 10,
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
padding: '18px 18px 14px',
|
|
|
|
|
borderBottom: '1px solid var(--border)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: 34,
|
|
|
|
|
height: 34,
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
display: 'grid',
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
background: 'var(--brand-soft)',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<rect x="3" y="3" width="18" height="18" rx="5" fill="var(--brand)" />
|
|
|
|
|
<path
|
|
|
|
|
d="M7 15l3-6 2 4 2-3 3 5"
|
|
|
|
|
stroke="var(--brand-ink)"
|
|
|
|
|
strokeWidth="1.8"
|
|
|
|
|
fill="none"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontFamily: 'var(--font-display)',
|
|
|
|
|
fontSize: 17,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
letterSpacing: '-0.02em',
|
|
|
|
|
color: 'var(--ink-strong)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
MeshiTrack
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 14:50:34 +09:00
|
|
|
</div>
|
2026-04-26 18:44:59 +09:00
|
|
|
|
|
|
|
|
{/* Navigation */}
|
|
|
|
|
<nav
|
|
|
|
|
style={{ flex: 1, overflowY: 'auto', padding: '10px 10px 16px' }}
|
|
|
|
|
aria-label="Main navigation"
|
|
|
|
|
>
|
|
|
|
|
{groups.map(([section, items]) => (
|
|
|
|
|
<div key={section} style={{ marginBottom: 14 }}>
|
|
|
|
|
{section !== '__root__' && (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
letterSpacing: '0.08em',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
color: 'var(--ink-faint)',
|
|
|
|
|
padding: '10px 10px 6px',
|
|
|
|
|
}}
|
2026-03-27 14:50:34 +09:00
|
|
|
>
|
2026-04-26 18:44:59 +09:00
|
|
|
{section}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{items.map((item) => {
|
|
|
|
|
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
key={item.id}
|
|
|
|
|
href={item.href}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 10,
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '7px 10px',
|
|
|
|
|
borderRadius: 'var(--r-sm)',
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
color: isActive ? 'var(--brand-soft-ink)' : 'var(--ink-muted)',
|
|
|
|
|
background: isActive ? 'var(--brand-soft)' : 'transparent',
|
|
|
|
|
fontWeight: isActive ? 500 : 400,
|
|
|
|
|
transition: 'background 0.1s, color 0.1s',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon
|
|
|
|
|
name={item.icon}
|
|
|
|
|
size={16}
|
|
|
|
|
style={{ opacity: isActive ? 1 : 0.8, flexShrink: 0 }}
|
|
|
|
|
/>
|
|
|
|
|
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
|
|
|
{item.label}
|
|
|
|
|
</span>
|
|
|
|
|
{item.badge != null && (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--danger-soft)',
|
|
|
|
|
color: 'var(--danger)',
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
padding: '1px 6px',
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
minWidth: 18,
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
fontVariantNumeric: 'tabular-nums',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{item.badge}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-03-27 14:50:34 +09:00
|
|
|
</nav>
|
2026-04-26 18:44:59 +09:00
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: 10,
|
|
|
|
|
padding: '12px 14px',
|
|
|
|
|
borderTop: '1px solid var(--border)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Avatar name={displayName} size={32} />
|
|
|
|
|
<div style={{ minWidth: 0 }}>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
color: 'var(--ink-strong)',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{displayName}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ fontSize: 11, color: 'var(--ink-muted)' }}>Household owner</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 14:50:34 +09:00
|
|
|
</aside>
|
|
|
|
|
);
|
|
|
|
|
}
|