🔄 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:
Randall Stillwell 2025-07-26 09:51:40 -05:00
parent 94f31a5d77
commit 9a5561c2c8

View file

@ -3,6 +3,125 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import { useTheme } from '../lib/theme-context'; 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 }) { export default function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, showSearch = false }) {
const router = useRouter(); const router = useRouter();
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
@ -203,85 +322,40 @@ export default function Layout({ children, user = { email: 'me@randallstillwell.
))} ))}
</nav> </nav>
{/* Profile Section */} {/* Bottom Section */}
<div className="mt-auto space-y-4"> <div className="mt-auto space-y-2">
{/* User Profile */} {/* Theme Toggle */}
<div className="px-4 py-3 rounded-2xl" style={{ backgroundColor: 'var(--bg-tertiary)' }}> <button
<div className="flex items-center space-x-3"> onClick={toggleTheme}
<div className="h-10 w-10 logo-container"> 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"
<span className="text-white text-sm font-medium"> style={{
{user?.email?.charAt(0).toUpperCase() || 'G'} backgroundColor: 'transparent',
</span> color: 'var(--text-secondary)',
</div> '--tw-ring-color': 'var(--accent-ember)',
<div className="flex-1"> '--tw-ring-offset-color': 'var(--bg-secondary)'
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> }}
{user?.email || 'Guest'} aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
</p> title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}> >
{user?.role || 'visitor'} <span className="mr-3" aria-hidden="true">
</p> {theme === 'light' ? (
</div> <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button <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" />
onClick={toggleTheme} </svg>
className="p-2 rounded-lg transition-all duration-200 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2" ) : (
style={{ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
backgroundColor: 'var(--bg-primary)', <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" />
'--tw-ring-color': 'var(--accent-ember)', </svg>
'--tw-ring-offset-color': 'var(--bg-tertiary)' )}
}} </span>
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} <span className="font-medium">{theme === 'light' ? 'Dark Mode' : 'Light Mode'}</span>
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} </button>
>
{theme === 'light' ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-primary)' }}>
<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)' }}>
<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>
)}
</button>
</div>
</div>
{/* Bottom Navigation */} {/* User Profile Dropdown */}
<nav className="space-y-1" role="navigation" aria-label="User navigation"> <UserProfileDropdown
{bottomNavigation.map((item) => ( user={user}
<Link key={item.name} href={item.href}> onMobileMenuClose={() => setIsMobileMenuOpen(false)}
<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>
</div> </div>
</div> </div>
</div> </div>