🔄 Restructured User Profile & Theme Toggle
✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
This commit is contained in:
parent
94f31a5d77
commit
9a5561c2c8
1 changed files with 152 additions and 78 deletions
|
|
@ -3,6 +3,125 @@ import { useRouter } from 'next/router';
|
|||
import Link from 'next/link';
|
||||
import { useTheme } from '../lib/theme-context';
|
||||
|
||||
// User Profile Dropdown Component
|
||||
function UserProfileDropdown({ user, onMobileMenuClose }) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const profileMenuItems = [
|
||||
{ name: 'Profile', href: '/profile', icon: 'user' },
|
||||
{ name: 'Settings', href: '/settings', icon: 'settings' },
|
||||
...(user?.role === 'admin' ? [
|
||||
{ name: 'Admin Panel', href: '/admin', icon: 'admin' }
|
||||
] : []),
|
||||
{ name: 'Logout', href: '/logout', icon: 'logout', isLogout: true }
|
||||
];
|
||||
|
||||
const getProfileIcon = (iconName) => {
|
||||
const icons = {
|
||||
user: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
),
|
||||
settings: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
),
|
||||
admin: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
logout: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
return icons[iconName] || icons.user;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Dropdown Menu */}
|
||||
{isDropdownOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Menu */}
|
||||
<div className="absolute bottom-full left-0 right-0 mb-2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border z-20" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border)' }}>
|
||||
<div className="py-2">
|
||||
{profileMenuItems.map((item) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-4 py-3 text-sm transition-all duration-200 cursor-pointer
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700
|
||||
${item.isLogout ? 'text-red-600 dark:text-red-400' : ''}
|
||||
`}
|
||||
style={{
|
||||
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(false);
|
||||
onMobileMenuClose();
|
||||
}}
|
||||
>
|
||||
<span className="mr-3" aria-hidden="true">{getProfileIcon(item.icon)}</span>
|
||||
<span className="font-medium">{item.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Profile Button */}
|
||||
<button
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="w-full flex items-center px-4 py-3 rounded-2xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 nav-item-hover"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--text-secondary)',
|
||||
'--tw-ring-color': 'var(--accent-ember)',
|
||||
'--tw-ring-offset-color': 'var(--bg-secondary)'
|
||||
}}
|
||||
aria-expanded={isDropdownOpen}
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<div className="h-8 w-8 logo-container mr-3">
|
||||
<span className="text-white text-xs font-medium">
|
||||
{user?.email?.charAt(0).toUpperCase() || 'G'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>
|
||||
{user?.email || 'Guest'}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{user?.role || 'visitor'}
|
||||
</p>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, showSearch = false }) {
|
||||
const router = useRouter();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
|
@ -203,85 +322,40 @@ export default function Layout({ children, user = { email: 'me@randallstillwell.
|
|||
))}
|
||||
</nav>
|
||||
|
||||
{/* Profile Section */}
|
||||
<div className="mt-auto space-y-4">
|
||||
{/* User Profile */}
|
||||
<div className="px-4 py-3 rounded-2xl" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-10 w-10 logo-container">
|
||||
<span className="text-white text-sm font-medium">
|
||||
{user?.email?.charAt(0).toUpperCase() || 'G'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{user?.email || 'Guest'}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{user?.role || 'visitor'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Bottom Section */}
|
||||
<div className="mt-auto space-y-2">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-lg transition-all duration-200 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
className="w-full flex items-center px-4 py-3 rounded-2xl transition-all duration-200 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 nav-item-hover"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--text-secondary)',
|
||||
'--tw-ring-color': 'var(--accent-ember)',
|
||||
'--tw-ring-offset-color': 'var(--bg-tertiary)'
|
||||
'--tw-ring-offset-color': 'var(--bg-secondary)'
|
||||
}}
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
>
|
||||
<span className="mr-3" aria-hidden="true">
|
||||
{theme === 'light' ? (
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-primary)' }}>
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-primary)' }}>
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-medium">{theme === 'light' ? 'Dark Mode' : 'Light Mode'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<nav className="space-y-1" role="navigation" aria-label="User navigation">
|
||||
{bottomNavigation.map((item) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<div
|
||||
className={`
|
||||
nav-item-bottom flex items-center px-4 py-2 rounded-xl
|
||||
transition-all duration-200 cursor-pointer
|
||||
focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2
|
||||
${item.active
|
||||
? 'shadow-md nav-item-active'
|
||||
: 'hover:shadow-sm nav-item-hover'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: item.active ? 'var(--bg-tertiary)' : 'transparent',
|
||||
color: item.active ? 'var(--text-primary)' : 'var(--text-secondary)',
|
||||
'--tw-ring-color': 'var(--accent-ember)',
|
||||
'--tw-ring-offset-color': 'var(--bg-secondary)'
|
||||
}}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
role="menuitem"
|
||||
aria-current={item.active ? 'page' : undefined}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="mr-3" aria-hidden="true">{getIcon(item.icon)}</span>
|
||||
<span className="text-sm font-medium">{item.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
{/* User Profile Dropdown */}
|
||||
<UserProfileDropdown
|
||||
user={user}
|
||||
onMobileMenuClose={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue