- Rename navigation from Feed to Home with Dashboard icon - Convert notifications from popup to dedicated page - Restructure Account page tabs: Profile, My Cards, My Webpage, My Stream, My Bookmarks - Fix mobile layout with edge-to-edge design and proper padding - Remove scroll buttons from tabs to eliminate left margin gap - Update My Bookmarks filters to display side-by-side on mobile 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
parent
f3097d7e3c
commit
530938ea6f
@ -0,0 +1 @@ |
||||
VITE_ANTHROPIC_API_KEY=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 |
@ -0,0 +1,21 @@ |
||||
# Project Permissions |
||||
|
||||
- You have permission to read, write, and edit any files in this project |
||||
- No need to ask for approval before making code changes |
||||
- Focus on implementing solutions efficiently |
||||
|
||||
# Development Commands |
||||
|
||||
- `npm run dev` - Start development server |
||||
- `npm run build` - Build for production |
||||
- `npm run lint` - Run linting |
||||
- `npm run typecheck` - Run TypeScript checks |
||||
|
||||
# Project Notes |
||||
|
||||
This is a React/TypeScript NAO (Network of Authentic Others) community management application with features for: |
||||
- Contact management with NAO membership status |
||||
- Group management and invitations |
||||
- Vouch/praise system for reputation |
||||
- Network visualization |
||||
- Content feeds and messaging |
@ -1,27 +1,41 @@ |
||||
import { Box, Typography, Container } from '@mui/material'; |
||||
import PostCreateButton from '../components/PostCreateButton'; |
||||
import { Box, Typography } from '@mui/material'; |
||||
|
||||
const FeedPage = () => { |
||||
const handleCreatePost = (type: 'post' | 'offer' | 'want') => { |
||||
console.log(`Creating ${type} in main feed`); |
||||
// TODO: Implement post creation logic
|
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<Container maxWidth="lg"> |
||||
<Box sx={{ py: 4 }}> |
||||
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}> |
||||
Feed |
||||
<Box sx={{
|
||||
height: '100%', |
||||
width: '100%', |
||||
maxWidth: { xs: '100vw', md: '100%' }, |
||||
overflow: 'hidden', |
||||
boxSizing: 'border-box', |
||||
p: { xs: '10px', md: 0 }, |
||||
mx: { xs: 0, md: 'auto' } |
||||
}}> |
||||
<Box sx={{
|
||||
mb: { xs: 2, md: 3 }, |
||||
width: '100%', |
||||
overflow: 'hidden', |
||||
minWidth: 0 |
||||
}}> |
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 1, |
||||
fontSize: { xs: '1.5rem', md: '2.125rem' }, |
||||
overflow: 'hidden', |
||||
textOverflow: 'ellipsis', |
||||
whiteSpace: 'nowrap' |
||||
}} |
||||
> |
||||
Home |
||||
</Typography> |
||||
<Typography variant="body1" color="text.secondary"> |
||||
Your personalized network feed will appear here. |
||||
Your personalized dashboard, where you can place your own selection of widgets... |
||||
</Typography> |
||||
</Box> |
||||
</Container> |
||||
|
||||
<PostCreateButton onCreatePost={handleCreatePost} /> |
||||
</> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,415 @@ |
||||
import { useState, useEffect } from 'react'; |
||||
import { |
||||
Typography, |
||||
Box, |
||||
Card, |
||||
CardContent, |
||||
Button, |
||||
Chip, |
||||
Avatar, |
||||
IconButton, |
||||
Divider, |
||||
Badge, |
||||
alpha, |
||||
useTheme |
||||
} from '@mui/material'; |
||||
import { |
||||
Notifications, |
||||
VerifiedUser, |
||||
Favorite, |
||||
Group, |
||||
Message, |
||||
Settings, |
||||
CheckCircle, |
||||
Schedule, |
||||
Close, |
||||
MarkEmailRead |
||||
} from '@mui/icons-material'; |
||||
import { notificationService } from '../services/notificationService'; |
||||
import type { Notification, NotificationSummary } from '../types/notification'; |
||||
|
||||
const NotificationsPage = () => { |
||||
const theme = useTheme(); |
||||
const [notifications, setNotifications] = useState<Notification[]>([]); |
||||
const [notificationSummary, setNotificationSummary] = useState<NotificationSummary>({ |
||||
total: 0, |
||||
unread: 0, |
||||
pending: 0, |
||||
byType: { vouch: 0, praise: 0, connection: 0, group_invite: 0, message: 0, system: 0 } |
||||
}); |
||||
const [isLoading, setIsLoading] = useState(true); |
||||
|
||||
useEffect(() => { |
||||
const loadNotifications = async () => { |
||||
setIsLoading(true); |
||||
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); |
||||
} finally { |
||||
setIsLoading(false); |
||||
} |
||||
}; |
||||
|
||||
loadNotifications(); |
||||
}, []); |
||||
|
||||
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 getNotificationIcon = (type: string) => { |
||||
switch (type) { |
||||
case 'vouch': |
||||
return <VerifiedUser sx={{ fontSize: 20, color: 'primary.main' }} />; |
||||
case 'praise': |
||||
return <Favorite sx={{ fontSize: 20, color: '#d81b60' }} />; |
||||
case 'group_invite': |
||||
return <Group sx={{ fontSize: 20, color: 'success.main' }} />; |
||||
case 'message': |
||||
return <Message sx={{ fontSize: 20, color: 'info.main' }} />; |
||||
case 'system': |
||||
return <Settings sx={{ fontSize: 20, color: 'warning.main' }} />; |
||||
default: |
||||
return <Notifications sx={{ fontSize: 20 }} />; |
||||
} |
||||
}; |
||||
|
||||
const formatDate = (date: Date) => { |
||||
return new Intl.DateTimeFormat('en-US', { |
||||
year: 'numeric', |
||||
month: 'short', |
||||
day: 'numeric', |
||||
hour: '2-digit', |
||||
minute: '2-digit' |
||||
}).format(date); |
||||
}; |
||||
|
||||
return ( |
||||
<Box sx={{
|
||||
height: '100%', |
||||
width: '100%', |
||||
maxWidth: { xs: '100vw', md: '100%' }, |
||||
overflow: 'hidden', |
||||
boxSizing: 'border-box', |
||||
p: { xs: '10px', md: 0 }, |
||||
mx: { xs: 0, md: 'auto' } |
||||
}}> |
||||
{/* Header */} |
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: { xs: 2, md: 3 }, |
||||
width: '100%', |
||||
overflow: 'hidden', |
||||
minWidth: 0 |
||||
}}> |
||||
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}> |
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: 1, |
||||
fontSize: { xs: '1.5rem', md: '2.125rem' }, |
||||
overflow: 'hidden', |
||||
textOverflow: 'ellipsis', |
||||
whiteSpace: 'nowrap' |
||||
}} |
||||
> |
||||
Notifications |
||||
</Typography> |
||||
{notificationSummary.unread > 0 && ( |
||||
<Typography variant="body2" color="text.secondary"> |
||||
You have {notificationSummary.unread} unread notification{notificationSummary.unread !== 1 ? 's' : ''} |
||||
</Typography> |
||||
)} |
||||
</Box> |
||||
{notificationSummary.unread > 0 && ( |
||||
<Button |
||||
variant="outlined" |
||||
startIcon={<MarkEmailRead />} |
||||
onClick={handleMarkAllAsRead} |
||||
sx={{
|
||||
borderRadius: 2, |
||||
fontSize: { xs: '0.75rem', md: '0.875rem' }, |
||||
px: { xs: 1, md: 2 }, |
||||
py: { xs: 0.5, md: 1 } |
||||
}} |
||||
> |
||||
Mark All Read |
||||
</Button> |
||||
)} |
||||
</Box> |
||||
|
||||
{/* Notifications List */} |
||||
<Card sx={{
|
||||
width: { xs: 'calc(100% + 20px)', md: '100%' },
|
||||
maxWidth: { xs: 'calc(100vw - 0px)', md: '100%' },
|
||||
overflow: 'hidden', |
||||
mx: { xs: '-10px', md: 0 }, |
||||
borderRadius: { xs: 0, md: 1 }, |
||||
boxSizing: 'border-box' |
||||
}}> |
||||
<Box sx={{ p: { xs: '10px', md: 3 } }}> |
||||
{isLoading ? ( |
||||
<Box sx={{ textAlign: 'center', py: 8 }}> |
||||
<Typography variant="h6" color="text.secondary" gutterBottom> |
||||
Loading notifications... |
||||
</Typography> |
||||
</Box> |
||||
) : notifications.length === 0 ? ( |
||||
<Box sx={{ textAlign: 'center', py: 8 }}> |
||||
<Typography variant="h6" color="text.secondary" gutterBottom> |
||||
No notifications yet |
||||
</Typography> |
||||
<Typography variant="body2" color="text.secondary"> |
||||
You'll see notifications here when you receive vouches, praises, and other updates. |
||||
</Typography> |
||||
</Box> |
||||
) : ( |
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}> |
||||
{notifications.map((notification, index) => ( |
||||
<Box key={notification.id}> |
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
py: 2, |
||||
backgroundColor: notification.isRead ? 'transparent' : alpha(theme.palette.primary.main, 0.02), |
||||
borderRadius: 1, |
||||
position: 'relative' |
||||
}}> |
||||
{/* Notification Icon */} |
||||
<Box sx={{ flexShrink: 0, mt: 0.5 }}> |
||||
{getNotificationIcon(notification.type)} |
||||
</Box> |
||||
|
||||
{/* Main Content */} |
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}> |
||||
{/* Sender Info */} |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}> |
||||
<Avatar |
||||
src={notification.sender?.avatar} |
||||
alt={notification.sender?.name} |
||||
sx={{ width: 24, height: 24, fontSize: '0.75rem' }} |
||||
> |
||||
{notification.sender?.name?.charAt(0)} |
||||
</Avatar> |
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
||||
{notification.sender?.name} |
||||
</Typography> |
||||
<Typography variant="caption" color="text.secondary"> |
||||
{formatDate(notification.createdAt)} |
||||
</Typography> |
||||
{!notification.isRead && ( |
||||
<Box sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'primary.main', |
||||
ml: 'auto'
|
||||
}} /> |
||||
)} |
||||
</Box> |
||||
|
||||
{/* Message */} |
||||
<Typography variant="body2" sx={{ mb: 1, lineHeight: 1.5 }}> |
||||
{notification.message} |
||||
</Typography> |
||||
|
||||
{/* Status and Actions */} |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> |
||||
{notification.status && ( |
||||
<Chip |
||||
icon={notification.status === 'accepted' ? <CheckCircle /> : <Schedule />} |
||||
label={notification.status} |
||||
size="small" |
||||
variant="outlined" |
||||
sx={{ |
||||
fontSize: '0.75rem', |
||||
height: 20, |
||||
textTransform: 'capitalize', |
||||
...(notification.status === 'accepted' && { |
||||
backgroundColor: alpha(theme.palette.success.main, 0.08), |
||||
borderColor: alpha(theme.palette.success.main, 0.2), |
||||
color: 'success.main' |
||||
}) |
||||
}} |
||||
/> |
||||
)} |
||||
|
||||
{/* Action Buttons */} |
||||
{notification.isActionable && notification.status === 'pending' && ( |
||||
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}> |
||||
{notification.type === 'vouch' && ( |
||||
<> |
||||
<Button |
||||
size="small" |
||||
variant="outlined" |
||||
onClick={() => handleRejectVouch(notification.id, notification.metadata?.vouchId || '')} |
||||
sx={{
|
||||
minWidth: 60, |
||||
fontSize: '0.75rem', |
||||
py: 0.25 |
||||
}} |
||||
> |
||||
Reject |
||||
</Button> |
||||
<Button |
||||
size="small" |
||||
variant="contained" |
||||
onClick={() => handleAcceptVouch(notification.id, notification.metadata?.vouchId || '')} |
||||
sx={{
|
||||
minWidth: 60, |
||||
fontSize: '0.75rem', |
||||
py: 0.25 |
||||
}} |
||||
> |
||||
Accept |
||||
</Button> |
||||
</> |
||||
)} |
||||
{notification.type === 'praise' && ( |
||||
<> |
||||
<Button |
||||
size="small" |
||||
variant="outlined" |
||||
onClick={() => handleRejectPraise(notification.id, notification.metadata?.praiseId || '')} |
||||
sx={{
|
||||
minWidth: 60, |
||||
fontSize: '0.75rem', |
||||
py: 0.25 |
||||
}} |
||||
> |
||||
Reject |
||||
</Button> |
||||
<Button |
||||
size="small" |
||||
variant="contained" |
||||
onClick={() => handleAcceptPraise(notification.id, notification.metadata?.praiseId || '')} |
||||
sx={{
|
||||
minWidth: 60, |
||||
fontSize: '0.75rem', |
||||
py: 0.25 |
||||
}} |
||||
> |
||||
Accept |
||||
</Button> |
||||
</> |
||||
)} |
||||
</Box> |
||||
)} |
||||
|
||||
{/* Mark as Read Button */} |
||||
{!notification.isRead && ( |
||||
<IconButton |
||||
size="small" |
||||
onClick={() => handleMarkAsRead(notification.id)} |
||||
sx={{ ml: 'auto' }} |
||||
> |
||||
<Close sx={{ fontSize: 16 }} /> |
||||
</IconButton> |
||||
)} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
{index < notifications.length - 1 && <Divider />} |
||||
</Box> |
||||
))} |
||||
</Box> |
||||
)} |
||||
</Box> |
||||
</Card> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default NotificationsPage; |
Loading…
Reference in new issue