UI/UX improvements: Premium redesign with navigation restructure, glassmorphism stat cards, and background integration
This commit is contained in:
@@ -1,99 +1,217 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { theme } from '../theme/index';
|
||||
import icLauncher from '../assets/ic_launcher_foreground.png';
|
||||
|
||||
type Item = {
|
||||
export interface SidebarNavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
interface SidebarNavProps {
|
||||
items: Item[];
|
||||
activeId: string;
|
||||
onSelect: (id: string) => void;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
const SidebarNav: React.FC<SidebarNavProps> = ({ items, activeId, onSelect }) => {
|
||||
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 className="w-16 md:w-56 border-r border-luxury-gold/20 bg-gradient-to-b from-deep-forest/90 via-deep-forest/80 to-rich-brown/70 backdrop-blur-xl sticky top-16 md:top-20 h-[calc(100vh-4rem)] md:h-[calc(100vh-5rem)] overflow-hidden">
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -top-20 -left-10 w-40 h-40 bg-luxury-gold/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-20 -right-10 w-32 h-32 bg-bronze/5 rounded-full blur-2xl"></div>
|
||||
<div className="absolute top-1/2 left-0 w-0.5 h-32 bg-gradient-to-b from-transparent via-luxury-gold/20 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{/* Premium border accent */}
|
||||
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-luxury-gold/40 to-transparent"></div>
|
||||
|
||||
<nav className="relative p-2 md:p-4 space-y-1.5">
|
||||
{/* Navigation title */}
|
||||
<div className="hidden md:block mb-3">
|
||||
<h3 className="text-luxury-gold/70 text-[10px] font-montserrat font-medium uppercase tracking-wider mb-1">Navigation</h3>
|
||||
<div className="w-6 h-0.5 bg-gradient-to-r from-luxury-gold to-transparent rounded-full"></div>
|
||||
<aside style={containerStyles}>
|
||||
<div style={headerStyles}>
|
||||
<div style={logoStyles}>
|
||||
<img
|
||||
src={icLauncher}
|
||||
alt="Pennyzo"
|
||||
style={{ width: '60%', height: '60%', objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{items.map((it, index) => {
|
||||
const isActive = activeId === it.id;
|
||||
{!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={it.id}
|
||||
onClick={() => onSelect(it.id)}
|
||||
className={`group relative w-full flex items-center justify-start gap-2 md:gap-2.5 px-3 py-2.5 rounded-lg transition-colors duration-300 ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-luxury-gold/20 to-brass/20 border border-luxury-gold/50 text-cream shadow-[0_2px_8px_rgba(197,157,89,0.15)]'
|
||||
: 'border border-transparent hover:border-luxury-gold/35 hover:bg-gradient-to-r hover:from-luxury-gold/10 hover:to-transparent text-cream/80 hover:text-cream'
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${index * 100}ms`
|
||||
}}
|
||||
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}
|
||||
>
|
||||
{/* Icon container with enhanced styling */}
|
||||
<div className={`relative w-8 h-8 rounded-md flex items-center justify-center transition-all duration-300 ${
|
||||
isActive
|
||||
? 'bg-gradient-to-br from-luxury-gold/40 to-brass/40 ring-1 ring-luxury-gold/40'
|
||||
: 'bg-deep-forest/60 group-hover:bg-luxury-gold/20'
|
||||
}`}>
|
||||
<span className={`text-lg transition-all duration-300 ${
|
||||
isActive ? 'scale-110' : 'group-hover:scale-105'
|
||||
}`}>
|
||||
{it.icon}
|
||||
</span>
|
||||
{/* Active indicator dot */}
|
||||
{isActive && (
|
||||
<div className="absolute -right-1 -top-1 w-3 h-3 bg-luxury-gold rounded-full shadow-[0_0_8px_rgba(197,157,89,0.6)] animate-pulse"></div>
|
||||
)}
|
||||
{isActive && <div className="nav-item-indicator" />}
|
||||
<div className={`nav-icon ${isActive ? 'nav-icon-active' : 'nav-icon-inactive'}`}>
|
||||
{item.icon}
|
||||
</div>
|
||||
|
||||
{/* Label with enhanced typography */}
|
||||
<div className="hidden md:flex flex-1 min-w-0 items-center">
|
||||
<span className={`truncate text-sm md:text-base leading-snug font-montserrat font-medium text-left tracking-wide transition-colors duration-300 ${
|
||||
isActive ? 'text-cream' : 'text-cream/85 group-hover:text-cream'
|
||||
}`}>
|
||||
{it.label}
|
||||
</span>
|
||||
{/* Subtle underline for active item */}
|
||||
{isActive && (
|
||||
<div className="absolute -bottom-1 left-0 w-5 h-0.5 bg-gradient-to-r from-luxury-gold/70 to-transparent rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hover effect overlay */}
|
||||
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-luxury-gold/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
|
||||
|
||||
{/* Active selection indicator */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-gradient-to-b from-luxury-gold to-brass rounded-r-full shadow-[0_0_6px_rgba(197,157,89,0.45)]"></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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Bottom decorative element */}
|
||||
{/* Removed large decorative bottom element for compactness */}
|
||||
</nav>
|
||||
|
||||
{/* Bottom gradient fade */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-16 bg-gradient-to-t from-deep-forest/50 to-transparent pointer-events-none"></div>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user