From 142920b264d6edaaf5039450a8df1e7d7753719b Mon Sep 17 00:00:00 2001 From: Oliver Sylvester-Bradley Date: Fri, 18 Jul 2025 17:01:35 +0100 Subject: [PATCH] Implement comprehensive notification system for vouches and praise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add NotificationDropdown component with filtering (all/pending/unread) - Create NotificationItem component with accept/reject/assign actions - Implement notification service with mock data for vouches and praise - Add data types for Notification, Vouch, Praise, and RCard categories - Include default rCard categories (Business, Colleague, Family, Friend, etc.) - Add rCard assignment dialog for organizing accepted vouches/praise - Replace static notification badge with dynamic notification system - Support real-time notification management with state updates Features: - Accept/reject vouches and praise from other users - Assign accepted items to specific rCard categories - Mark notifications as read/unread - Filter by status (pending, unread, all) - Real-time badge count updates - Professional notification UI with avatars and status chips 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/components/layout/DashboardLayout.tsx | 154 +++++++- .../notifications/NotificationDropdown.tsx | 247 ++++++++++++ .../notifications/NotificationItem.tsx | 369 ++++++++++++++++++ src/services/notificationService.ts | 269 +++++++++++++ src/types/notification.ts | 146 +++++++ 5 files changed, 1178 insertions(+), 7 deletions(-) create mode 100644 src/components/notifications/NotificationDropdown.tsx create mode 100644 src/components/notifications/NotificationItem.tsx create mode 100644 src/services/notificationService.ts create mode 100644 src/types/notification.ts diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index b0c61f0..ee28e39 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import type { ReactNode } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { @@ -26,7 +26,6 @@ import { Menu as MenuIcon, Settings, Logout, - NotificationsNone, SearchRounded, Groups, RssFeed, @@ -36,6 +35,9 @@ import { Hub, } from '@mui/icons-material'; import BottomNavigation from '../navigation/BottomNavigation'; +import NotificationDropdown from '../notifications/NotificationDropdown'; +import { notificationService } from '../../services/notificationService'; +import type { Notification, NotificationSummary } from '../../types/notification'; const drawerWidth = 280; @@ -57,6 +59,13 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { const [mobileOpen, setMobileOpen] = useState(false); const [profileMenuAnchor, setProfileMenuAnchor] = useState(null); const [expandedItems, setExpandedItems] = useState>(new Set(['Network'])); + const [notifications, setNotifications] = useState([]); + const [notificationSummary, setNotificationSummary] = useState({ + total: 0, + unread: 0, + pending: 0, + byType: { vouch: 0, praise: 0, connection: 0, group_invite: 0, message: 0, system: 0 } + }); const location = useLocation(); const navigate = useNavigate(); @@ -67,6 +76,131 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { { text: 'Chat', icon: , path: '/messages' }, ]; + // Load notifications + useEffect(() => { + const loadNotifications = async () => { + try { + const [notificationData, summaryData] = await Promise.all([ + notificationService.getNotifications('current-user'), + notificationService.getNotificationSummary('current-user') + ]); + setNotifications(notificationData); + setNotificationSummary(summaryData); + } catch (error) { + console.error('Failed to load notifications:', error); + } + }; + + loadNotifications(); + }, []); + + // Notification handlers + const handleMarkAsRead = async (notificationId: string) => { + try { + await notificationService.markAsRead(notificationId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + unread: Math.max(0, prev.unread - 1) + })); + } catch (error) { + console.error('Failed to mark notification as read:', error); + } + }; + + const handleMarkAllAsRead = async () => { + try { + await notificationService.markAllAsRead('current-user'); + setNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); + setNotificationSummary(prev => ({ ...prev, unread: 0 })); + } catch (error) { + console.error('Failed to mark all notifications as read:', error); + } + }; + + const handleAcceptVouch = async (notificationId: string, vouchId: string) => { + try { + await notificationService.acceptVouch(notificationId, vouchId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, status: 'accepted', isActionable: true } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + pending: Math.max(0, prev.pending - 1) + })); + } catch (error) { + console.error('Failed to accept vouch:', error); + } + }; + + const handleRejectVouch = async (notificationId: string, vouchId: string) => { + try { + await notificationService.rejectVouch(notificationId, vouchId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, status: 'rejected', isActionable: false } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + pending: Math.max(0, prev.pending - 1) + })); + } catch (error) { + console.error('Failed to reject vouch:', error); + } + }; + + const handleAcceptPraise = async (notificationId: string, praiseId: string) => { + try { + await notificationService.acceptPraise(notificationId, praiseId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, status: 'accepted', isActionable: true } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + pending: Math.max(0, prev.pending - 1) + })); + } catch (error) { + console.error('Failed to accept praise:', error); + } + }; + + const handleRejectPraise = async (notificationId: string, praiseId: string) => { + try { + await notificationService.rejectPraise(notificationId, praiseId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, status: 'rejected', isActionable: false } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + pending: Math.max(0, prev.pending - 1) + })); + } catch (error) { + console.error('Failed to reject praise:', error); + } + }; + + const handleAssignToRCard = async (notificationId: string, rCardId: string) => { + try { + await notificationService.assignToRCard(notificationId, rCardId); + setNotifications(prev => + prev.map(n => n.id === notificationId ? { + ...n, + status: 'completed', + isActionable: false, + isRead: true, + metadata: { ...n.metadata, rCardId } + } : n) + ); + setNotificationSummary(prev => ({ + ...prev, + unread: Math.max(0, prev.unread - 1) + })); + } catch (error) { + console.error('Failed to assign to rCard:', error); + } + }; + const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); }; @@ -264,11 +398,17 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { - - - - - + void; + onMarkAllAsRead: () => void; + onAcceptVouch: (notificationId: string, vouchId: string) => void; + onRejectVouch: (notificationId: string, vouchId: string) => void; + onAcceptPraise: (notificationId: string, praiseId: string) => void; + onRejectPraise: (notificationId: string, praiseId: string) => void; + onAssignToRCard: (notificationId: string, rCardId: string) => void; +} + +const NotificationDropdown = ({ + notifications, + summary, + onMarkAsRead, + onMarkAllAsRead, + onAcceptVouch, + onRejectVouch, + onAcceptPraise, + onRejectPraise, + onAssignToRCard, +}: NotificationDropdownProps) => { + const [anchorEl, setAnchorEl] = useState(null); + const [filter, setFilter] = useState<'all' | 'pending' | 'unread'>('all'); + + const isOpen = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const filteredNotifications = notifications.filter(notification => { + switch (filter) { + case 'pending': + return notification.status === 'pending' && notification.isActionable; + case 'unread': + return !notification.isRead; + default: + return true; + } + }); + + const getFilterChipColor = (filterType: string) => { + return filter === filterType ? 'primary' : 'default'; + }; + + + return ( + <> + + + {summary.unread > 0 ? : } + + + + e.stopPropagation()} + PaperProps={{ + elevation: 8, + sx: { + width: 400, + maxWidth: '90vw', + maxHeight: '80vh', + mt: 1.5, + borderRadius: 2, + border: 1, + borderColor: 'divider', + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + top: 0, + right: 20, + width: 10, + height: 10, + bgcolor: 'background.paper', + transform: 'translateY(-50%) rotate(45deg)', + zIndex: 0, + border: 1, + borderColor: 'divider', + borderBottom: 0, + borderRight: 0, + }, + }, + }} + transformOrigin={{ horizontal: 'right', vertical: 'top' }} + anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} + > + {/* Header */} + + + + Notifications + + {summary.unread > 0 && ( + + )} + + + {/* Summary Stats */} + + + {summary.unread > 0 && ( + + )} + {summary.pending > 0 && ( + + )} + + + {/* Filter Chips */} + + setFilter('all')} + color={getFilterChipColor('all')} + variant={filter === 'all' ? 'filled' : 'outlined'} + sx={{ fontSize: '0.75rem', cursor: 'pointer' }} + /> + setFilter('pending')} + color={getFilterChipColor('pending')} + variant={filter === 'pending' ? 'filled' : 'outlined'} + sx={{ fontSize: '0.75rem', cursor: 'pointer' }} + /> + setFilter('unread')} + color={getFilterChipColor('unread')} + variant={filter === 'unread' ? 'filled' : 'outlined'} + sx={{ fontSize: '0.75rem', cursor: 'pointer' }} + /> + + + + {/* Notification List */} + + {filteredNotifications.length === 0 ? ( + + + {filter === 'all' + ? 'No notifications yet' + : filter === 'pending' + ? 'No pending notifications' + : 'No unread notifications' + } + + + ) : ( + + {filteredNotifications.map((notification, index) => ( + + + {index < filteredNotifications.length - 1 && } + + ))} + + )} + + + {/* Footer */} + {summary.total > 0 && ( + + + + )} + + + ); +}; + +export default NotificationDropdown; \ No newline at end of file diff --git a/src/components/notifications/NotificationItem.tsx b/src/components/notifications/NotificationItem.tsx new file mode 100644 index 0000000..5c321b6 --- /dev/null +++ b/src/components/notifications/NotificationItem.tsx @@ -0,0 +1,369 @@ +import { useState } from 'react'; +import { + ListItem, + Box, + Avatar, + Typography, + Button, + Chip, + IconButton, + Menu, + MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + FormControl, + InputLabel, + Select, + useTheme, + alpha, +} from '@mui/material'; +import { + ThumbUp, + StarBorder, + CheckCircle, + Cancel, + Assignment, + MoreVert, + Business, + PersonOutline, + Groups, + FamilyRestroom, + Favorite, + Home, +} from '@mui/icons-material'; +import type { Notification } from '../../types/notification'; +import { DEFAULT_RCARDS } from '../../types/notification'; + +interface NotificationItemProps { + notification: Notification; + onMarkAsRead: (notificationId: string) => void; + onAcceptVouch: (notificationId: string, vouchId: string) => void; + onRejectVouch: (notificationId: string, vouchId: string) => void; + onAcceptPraise: (notificationId: string, praiseId: string) => void; + onRejectPraise: (notificationId: string, praiseId: string) => void; + onAssignToRCard: (notificationId: string, rCardId: string) => void; +} + +const NotificationItem = ({ + notification, + onMarkAsRead, + onAcceptVouch, + onRejectVouch, + onAcceptPraise, + onRejectPraise, + onAssignToRCard, +}: NotificationItemProps) => { + const theme = useTheme(); + const [menuAnchor, setMenuAnchor] = useState(null); + const [showAssignDialog, setShowAssignDialog] = useState(false); + const [selectedRCard, setSelectedRCard] = useState(''); + + const isMenuOpen = Boolean(menuAnchor); + + const handleMenuClick = (event: React.MouseEvent) => { + event.stopPropagation(); + setMenuAnchor(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchor(null); + }; + + const handleAccept = () => { + if (notification.type === 'vouch' && notification.metadata?.vouchId) { + onAcceptVouch(notification.id, notification.metadata.vouchId); + } else if (notification.type === 'praise' && notification.metadata?.praiseId) { + onAcceptPraise(notification.id, notification.metadata.praiseId); + } + handleMenuClose(); + }; + + const handleReject = () => { + if (notification.type === 'vouch' && notification.metadata?.vouchId) { + onRejectVouch(notification.id, notification.metadata.vouchId); + } else if (notification.type === 'praise' && notification.metadata?.praiseId) { + onRejectPraise(notification.id, notification.metadata.praiseId); + } + handleMenuClose(); + }; + + const handleAssignClick = () => { + setShowAssignDialog(true); + handleMenuClose(); + }; + + const handleAssignSubmit = () => { + if (selectedRCard) { + onAssignToRCard(notification.id, selectedRCard); + setShowAssignDialog(false); + setSelectedRCard(''); + } + }; + + const handleMarkAsRead = () => { + onMarkAsRead(notification.id); + handleMenuClose(); + }; + + const getNotificationIcon = () => { + switch (notification.type) { + case 'vouch': + return ; + case 'praise': + return ; + default: + return null; + } + }; + + const getStatusChip = () => { + switch (notification.status) { + case 'pending': + return ; + case 'accepted': + return ; + case 'rejected': + return ; + case 'completed': + return ; + default: + return null; + } + }; + + const getRCardIcon = (iconName: string) => { + switch (iconName) { + case 'Business': + return ; + case 'PersonOutline': + return ; + case 'Groups': + return ; + case 'FamilyRestroom': + return ; + case 'Favorite': + return ; + case 'Home': + return ; + default: + return ; + } + }; + + const formatTimeAgo = (date: Date) => { + const now = new Date(); + const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60)); + + if (diffInMinutes < 1) return 'Just now'; + if (diffInMinutes < 60) return `${diffInMinutes}m ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours}h ago`; + + const diffInDays = Math.floor(diffInHours / 24); + if (diffInDays < 7) return `${diffInDays}d ago`; + + return date.toLocaleDateString(); + }; + + return ( + <> + + + {/* Avatar and Icon */} + + + {notification.fromUserName?.charAt(0) || 'N'} + + {getNotificationIcon() && ( + + {getNotificationIcon()} + + )} + + + {/* Content */} + + + + {notification.title} + + + {getStatusChip()} + + + + + + + + {notification.message} + + + + + {formatTimeAgo(notification.createdAt)} + + + {/* Action Buttons */} + {notification.isActionable && notification.status === 'pending' && ( + + + + + )} + + {notification.status === 'accepted' && !notification.metadata?.rCardId && ( + + )} + + + + + + {/* Menu */} + + {!notification.isRead && ( + + Mark as read + + )} + {notification.status === 'accepted' && !notification.metadata?.rCardId && ( + + Assign to rCard + + )} + {notification.isActionable && notification.status === 'pending' && ( + <> + Accept + Decline + + )} + + + {/* Assign to rCard Dialog */} + setShowAssignDialog(false)} + maxWidth="sm" + fullWidth + > + + Assign to rCard + + + + Choose which rCard category to assign this {notification.type} to. This helps organize your connections and endorsements. + + + + Select rCard + + + + + + + + + + ); +}; + +export default NotificationItem; \ No newline at end of file diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts new file mode 100644 index 0000000..9a33a80 --- /dev/null +++ b/src/services/notificationService.ts @@ -0,0 +1,269 @@ +import type { + Notification, + NotificationSummary, + Vouch, + Praise +} from '../types/notification'; + +// Mock data for development +const mockNotifications: Notification[] = [ + { + id: '1', + type: 'vouch', + title: 'New Skill Vouch', + message: 'Sarah Johnson vouched for your React Development skills at Advanced level', + fromUserId: 'user-sarah', + fromUserName: 'Sarah Johnson', + fromUserAvatar: undefined, + targetUserId: 'current-user', + isRead: false, + isActionable: true, + status: 'pending', + metadata: { + vouchId: 'vouch-1', + }, + createdAt: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago + updatedAt: new Date(Date.now() - 1000 * 60 * 30), + }, + { + id: '2', + type: 'praise', + title: 'New Praise', + message: 'Mike Chen praised your leadership during the Q3 project launch', + fromUserId: 'user-mike', + fromUserName: 'Mike Chen', + fromUserAvatar: undefined, + targetUserId: 'current-user', + isRead: false, + isActionable: true, + status: 'pending', + metadata: { + praiseId: 'praise-1', + }, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2), + }, + { + id: '3', + type: 'vouch', + title: 'Skill Vouch Accepted', + message: 'Your TypeScript vouch from Alex Rodriguez has been assigned to Business contacts', + fromUserId: 'user-alex', + fromUserName: 'Alex Rodriguez', + fromUserAvatar: undefined, + targetUserId: 'current-user', + isRead: true, + isActionable: false, + status: 'completed', + metadata: { + vouchId: 'vouch-2', + rCardId: 'rcard-business', + }, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 12), // Updated 12 hours ago + }, + { + id: '4', + type: 'praise', + title: 'Teamwork Praise', + message: 'Jessica Liu praised your collaborative approach on the mobile app redesign', + fromUserId: 'user-jessica', + fromUserName: 'Jessica Liu', + fromUserAvatar: undefined, + targetUserId: 'current-user', + isRead: false, + isActionable: true, + status: 'accepted', + metadata: { + praiseId: 'praise-2', + }, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 6), // 6 hours ago + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 4), // Updated 4 hours ago + }, +]; + +const mockVouches: Vouch[] = [ + { + id: 'vouch-1', + fromUserId: 'user-sarah', + fromUserName: 'Sarah Johnson', + fromUserAvatar: undefined, + toUserId: 'current-user', + skill: 'React Development', + description: 'Excellent component architecture and state management. Always writes clean, maintainable code.', + level: 'advanced', + endorsementText: 'I worked with this person on multiple React projects and they consistently delivered high-quality solutions.', + createdAt: new Date(Date.now() - 1000 * 60 * 30), + updatedAt: new Date(Date.now() - 1000 * 60 * 30), + }, + { + id: 'vouch-2', + fromUserId: 'user-alex', + fromUserName: 'Alex Rodriguez', + fromUserAvatar: undefined, + toUserId: 'current-user', + skill: 'TypeScript', + description: 'Strong type safety practices and excellent knowledge of advanced TypeScript features.', + level: 'expert', + endorsementText: 'One of the best TypeScript developers I have worked with.', + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24), + }, +]; + +const mockPraises: Praise[] = [ + { + id: 'praise-1', + fromUserId: 'user-mike', + fromUserName: 'Mike Chen', + fromUserAvatar: undefined, + toUserId: 'current-user', + category: 'leadership', + title: 'Outstanding Project Leadership', + description: 'Led the Q3 project launch with exceptional coordination and communication. Kept the team motivated and on track.', + tags: ['project-management', 'team-leadership', 'communication'], + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2), + }, + { + id: 'praise-2', + fromUserId: 'user-jessica', + fromUserName: 'Jessica Liu', + fromUserAvatar: undefined, + toUserId: 'current-user', + category: 'teamwork', + title: 'Collaborative Team Player', + description: 'Always willing to help teammates and shares knowledge freely. Made the mobile app redesign a huge success.', + tags: ['collaboration', 'mobile-development', 'mentoring'], + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 6), + updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 6), + }, +]; + +export class NotificationService { + private notifications: Notification[] = [...mockNotifications]; + private vouches: Vouch[] = [...mockVouches]; + private praises: Praise[] = [...mockPraises]; + + // Get all notifications for a user + async getNotifications(userId: string): Promise { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 300)); + return this.notifications.filter(n => n.targetUserId === userId); + } + + // Get notification summary + async getNotificationSummary(userId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + const userNotifications = this.notifications.filter(n => n.targetUserId === userId); + + const summary: NotificationSummary = { + total: userNotifications.length, + unread: userNotifications.filter(n => !n.isRead).length, + pending: userNotifications.filter(n => n.status === 'pending' && n.isActionable).length, + byType: { + vouch: userNotifications.filter(n => n.type === 'vouch').length, + praise: userNotifications.filter(n => n.type === 'praise').length, + connection: userNotifications.filter(n => n.type === 'connection').length, + group_invite: userNotifications.filter(n => n.type === 'group_invite').length, + message: userNotifications.filter(n => n.type === 'message').length, + system: userNotifications.filter(n => n.type === 'system').length, + }, + }; + + return summary; + } + + // Mark notification as read + async markAsRead(notificationId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 200)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification) { + notification.isRead = true; + notification.updatedAt = new Date(); + } + } + + // Mark all notifications as read for a user + async markAllAsRead(userId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 300)); + this.notifications + .filter(n => n.targetUserId === userId && !n.isRead) + .forEach(notification => { + notification.isRead = true; + notification.updatedAt = new Date(); + }); + } + + // Accept a vouch + async acceptVouch(notificationId: string, _vouchId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 400)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification) { + notification.status = 'accepted'; + notification.isActionable = true; // Still actionable for rCard assignment + notification.updatedAt = new Date(); + } + } + + // Reject a vouch + async rejectVouch(notificationId: string, _vouchId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 400)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification) { + notification.status = 'rejected'; + notification.isActionable = false; + notification.updatedAt = new Date(); + } + } + + // Accept praise + async acceptPraise(notificationId: string, _praiseId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 400)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification) { + notification.status = 'accepted'; + notification.isActionable = true; // Still actionable for rCard assignment + notification.updatedAt = new Date(); + } + } + + // Reject praise + async rejectPraise(notificationId: string, _praiseId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 400)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification) { + notification.status = 'rejected'; + notification.isActionable = false; + notification.updatedAt = new Date(); + } + } + + // Assign to rCard + async assignToRCard(notificationId: string, rCardId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 300)); + const notification = this.notifications.find(n => n.id === notificationId); + if (notification && notification.metadata) { + notification.metadata.rCardId = rCardId; + notification.status = 'completed'; + notification.isActionable = false; + notification.isRead = true; + notification.updatedAt = new Date(); + } + } + + // Get vouch details + async getVouch(vouchId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 200)); + return this.vouches.find(v => v.id === vouchId) || null; + } + + // Get praise details + async getPraise(praiseId: string): Promise { + await new Promise(resolve => setTimeout(resolve, 200)); + return this.praises.find(p => p.id === praiseId) || null; + } +} + +// Export singleton instance +export const notificationService = new NotificationService(); \ No newline at end of file diff --git a/src/types/notification.ts b/src/types/notification.ts new file mode 100644 index 0000000..606f4d3 --- /dev/null +++ b/src/types/notification.ts @@ -0,0 +1,146 @@ +export interface RCard { + id: string; + name: string; + description?: string; + color?: string; + icon?: string; + isDefault: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface Vouch { + id: string; + fromUserId: string; + fromUserName: string; + fromUserAvatar?: string; + toUserId: string; + skill: string; + description: string; + level: 'beginner' | 'intermediate' | 'advanced' | 'expert'; + endorsementText?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface Praise { + id: string; + fromUserId: string; + fromUserName: string; + fromUserAvatar?: string; + toUserId: string; + category: 'professional' | 'personal' | 'leadership' | 'teamwork' | 'communication' | 'creativity' | 'other'; + title: string; + description: string; + tags?: string[]; + createdAt: Date; + updatedAt: Date; +} + +export interface NotificationAction { + id: string; + type: 'accept' | 'reject' | 'assign' | 'view'; + label: string; + variant?: 'text' | 'outlined' | 'contained'; + color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning'; +} + +export interface Notification { + id: string; + type: 'vouch' | 'praise' | 'connection' | 'group_invite' | 'message' | 'system'; + title: string; + message: string; + fromUserId?: string; + fromUserName?: string; + fromUserAvatar?: string; + targetUserId: string; + isRead: boolean; + isActionable: boolean; + status: 'pending' | 'accepted' | 'rejected' | 'completed'; + actions?: NotificationAction[]; + metadata?: { + vouchId?: string; + praiseId?: string; + groupId?: string; + messageId?: string; + rCardId?: string; + }; + createdAt: Date; + updatedAt: Date; +} + +export interface VouchNotification extends Notification { + type: 'vouch'; + metadata: { + vouchId: string; + rCardId?: string; + }; +} + +export interface PraiseNotification extends Notification { + type: 'praise'; + metadata: { + praiseId: string; + rCardId?: string; + }; +} + +export interface NotificationSummary { + total: number; + unread: number; + pending: number; + byType: { + vouch: number; + praise: number; + connection: number; + group_invite: number; + message: number; + system: number; + }; +} + +// Default rCard categories +export const DEFAULT_RCARDS: Omit[] = [ + { + name: 'Business', + description: 'Professional business contacts and partnerships', + color: '#2563eb', + icon: 'Business', + isDefault: true, + }, + { + name: 'Acquaintance', + description: 'People you know casually or have met briefly', + color: '#10b981', + icon: 'PersonOutline', + isDefault: true, + }, + { + name: 'Colleague', + description: 'Current and former work colleagues', + color: '#8b5cf6', + icon: 'Groups', + isDefault: true, + }, + { + name: 'Family', + description: 'Extended family members and relatives', + color: '#f59e0b', + icon: 'FamilyRestroom', + isDefault: true, + }, + { + name: 'Friend', + description: 'Personal friends and social connections', + color: '#ef4444', + icon: 'Favorite', + isDefault: true, + }, + { + name: 'Close Family', + description: 'Immediate family members', + color: '#ec4899', + icon: 'Home', + isDefault: true, + }, +]; \ No newline at end of file