feat: Add notification bell UI component to header
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Implements Phase 1 of notification bell feature with:
- NotificationBell component with badge counter (99+ max)
- useNotifications hook with 30s polling and caching
- Notifications API client for backend integration
- Integration into AppShell header next to user avatar
- Responsive dropdown (400px desktop, full-width mobile)
- Empty state, loading, and error handling
- Optimistic UI updates for mark as read
- Animated badge with pulse effect
- Icon mapping for notification types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrei
2025-10-09 13:24:56 +00:00
parent 2dbfb79e72
commit 090fe7f63b
4 changed files with 585 additions and 51 deletions

View File

@@ -24,6 +24,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import Link from 'next/link';
import { NotificationBell } from '@/components/notifications/NotificationBell';
interface AppShellProps {
children: ReactNode;
@@ -149,59 +150,62 @@ export const AppShell = ({ children }: AppShellProps) => {
</Box>
</Box>
{/* Right Side - User Menu Button with Status Indicator */}
<Box sx={{ width: 80, display: 'flex', justifyContent: 'flex-end' }}>
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<IconButton
onClick={handleMenuOpen}
size="medium"
aria-label="user menu"
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
sx={{
minWidth: 44,
minHeight: 44,
position: 'relative',
}}
>
<Avatar
src={user?.photoUrl || undefined}
alt={user?.name ? `${user.name}'s profile photo` : 'User profile photo'}
key={user?.photoUrl || 'no-photo'} // Force re-render when photoUrl changes
{/* Right Side - Notifications & User Menu */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<NotificationBell />
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<IconButton
onClick={handleMenuOpen}
size="medium"
aria-label="user menu"
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
sx={{
width: 36,
height: 36,
bgcolor: 'primary.main',
fontSize: '0.875rem',
}}
imgProps={{
onError: (e: any) => {
console.error('Avatar image failed to load:', user?.photoUrl?.substring(0, 50));
e.target.style.display = 'none'; // Hide broken image, show fallback
}
minWidth: 44,
minHeight: 44,
position: 'relative',
}}
>
{user?.name?.charAt(0).toUpperCase() || 'U'}
</Avatar>
{/* Status Dot Indicator */}
<Box
sx={{
position: 'absolute',
bottom: 2,
right: 2,
width: 12,
height: 12,
borderRadius: '50%',
bgcolor: isConnected ? '#4caf50' : '#9e9e9e',
border: '2px solid',
borderColor: 'background.paper',
boxShadow: 1,
}}
aria-label={isConnected ? 'Online' : 'Offline'}
/>
</IconButton>
</Tooltip>
<Avatar
src={user?.photoUrl || undefined}
alt={user?.name ? `${user.name}'s profile photo` : 'User profile photo'}
key={user?.photoUrl || 'no-photo'} // Force re-render when photoUrl changes
sx={{
width: 36,
height: 36,
bgcolor: 'primary.main',
fontSize: '0.875rem',
}}
imgProps={{
onError: (e: any) => {
console.error('Avatar image failed to load:', user?.photoUrl?.substring(0, 50));
e.target.style.display = 'none'; // Hide broken image, show fallback
}
}}
>
{user?.name?.charAt(0).toUpperCase() || 'U'}
</Avatar>
{/* Status Dot Indicator */}
<Box
sx={{
position: 'absolute',
bottom: 2,
right: 2,
width: 12,
height: 12,
borderRadius: '50%',
bgcolor: isConnected ? '#4caf50' : '#9e9e9e',
border: '2px solid',
borderColor: 'background.paper',
boxShadow: 1,
}}
aria-label={isConnected ? 'Online' : 'Offline'}
/>
</IconButton>
</Tooltip>
</Box>
<Menu
id="user-menu"
@@ -248,7 +252,6 @@ export const AppShell = ({ children }: AppShellProps) => {
<ListItemText>{t('navigation.logout')}</ListItemText>
</MenuItem>
</Menu>
</Box>
</Box>
<Container