317 lines
13 KiB
TypeScript
317 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Search,
|
|
Bell,
|
|
Sun,
|
|
Moon,
|
|
LogOut,
|
|
Menu,
|
|
User,
|
|
Settings,
|
|
HelpCircle,
|
|
ShieldAlert,
|
|
AlertTriangle,
|
|
ShoppingCart,
|
|
Package,
|
|
ChevronDown,
|
|
} from 'lucide-react';
|
|
import { useTheme } from 'next-themes';
|
|
import { authService } from '@/services/api/authService';
|
|
import { parseJwt, getAccessToken } from '@/services/api/client';
|
|
import { useState, useEffect } from 'react';
|
|
import { toast } from 'sonner';
|
|
import Link from 'next/link';
|
|
import { SlideOver } from '@/components/ui/SlideOver';
|
|
|
|
interface HeaderProps {
|
|
onMenuToggle: () => void;
|
|
}
|
|
|
|
interface NotificationItem {
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
time: string;
|
|
type: 'security' | 'stock' | 'order' | 'ticket';
|
|
read: boolean;
|
|
}
|
|
|
|
export function Header({ onMenuToggle }: HeaderProps) {
|
|
const router = useRouter();
|
|
const { theme, setTheme } = useTheme();
|
|
const [userName, setUserName] = useState('');
|
|
const [userEmail, setUserEmail] = useState('');
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const [notificationsOpen, setNotificationsOpen] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
|
|
|
const unreadCount = notifications.filter(n => !n.read).length;
|
|
|
|
const markAllAsRead = () => {
|
|
setNotifications(notifications.map(n => ({ ...n, read: true })));
|
|
toast.success('All notifications marked as read');
|
|
};
|
|
|
|
const toggleReadStatus = (id: string) => {
|
|
setNotifications(notifications.map(n => n.id === id ? { ...n, read: !n.read } : n));
|
|
};
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
const token = getAccessToken();
|
|
if (token) {
|
|
const payload = parseJwt(token);
|
|
if (payload) {
|
|
setUserEmail((payload.email as string) || '');
|
|
setUserName((payload.email as string)?.split('@')[0] || 'Admin');
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await authService.logout();
|
|
toast.success('Signed out');
|
|
} catch {
|
|
// Logout anyway on client side
|
|
}
|
|
router.push('/login');
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<header className="sticky top-0 z-10 h-14 bg-card border-b border-border flex items-center justify-between px-3 sm:px-5">
|
|
{/* Left Side */}
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={onMenuToggle}
|
|
className="lg:hidden w-[38px] h-[38px] rounded-[5px] border border-border shadow-xs flex items-center justify-center hover:bg-muted cursor-pointer transition-colors"
|
|
aria-label="Toggle menu"
|
|
>
|
|
<Menu className="w-[18px] h-[18px] text-foreground" />
|
|
</button>
|
|
|
|
<div className="relative hidden sm:block">
|
|
<input
|
|
type="text"
|
|
placeholder="Search Keyword"
|
|
className="pl-3 pr-9 h-[38px] w-[240px] bg-card border border-border rounded-[5px] shadow-xs text-[13px] text-foreground placeholder:text-muted-foreground outline-hidden focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors"
|
|
/>
|
|
<Search className="absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side */}
|
|
<div className="flex items-center gap-2">
|
|
{/* Theme Toggle */}
|
|
<button
|
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
className="w-[38px] h-[38px] rounded-[5px] border border-border shadow-xs flex items-center justify-center bg-warning/10 text-warning hover:bg-warning hover:text-white cursor-pointer transition-colors"
|
|
aria-label="Toggle theme"
|
|
>
|
|
{mounted && theme === 'dark' ? (
|
|
<Sun className="w-[18px] h-[18px]" />
|
|
) : (
|
|
<Moon className="w-[18px] h-[18px]" />
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setNotificationsOpen(true)}
|
|
className="w-[38px] h-[38px] rounded-[5px] border border-border shadow-xs flex items-center justify-center hover:bg-muted cursor-pointer transition-colors relative"
|
|
aria-label="Notifications"
|
|
>
|
|
<Bell className="w-[18px] h-[18px] text-foreground" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-0.5 rounded-full bg-primary text-[8px] font-bold text-white flex items-center justify-center">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{/* Divider */}
|
|
<div className="w-px h-5 bg-border mx-1" />
|
|
|
|
{/* User Profile Dropdown */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
className="flex items-center gap-2.5 pl-1 cursor-pointer focus:outline-hidden group"
|
|
>
|
|
<div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-bold text-white relative bg-sidebar">
|
|
{userName ? userName.charAt(0).toUpperCase() : 'A'}
|
|
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-success border-2 border-card" />
|
|
</div>
|
|
<div className="hidden md:block text-left">
|
|
<p className="text-[13px] font-semibold text-foreground leading-tight capitalize group-hover:text-primary transition-colors">
|
|
{userName || 'Admin'}
|
|
</p>
|
|
<p className="text-[11px] text-muted-foreground leading-tight">
|
|
{userEmail || 'admin@ifixkart.com'}
|
|
</p>
|
|
</div>
|
|
<ChevronDown className="hidden md:block w-3.5 h-3.5 text-muted-foreground" />
|
|
</button>
|
|
|
|
{dropdownOpen && (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-30"
|
|
onClick={() => setDropdownOpen(false)}
|
|
/>
|
|
<div className="absolute right-0 mt-2 w-60 rounded-[5px] bg-card border border-border shadow-lg py-1 z-40">
|
|
{/* User Info Header */}
|
|
<div className="px-4 py-3 flex items-center gap-3 border-b border-border">
|
|
<div className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold text-white bg-sidebar">
|
|
{userName ? userName.charAt(0).toUpperCase() : 'A'}
|
|
</div>
|
|
<div>
|
|
<h4 className="text-[13px] font-semibold text-foreground capitalize">{userName || 'Admin'}</h4>
|
|
<p className="text-[11px] text-muted-foreground mt-0.5">Administrator</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Menu Items */}
|
|
<div className="p-1 space-y-0.5 text-[13px] text-muted-foreground">
|
|
<Link
|
|
href="/settings?tab=profile"
|
|
onClick={() => setDropdownOpen(false)}
|
|
className="flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-muted hover:text-foreground transition-colors"
|
|
>
|
|
<User className="w-4 h-4" />
|
|
<span>Profile Settings</span>
|
|
</Link>
|
|
<div className="flex items-center justify-between px-3 py-2 rounded-md hover:bg-muted hover:text-foreground transition-colors">
|
|
<div className="flex items-center gap-2.5">
|
|
<Bell className="w-4 h-4" />
|
|
<span>Notifications</span>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" className="sr-only peer" defaultChecked />
|
|
<div className="w-7 h-4 bg-muted peer-focus:outline-hidden rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-card after:border-border after:border after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:bg-primary"></div>
|
|
</label>
|
|
</div>
|
|
<Link
|
|
href="/settings?tab=help"
|
|
onClick={() => setDropdownOpen(false)}
|
|
className="flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-muted hover:text-foreground transition-colors"
|
|
>
|
|
<HelpCircle className="w-4 h-4" />
|
|
<span>Help & Support</span>
|
|
</Link>
|
|
<Link
|
|
href="/settings"
|
|
onClick={() => setDropdownOpen(false)}
|
|
className="flex items-center gap-2.5 px-3 py-2 rounded-md hover:bg-muted hover:text-foreground transition-colors"
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
<span>Settings</span>
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="h-px bg-border my-1" />
|
|
|
|
{/* Sign Out */}
|
|
<div className="p-1">
|
|
<button
|
|
onClick={() => {
|
|
setDropdownOpen(false);
|
|
handleLogout();
|
|
}}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-[13px] font-medium text-destructive hover:bg-destructive/10 cursor-pointer transition-colors"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
<span>Sign Out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<SlideOver
|
|
open={notificationsOpen}
|
|
onClose={() => setNotificationsOpen(false)}
|
|
title="Notifications"
|
|
icon={<Bell className="w-4 h-4 text-primary" />}
|
|
size="compact"
|
|
>
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<div className="shrink-0 px-5 py-3 border-b border-border flex items-center justify-between gap-3">
|
|
<p className="text-[13px] text-muted-foreground">
|
|
{unreadCount > 0 ? `${unreadCount} unread` : 'You are all caught up'}
|
|
</p>
|
|
{unreadCount > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={markAllAsRead}
|
|
className="text-[13px] font-semibold text-primary hover:text-primary/80 cursor-pointer"
|
|
>
|
|
Mark all as read
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
{notifications.length === 0 ? (
|
|
<div className="py-16 px-5 text-center text-[13px] text-muted-foreground">No notifications yet.</div>
|
|
) : (
|
|
notifications.map((notification) => (
|
|
<button
|
|
key={notification.id}
|
|
type="button"
|
|
onClick={() => toggleReadStatus(notification.id)}
|
|
className={`w-full text-left px-5 py-3.5 flex items-start gap-3 border-b border-border cursor-pointer transition-colors ${
|
|
notification.read ? 'bg-card hover:bg-muted/40' : 'bg-primary/5 hover:bg-primary/10'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-9 h-9 crm-radius-icon flex items-center justify-center shrink-0 text-white ${
|
|
notification.type === 'security'
|
|
? 'bg-destructive'
|
|
: notification.type === 'stock'
|
|
? 'bg-warning'
|
|
: notification.type === 'order'
|
|
? 'bg-[#3b82f6]'
|
|
: 'bg-success'
|
|
}`}
|
|
>
|
|
{notification.type === 'security' && <ShieldAlert className="w-4 h-4" />}
|
|
{notification.type === 'stock' && <AlertTriangle className="w-4 h-4" />}
|
|
{notification.type === 'order' && <ShoppingCart className="w-4 h-4" />}
|
|
{notification.type === 'ticket' && <Package className="w-4 h-4" />}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<p className="text-[13px] font-semibold text-foreground leading-5">{notification.title}</p>
|
|
<span className="text-[12px] text-muted-foreground whitespace-nowrap shrink-0">{notification.time}</span>
|
|
</div>
|
|
<p className="text-[13px] text-muted-foreground mt-1 leading-5">{notification.message}</p>
|
|
</div>
|
|
{!notification.read && <span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0 mt-2" />}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<div className="shrink-0 px-5 py-3 border-t border-border">
|
|
<Link
|
|
href="/activity-logs"
|
|
onClick={() => setNotificationsOpen(false)}
|
|
className="inline-flex items-center justify-center h-9 w-full crm-radius-control border border-border bg-card text-[13px] font-semibold text-foreground hover:bg-muted cursor-pointer"
|
|
>
|
|
View all activity logs
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</SlideOver>
|
|
</>
|
|
);
|
|
}
|