Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 43 additions & 26 deletions components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import { useState } from 'react';
import Link from 'next/link';
import { Menu, X } from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import ProfileDropdown from './ProfileDropdown';
export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false);
const { isAuthenticated } = useAuthStore();

return (
<header
Expand All @@ -24,18 +27,24 @@ export default function Header() {

{/* Desktop nav */}
<div className="hidden md:flex items-center gap-6">
<Link
href="/auth/login"
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
Sign In
</Link>
<Link
href="/auth/signup"
className="inline-flex items-center justify-center text-sm font-semibold text-white bg-[#1a3a6b] hover:bg-[#15305a] rounded-lg px-5 py-2 transition-colors"
>
Get Started
</Link>
{isAuthenticated ? (
<ProfileDropdown />
) : (
<>
<Link
href="/auth/login"
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
Sign In
</Link>
<Link
href="/auth/signup"
className="inline-flex items-center justify-center text-sm font-semibold text-white bg-[#1a3a6b] hover:bg-[#15305a] rounded-lg px-5 py-2 transition-colors"
>
Get Started
</Link>
</>
)}
</div>

{/* Mobile hamburger */}
Expand All @@ -57,20 +66,28 @@ export default function Header() {
className="md:hidden border-t border-gray-200 px-4 py-4 space-y-3"
style={{ backgroundColor: '#eef3fa' }}
>
<Link
href="/auth/login"
onClick={() => setMobileOpen(false)}
className="block text-sm font-medium text-gray-700 hover:text-gray-900 py-2 transition-colors"
>
Sign In
</Link>
<Link
href="/auth/signup"
onClick={() => setMobileOpen(false)}
className="block w-full text-center text-sm font-semibold text-white bg-[#1a3a6b] hover:bg-[#15305a] rounded-lg px-5 py-2.5 transition-colors"
>
Get Started
</Link>
{isAuthenticated ? (
<div className="flex justify-center">
<ProfileDropdown />
</div>
) : (
<>
<Link
href="/auth/login"
onClick={() => setMobileOpen(false)}
className="block text-sm font-medium text-gray-700 hover:text-gray-900 py-2 transition-colors"
>
Sign In
</Link>
<Link
href="/auth/signup"
onClick={() => setMobileOpen(false)}
className="block w-full text-center text-sm font-semibold text-white bg-[#1a3a6b] hover:bg-[#15305a] rounded-lg px-5 py-2.5 transition-colors"
>
Get Started
</Link>
</>
)}
</div>
)}
</header>
Expand Down
192 changes: 192 additions & 0 deletions components/ProfileDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
'use client';

import { useState, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { User, Settings, LayoutDashboard, LogOut, ChevronDown } from 'lucide-react';
import { useAuthStore } from '@/store/authStore';

export default function ProfileDropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { user, logout } = useAuthStore();

// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

// Handle keyboard navigation
useEffect(() => {
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
setIsOpen(false);
}
}

document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, []);

const handleLogout = async () => {
try {
// Clear auth store
logout();

// Clear any persisted auth data
localStorage.removeItem('auth-storage');

// Redirect to home
router.push('/');

// Close dropdown
setIsOpen(false);
} catch (error) {
console.error('Logout error:', error);
}
};

const menuItems = [
{
icon: User,
label: 'Profile',
href: '/profile',
description: 'View your profile'
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
description: 'Account settings'
},
{
icon: LayoutDashboard,
label: 'Dashboard',
href: '/dashboard',
description: 'Go to dashboard'
},
{
icon: LogOut,
label: 'Logout',
href: '#',
description: 'Sign out of your account',
onClick: handleLogout,
isDanger: true
}
];

const getUserInitials = (name: string) => {
return name
.split(' ')
.map(word => word.charAt(0).toUpperCase())
.join('')
.slice(0, 2);
};

return (
<div className="relative" ref={dropdownRef}>
{/* Trigger Button */}
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-100 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-[#1a3a6b] focus:ring-offset-2"
aria-expanded={isOpen}
aria-haspopup="true"
>
{/* User Avatar */}
<div className="w-8 h-8 rounded-full bg-[#1a3a6b] flex items-center justify-center text-white text-sm font-medium">
{user?.avatar ? (
<img
src={user.avatar}
alt={user.name}
className="w-full h-full rounded-full object-cover"
/>
) : (
<span>{user?.name ? getUserInitials(user.name) : 'U'}</span>
)}
</div>

{/* User Name */}
<span className="hidden sm:block text-sm font-medium text-gray-700">
{user?.name || 'User'}
</span>

{/* Chevron Icon */}
<ChevronDown
className={`w-4 h-4 text-gray-500 transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</button>

{/* Dropdown Menu */}
<div
className={`absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50 transition-all duration-200 transform origin-top-right ${
isOpen
? 'opacity-100 scale-100 translate-y-0'
: 'opacity-0 scale-95 -translate-y-2 pointer-events-none'
}`}
>
{/* User Info Header */}
<div className="px-4 py-3 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900">{user?.name || 'User'}</p>
<p className="text-xs text-gray-500 truncate">{user?.email}</p>
</div>

{/* Menu Items */}
<div className="py-2">
{menuItems.map((item, index) => {
const Icon = item.icon;

if (item.onClick) {
// Logout button
return (
<button
key={index}
onClick={item.onClick}
className={`w-full flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-50 transition-colors duration-150 focus:outline-none focus:bg-gray-50 ${
item.isDanger ? 'text-red-600 hover:text-red-700' : 'text-gray-700'
}`}
>
<Icon className="w-4 h-4" />
<div className="flex-1 text-left">
<p className="font-medium">{item.label}</p>
<p className="text-xs text-gray-500">{item.description}</p>
</div>
</button>
);
} else {
// Regular navigation links
return (
<Link
key={index}
href={item.href}
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-150 focus:outline-none focus:bg-gray-50"
>
<Icon className="w-4 h-4" />
<div className="flex-1">
<p className="font-medium">{item.label}</p>
<p className="text-xs text-gray-500">{item.description}</p>
</div>
</Link>
);
}
})}
</div>
</div>
</div>
);
}
14 changes: 1 addition & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading