| import { useState, useEffect, useRef } from "react" |
| import { motion, AnimatePresence } from "framer-motion" |
| import { toast } from "react-toastify" |
| import { useNavigate } from "react-router-dom" |
| import { |
| Bell, |
| X, |
| Check, |
| AlertCircle, |
| Info, |
| CheckCircle, |
| AlertTriangle, |
| Package, |
| ShoppingCart, |
| Heart, |
| Star, |
| Truck, |
| Gift, |
| Crown, |
| Zap, |
| Trash2, |
| Settings, |
| Eye, |
| EyeOff, |
| } from "lucide-react" |
| import { |
| subscribeToNotifications, |
| getNotifications, |
| markNotificationAsRead, |
| markAllNotificationsAsRead as markAllReadService, |
| deleteNotification as deleteNotificationService, |
| clearAllNotifications as clearAllService, |
| getUnreadCount as getUnreadCountService |
| } from "../utils/notificationService" |
| import "./NotificationCenter.css" |
|
|
| const NotificationCenter = () => { |
| const navigate = useNavigate() |
| const [isOpen, setIsOpen] = useState(false) |
| const [notifications, setNotifications] = useState([]) |
| const [unreadCount, setUnreadCount] = useState(0) |
| const [filter, setFilter] = useState("all") |
| const [showSettings, setShowSettings] = useState(false) |
|
|
| const notificationRef = useRef(null) |
| const timeoutRef = useRef(null) |
|
|
| const notificationTypes = { |
| success: { icon: CheckCircle, color: "#4ecdc4", bgColor: "rgba(78, 205, 196, 0.1)" }, |
| warning: { icon: AlertTriangle, color: "#f39c12", bgColor: "rgba(243, 156, 18, 0.1)" }, |
| error: { icon: AlertCircle, color: "#e74c3c", bgColor: "rgba(231, 76, 60, 0.1)" }, |
| info: { icon: Info, color: "#667eea", bgColor: "rgba(102, 126, 234, 0.1)" }, |
| order: { icon: Package, color: "#9b59b6", bgColor: "rgba(155, 89, 182, 0.1)" }, |
| cart: { icon: ShoppingCart, color: "#2ecc71", bgColor: "rgba(46, 204, 113, 0.1)" }, |
| wishlist: { icon: Heart, color: "#e91e63", bgColor: "rgba(233, 30, 99, 0.1)" }, |
| review: { icon: Star, color: "#ff9800", bgColor: "rgba(255, 152, 0, 0.1)" }, |
| shipping: { icon: Truck, color: "#00bcd4", bgColor: "rgba(0, 188, 212, 0.1)" }, |
| promotion: { icon: Gift, color: "#ff5722", bgColor: "rgba(255, 87, 34, 0.1)" }, |
| } |
|
|
| useEffect(() => { |
| const unsubscribe = subscribeToNotifications((updatedNotifications) => { |
| setNotifications(updatedNotifications) |
| setUnreadCount(getUnreadCountService()) |
| }) |
|
|
| setNotifications(getNotifications()) |
| setUnreadCount(getUnreadCountService()) |
|
|
| return unsubscribe |
| }, []) |
|
|
| useEffect(() => { |
| const handleClickOutside = (event) => { |
| if (notificationRef.current && !notificationRef.current.contains(event.target)) { |
| setIsOpen(false) |
| setShowSettings(false) |
| } |
| } |
|
|
| document.addEventListener("mousedown", handleClickOutside) |
| return () => document.removeEventListener("mousedown", handleClickOutside) |
| }, []) |
|
|
| const generateRandomNotification = () => { |
| const types = Object.keys(notificationTypes) |
| const randomType = types[Math.floor(Math.random() * types.length)] |
|
|
| const messages = { |
| success: ["Payment processed successfully!", "Account verified!", "Settings updated!"], |
| warning: ["Low stock alert for wishlist item", "Payment method expires soon", "Profile incomplete"], |
| error: ["Payment failed", "Connection timeout", "Invalid coupon code"], |
| info: ["New features available", "System maintenance scheduled", "Privacy policy updated"], |
| order: ["New order received", "Order status updated", "Order ready for pickup"], |
| cart: ["Item added to cart", "Cart saved for later", "Price drop in cart"], |
| wishlist: ["Item back in stock", "Price drop alert", "Similar item suggestion"], |
| review: ["Review published", "Review helpful vote", "Review response received"], |
| shipping: ["Package out for delivery", "Delivery attempted", "Package delivered"], |
| promotion: ["Flash sale started", "Exclusive member offer", "Cashback available"], |
| } |
|
|
| const titles = { |
| success: ["Success!", "Completed", "Done"], |
| warning: ["Attention", "Notice", "Warning"], |
| error: ["Error", "Failed", "Problem"], |
| info: ["Information", "Update", "News"], |
| order: ["Order Update", "New Order", "Order Status"], |
| cart: ["Cart Update", "Shopping Cart", "Cart Alert"], |
| wishlist: ["Wishlist Alert", "Wishlist Update", "Saved Item"], |
| review: ["Review Update", "Customer Review", "Feedback"], |
| shipping: ["Shipping Update", "Delivery Status", "Package Info"], |
| promotion: ["Special Offer", "Promotion", "Deal Alert"], |
| } |
|
|
| return { |
| id: Date.now(), |
| type: randomType, |
| title: titles[randomType][Math.floor(Math.random() * titles[randomType].length)], |
| message: messages[randomType][Math.floor(Math.random() * messages[randomType].length)], |
| timestamp: new Date(), |
| read: false, |
| priority: ["low", "medium", "high"][Math.floor(Math.random() * 3)], |
| } |
| } |
|
|
| const formatTimestamp = (timestamp) => { |
| const now = new Date() |
| const diff = now - timestamp |
| const minutes = Math.floor(diff / 60000) |
| const hours = Math.floor(diff / 3600000) |
| const days = Math.floor(diff / 86400000) |
|
|
| if (minutes < 1) return "Just now" |
| if (minutes < 60) return `${minutes}m ago` |
| if (hours < 24) return `${hours}h ago` |
| if (days < 7) return `${days}d ago` |
| return timestamp.toLocaleDateString() |
| } |
|
|
| const markAsRead = (id) => { |
| markNotificationAsRead(id) |
| setUnreadCount(getUnreadCountService()) |
|
|
| } |
|
|
| const markAllAsRead = () => { |
| markAllReadService() |
| setUnreadCount(0) |
| } |
|
|
| const deleteNotification = (id) => { |
| deleteNotificationService(id) |
| setUnreadCount(getUnreadCountService()) |
| toast.success("Notification removed!", { |
| position: "top-right", |
| autoClose: 2000, |
| }) |
| } |
|
|
| const clearAllNotifications = () => { |
| clearAllService() |
| setUnreadCount(0) |
| toast.success("All notifications cleared!", { |
| position: "top-right", |
| autoClose: 2000, |
| }) |
| } |
|
|
| const filteredNotifications = notifications.filter((notification) => { |
| if (filter === "all") return true |
| if (filter === "unread") return !notification.read |
| return notification.type === filter |
| }) |
|
|
| const toggleNotificationPanel = () => { |
| setIsOpen(!isOpen) |
| } |
|
|
| const handleNotificationAction = (notification) => { |
| markAsRead(notification.id) |
|
|
| switch (notification.type) { |
| case 'order': |
| navigate('/orders') |
| toast.success('Navigating to Orders page') |
| break |
| case 'cart': |
| navigate('/cart') |
| toast.success('Navigating to Cart page') |
| break |
| case 'wishlist': |
| navigate('/wishlist') |
| toast.success('Navigating to Wishlist page') |
| break |
| case 'review': |
| navigate('/productlist') |
| toast.success('Navigating to Products page') |
| break |
| case 'shipping': |
| navigate('/orders') |
| toast.success('Navigating to Orders page') |
| break |
| case 'promotion': |
| navigate('/productlist') |
| toast.success('Navigating to Products page') |
| break |
| case 'success': |
| navigate('/profile') |
| toast.success('Navigating to Profile page') |
| break |
| case 'warning': |
| navigate('/profile') |
| toast.success('Navigating to Profile page') |
| break |
| case 'error': |
| navigate('/cart') |
| toast.success('Navigating to Cart page') |
| break |
| case 'info': |
| navigate('/productlist') |
| toast.success('Navigating to Products page') |
| break |
| default: |
| navigate('/productlist') |
| toast.success('Navigating to Products page') |
| break |
| } |
|
|
| setIsOpen(false) |
| } |
|
|
| return ( |
| <div className="notification-center" ref={notificationRef}> |
| {} |
| <motion.button |
| className="notification-bell" |
| onClick={toggleNotificationPanel} |
| whileHover={{ scale: 1.1 }} |
| whileTap={{ scale: 0.9 }} |
| animate={{ |
| rotate: unreadCount > 0 ? [0, 15, -15, 0] : 0, |
| }} |
| transition={{ |
| duration: 0.5, |
| repeat: unreadCount > 0 ? Number.POSITIVE_INFINITY : 0, |
| repeatDelay: 3, |
| }} |
| > |
| <Bell size={20} /> |
| |
| {} |
| {unreadCount > 0 && ( |
| <motion.div |
| className="notification-badge" |
| initial={{ scale: 0, rotate: 180 }} |
| animate={{ scale: 1, rotate: 0 }} |
| transition={{ type: "spring", stiffness: 300 }} |
| > |
| {unreadCount > 99 ? "99+" : unreadCount} |
| </motion.div> |
| )} |
| |
| {} |
| {unreadCount > 0 && ( |
| <motion.div |
| className="notification-pulse" |
| initial={{ scale: 1, opacity: 0.8 }} |
| animate={{ scale: 1.5, opacity: 0 }} |
| transition={{ |
| duration: 2, |
| repeat: Number.POSITIVE_INFINITY, |
| ease: "easeOut", |
| }} |
| /> |
| )} |
| </motion.button> |
| |
| {} |
| <AnimatePresence> |
| {isOpen && ( |
| <motion.div |
| className="notification-panel" |
| initial={{ opacity: 0, scale: 0.9, y: -20 }} |
| animate={{ opacity: 1, scale: 1, y: 0 }} |
| exit={{ opacity: 0, scale: 0.9, y: -20 }} |
| transition={{ duration: 0.3, type: "spring", stiffness: 200 }} |
| > |
| {} |
| <div className="panel-header"> |
| <div className="header-content"> |
| <div className="header-title"> |
| <Crown size={20} /> |
| <h3>Notifications</h3> |
| {unreadCount > 0 && <span className="unread-indicator">({unreadCount} new)</span>} |
| <span className="notification-limit">({notifications.length}/6)</span> |
| </div> |
| |
| <div className="header-actions"> |
| <motion.button |
| className="header-action-btn" |
| onClick={() => setShowSettings(!showSettings)} |
| whileHover={{ scale: 1.1 }} |
| whileTap={{ scale: 0.9 }} |
| > |
| <Settings size={16} /> |
| </motion.button> |
| |
| <motion.button |
| className="header-action-btn" |
| onClick={markAllAsRead} |
| disabled={unreadCount === 0} |
| whileHover={{ scale: 1.1 }} |
| whileTap={{ scale: 0.9 }} |
| > |
| <Check size={16} /> |
| </motion.button> |
| |
| <motion.button |
| className="header-action-btn close" |
| onClick={() => setIsOpen(false)} |
| whileHover={{ scale: 1.1, rotate: 90 }} |
| whileTap={{ scale: 0.9 }} |
| > |
| <X size={16} /> |
| </motion.button> |
| </div> |
| </div> |
| |
| {} |
| <AnimatePresence> |
| {showSettings && ( |
| <motion.div |
| className="settings-panel" |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: "auto" }} |
| exit={{ opacity: 0, height: 0 }} |
| transition={{ duration: 0.3 }} |
| > |
| <div className="settings-content"> |
| <motion.button className="settings-action" onClick={clearAllNotifications} whileHover={{ x: 5 }}> |
| <Trash2 size={14} /> |
| <span>Clear All</span> |
| </motion.button> |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| |
| {} |
| <div className="filter-tabs"> |
| {[ |
| { key: "all", label: "All", icon: Bell }, |
| { key: "unread", label: "Unread", icon: EyeOff }, |
| { key: "order", label: "Orders", icon: Package }, |
| { key: "promotion", label: "Offers", icon: Gift }, |
| ].map((tab) => ( |
| <motion.button |
| key={tab.key} |
| className={`filter-tab ${filter === tab.key ? "active" : ""}`} |
| onClick={() => setFilter(tab.key)} |
| whileHover={{ scale: 1.05 }} |
| whileTap={{ scale: 0.95 }} |
| > |
| <tab.icon size={14} /> |
| <span>{tab.label}</span> |
| </motion.button> |
| ))} |
| </div> |
| </div> |
| |
| {} |
| <div className="notifications-list"> |
| <AnimatePresence mode="popLayout"> |
| {filteredNotifications.length === 0 ? ( |
| <motion.div |
| className="empty-state" |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -20 }} |
| > |
| <Bell size={48} /> |
| <h4>No notifications</h4> |
| <p>You're all caught up!</p> |
| </motion.div> |
| ) : ( |
| filteredNotifications.map((notification, index) => { |
| const NotificationIcon = notificationTypes[notification.type]?.icon || Info |
| const iconColor = notificationTypes[notification.type]?.color || "#667eea" |
| const bgColor = notificationTypes[notification.type]?.bgColor || "rgba(102, 126, 234, 0.1)" |
| |
| return ( |
| <motion.div |
| key={notification.id} |
| className={`notification-item ${notification.read ? "read" : "unread"} priority-${ |
| notification.priority |
| }`} |
| initial={{ opacity: 0, x: -20, scale: 0.9 }} |
| animate={{ opacity: 1, x: 0, scale: 1 }} |
| exit={{ opacity: 0, x: 20, scale: 0.9 }} |
| transition={{ |
| duration: 0.3, |
| delay: index * 0.05, |
| type: "spring", |
| stiffness: 200, |
| }} |
| whileHover={{ x: 5, scale: 1.02 }} |
| layout |
| > |
| {} |
| <div className={`priority-indicator priority-${notification.priority}`} /> |
| |
| {} |
| <div className="notification-icon" style={{ backgroundColor: bgColor }}> |
| <NotificationIcon size={18} style={{ color: iconColor }} /> |
| {!notification.read && <div className="unread-dot" />} |
| </div> |
| |
| {} |
| <div className="notification-content"> |
| <div className="notification-header"> |
| <h4 className="notification-title">{notification.title}</h4> |
| <div className="notification-actions"> |
| <span className="notification-time">{formatTimestamp(notification.timestamp)}</span> |
| <motion.button |
| className="action-btn" |
| onClick={() => deleteNotification(notification.id)} |
| whileHover={{ scale: 1.2, rotate: 90 }} |
| whileTap={{ scale: 0.8 }} |
| > |
| <X size={12} /> |
| </motion.button> |
| </div> |
| </div> |
| |
| <p className="notification-message">{notification.message}</p> |
| |
| {} |
| <motion.button |
| className="notification-action-btn" |
| onClick={() => handleNotificationAction(notification)} |
| whileHover={{ scale: 1.05 }} |
| whileTap={{ scale: 0.95 }} |
| > |
| <Zap size={14} /> |
| <span>View</span> |
| </motion.button> |
| </div> |
| |
| {} |
| <motion.button |
| className="read-toggle" |
| onClick={() => markAsRead(notification.id)} |
| whileHover={{ scale: 1.2 }} |
| whileTap={{ scale: 0.8 }} |
| > |
| {notification.read ? <Eye size={14} /> : <EyeOff size={14} />} |
| </motion.button> |
| |
| {} |
| <div className="notification-glow" /> |
| </motion.div> |
| ) |
| }) |
| )} |
| </AnimatePresence> |
| </div> |
| |
| {} |
| {filteredNotifications.length > 0 && ( |
| <div className="panel-footer"> |
| <motion.button |
| className="view-all-btn" |
| onClick={() => { |
| navigate('/productlist') |
| setIsOpen(false) |
| }} |
| whileHover={{ scale: 1.02, y: -2 }} |
| whileTap={{ scale: 0.98 }} |
| > |
| <span>View All Notifications</span> |
| <div className="btn-glow" /> |
| </motion.button> |
| </div> |
| )} |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </div> |
| ) |
| } |
|
|
| export default NotificationCenter |