ifixkart-admin/components/layouts/Header.tsx

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>
</>
);
}