Notifications added

This commit is contained in:
Sidharth Prabhu
2026-04-21 14:15:55 +05:30
parent 3e35bacc14
commit 21c581b7eb
9 changed files with 373 additions and 8 deletions

View File

@@ -44,8 +44,11 @@ const PAGES = [
const Header = () => {
const navigate = useNavigate();
const searchRef = useRef<HTMLDivElement>(null);
const notificationRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const [showResults, setShowResults] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [notifications, setNotifications] = useState<any[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const filteredPages = PAGES.filter(page =>
@@ -60,25 +63,47 @@ const Header = () => {
day: 'numeric',
});
const fetchNotifications = async () => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/notifications`);
if (response.ok) {
const data = await response.json();
setNotifications(data);
}
} catch (error) {
console.error('Error fetching notifications:', error);
}
};
useEffect(() => {
fetchNotifications();
const interval = setInterval(fetchNotifications, 30000); // Poll every 30s
return () => clearInterval(interval);
}, []);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setShowResults(false);
}
if (notificationRef.current && !notificationRef.current.contains(event.target as Node)) {
setShowNotifications(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
if (e.key === 'ArrowDown' && showResults) {
setSelectedIndex(prev => (prev + 1) % filteredPages.length);
} else if (e.key === 'ArrowUp') {
} else if (e.key === 'ArrowUp' && showResults) {
setSelectedIndex(prev => (prev - 1 + filteredPages.length) % filteredPages.length);
} else if (e.key === 'Enter' && filteredPages[selectedIndex]) {
} else if (e.key === 'Enter' && showResults && filteredPages[selectedIndex]) {
handleNavigate(filteredPages[selectedIndex].path);
} else if (e.key === 'Escape') {
setShowResults(false);
setShowNotifications(false);
}
};
@@ -88,6 +113,37 @@ const Header = () => {
setShowResults(false);
};
const handleNotificationClick = async (notif: any) => {
try {
await fetch(`http://${window.location.hostname}:8080/api/notifications/mark-read/${notif.id}`, { method: 'POST' });
if (notif.link) navigate(notif.link);
setShowNotifications(false);
fetchNotifications();
} catch (error) {
console.error('Error marking notification as read:', error);
}
};
const markAllAsRead = async () => {
try {
await fetch(`http://${window.location.hostname}:8080/api/notifications/mark-all-read`, { method: 'POST' });
fetchNotifications();
setShowNotifications(false);
} catch (error) {
console.error('Error marking all as read:', error);
}
};
const getNotificationIcon = (type: string) => {
switch(type) {
case 'FEEDBACK': return <MessageSquare size={16} className="text-emerald-500" />;
case 'PURCHASE': return <ShoppingBag size={16} className="text-indigo-500" />;
case 'PRODUCT': return <Package size={16} className="text-amber-500" />;
case 'COUPON': return <Ticket size={16} className="text-rose-500" />;
default: return <Bell size={16} className="text-slate-400" />;
}
};
return (
<header className="h-16 bg-white/80 backdrop-blur-md border-b border-[#e2e8f0] px-8 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-4 flex-1">
@@ -161,10 +217,72 @@ const Header = () => {
</div>
<div className="flex items-center gap-4 border-l border-[#e2e8f0] pl-6">
<button className="relative p-2 text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100 rounded-lg transition-all group">
<Bell size={20} />
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
</button>
<div className="relative" ref={notificationRef}>
<button
onClick={() => setShowNotifications(!showNotifications)}
className={`relative p-2 rounded-lg transition-all group ${showNotifications ? 'bg-indigo-50 text-[#0f4475]' : 'text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100'}`}
>
<Bell size={20} />
{notifications.length > 0 && (
<span className="absolute top-1.5 right-1.5 w-4 h-4 bg-red-500 rounded-full border-2 border-white text-[10px] text-white font-bold flex items-center justify-center animate-pulse">
{notifications.length}
</span>
)}
</button>
<AnimatePresence>
{showNotifications && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 5, scale: 0.95 }}
className="absolute right-0 top-full mt-2 w-80 bg-white/95 backdrop-blur-xl border border-[#e2e8f0] rounded-2xl shadow-2xl overflow-hidden z-50"
>
<div className="p-4 border-b border-[#e2e8f0] flex items-center justify-between bg-gray-50/50">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-wider">Notifications</h3>
{notifications.length > 0 && (
<button
onClick={markAllAsRead}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 uppercase tracking-tighter"
>
Clear All
</button>
)}
</div>
<div className="max-h-[400px] overflow-y-auto custom-scrollbar">
{notifications.length > 0 ? (
notifications.map((notif) => (
<button
key={notif.id}
onClick={() => handleNotificationClick(notif)}
className="w-full p-4 border-b border-gray-50 hover:bg-gray-50/80 transition-colors flex gap-3 text-left group"
>
<div className="mt-1 shrink-0 p-2 bg-white rounded-xl shadow-sm border border-gray-100 group-hover:border-indigo-100 transition-colors">
{getNotificationIcon(notif.type)}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-black text-slate-800 mb-0.5 truncate">{notif.title}</div>
<div className="text-[11px] text-slate-500 font-medium leading-relaxed line-clamp-2">{notif.message}</div>
<div className="mt-2 text-[9px] font-bold text-slate-400 uppercase tracking-widest">
{new Date(notif.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</button>
))
) : (
<div className="p-12 text-center">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<Bell size={24} className="text-slate-200" />
</div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">All caught up!</p>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link to="/settings" className="p-2 text-[#64748b] hover:text-[#0f4475] hover:bg-indigo-50 rounded-lg transition-all">
<Settings size={20} />