Files
Pennyzo-webapp-frontend/src/components/SidebarNav.tsx

220 lines
6.9 KiB
TypeScript

import React, { useMemo } from 'react';
import type { CSSProperties } from 'react';
import { theme } from '../theme/index';
import icLauncher from '../assets/ic_launcher_foreground.png';
export interface SidebarNavItem {
id: string;
label: string;
icon: React.ReactNode;
badge?: number;
}
interface SidebarNavProps {
items: SidebarNavItem[];
footerItems?: SidebarNavItem[];
activeId: string;
collapsed?: boolean;
onSelect: (id: string) => void;
onToggleCollapse?: (collapsed: boolean) => void;
variant?: 'desktop' | 'mobile';
}
const SidebarNav: React.FC<SidebarNavProps> = ({
items,
footerItems,
activeId,
collapsed = false,
onSelect,
onToggleCollapse,
variant = 'desktop',
}) => {
const containerStyles: CSSProperties = useMemo(
() => ({
width: collapsed ? '92px' : '280px',
transition: 'width 240ms ease',
background: 'linear-gradient(180deg, rgba(4, 24, 21, 0.98) 0%, rgba(6, 35, 31, 0.95) 100%)',
borderRight: variant === 'desktop' ? `1px solid rgba(203, 169, 81, 0.15)` : undefined,
padding: collapsed ? '28px 12px 24px' : '28px 20px 24px',
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.md,
height: variant === 'desktop' ? '100vh' : '100%',
position: variant === 'desktop' ? 'sticky' : 'relative',
top: variant === 'desktop' ? 0 : undefined,
backdropFilter: 'blur(16px)',
boxShadow: variant === 'mobile' ? theme.shadows.xl : 'none',
zIndex: variant === 'mobile' ? theme.zIndex.modal : theme.zIndex.sticky,
}),
[collapsed, variant],
);
const headerStyles: CSSProperties = useMemo(
() => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing.md,
paddingBottom: theme.spacing.lg,
borderBottom: `1px solid rgba(203, 169, 81, 0.12)`,
}),
[],
);
const logoStyles: CSSProperties = {
width: '48px',
height: '48px',
borderRadius: theme.borderRadius.sm,
backgroundColor: 'rgba(203, 169, 81, 0.12)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: 'inset 0 0 0 1px rgba(203, 169, 81, 0.2)',
};
const logoTextStyles: CSSProperties = {
...theme.typography.h4,
color: theme.colors.textPrimary,
};
const navListStyles: CSSProperties = useMemo(
() => ({
display: 'flex',
flexDirection: 'column',
gap: '2px',
flex: 1,
overflowY: 'auto',
paddingRight: collapsed ? 0 : '8px',
marginTop: theme.spacing.sm,
}),
[collapsed],
);
const footerStyles: CSSProperties = {
marginTop: 'auto',
paddingTop: theme.spacing.lg,
display: 'flex',
flexDirection: 'column',
gap: '2px',
};
const dividerStyles: CSSProperties = {
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(203, 169, 81, 0.3) 50%, transparent 100%)',
marginBottom: theme.spacing.sm,
};
return (
<aside style={containerStyles}>
<div style={headerStyles}>
<div style={logoStyles}>
<img
src={icLauncher}
alt="Pennyzo"
style={{ width: '60%', height: '60%', objectFit: 'contain' }}
/>
</div>
{!collapsed && <span style={logoTextStyles}>Pennyzo</span>}
{variant === 'desktop' && onToggleCollapse && (
<button
onClick={() => onToggleCollapse(!collapsed)}
aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
style={{
marginLeft: 'auto',
width: '36px',
height: '36px',
borderRadius: theme.borderRadius.md,
border: `1px solid rgba(203, 169, 81, 0.25)`,
backgroundColor: 'rgba(203, 169, 81, 0.08)',
color: theme.colors.highlight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 180ms ease',
fontSize: '18px',
fontWeight: 'bold',
}}
>
{collapsed ? '»' : '«'}
</button>
)}
</div>
{/* Decorative divider - works in both collapsed and expanded states */}
<div style={{
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(203, 169, 81, 0.3) 50%, transparent 100%)',
marginTop: theme.spacing.md,
marginBottom: theme.spacing.sm,
}} />
<nav
style={navListStyles}
className="custom-scrollbar pennyzo-scroll"
>
{items.map((item) => {
const isActive = activeId === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => onSelect(item.id)}
className={`nav-item ${isActive ? 'nav-item-active' : 'nav-item-inactive'} pennyzo-transition`}
aria-label={item.label}
title={collapsed ? item.label : undefined}
>
{isActive && <div className="nav-item-indicator" />}
<div className={`nav-icon ${isActive ? 'nav-icon-active' : 'nav-icon-inactive'}`}>
{item.icon}
</div>
{!collapsed && (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: theme.spacing.sm }}>
<span className={`nav-label ${isActive ? 'nav-label-active' : 'nav-label-inactive'}`}>
{item.label}
</span>
{item.badge && item.badge > 0 && (
<span className="pennyzo-badge">{item.badge > 99 ? '99+' : item.badge}</span>
)}
</div>
)}
</button>
);
})}
</nav>
{footerItems && footerItems.length > 0 && (
<div style={footerStyles}>
{/* Footer divider */}
<div style={dividerStyles} />
{footerItems.map((item) => {
const isActive = activeId === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => onSelect(item.id)}
className={`nav-item ${isActive ? 'nav-item-active' : 'nav-item-inactive'} pennyzo-transition`}
aria-label={item.label}
title={collapsed ? item.label : undefined}
>
{isActive && <div className="nav-item-indicator" />}
<div className={`nav-icon ${isActive ? 'nav-icon-active' : 'nav-icon-inactive'}`}>
{item.icon}
</div>
{!collapsed && (
<span className={`nav-label ${isActive ? 'nav-label-active' : 'nav-label-inactive'}`}>
{item.label}
</span>
)}
</button>
);
})}
</div>
)}
</aside>
);
};
export default SidebarNav;