Redesign ContactListPage with clean, professional contact cards layout

- Separate responsive layouts for desktop and mobile
- Desktop: Clean 4-column layout (Avatar | Info | Vouches/Tags | Status)
- Mobile: Optimized 3-column layout (Avatar | Info | Status)
- Fix contact card alignment and spacing issues
- Restore directional arrows for vouches/praises (↑ sent, ↓ received)
- Right-align status buttons (Invite/NAO Member/Invited) on both layouts
- Improve mobile spacing and chip sizing
- Remove unused formatDate function and fix imports

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

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent 9849318490
commit e0ce0fbcd4
  1. 641
      src/pages/ContactListPage.tsx
  2. 30
      src/services/dataService.ts

@ -118,13 +118,6 @@ const ContactListPage = () => {
} }
}; };
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
const getNaoStatusIndicator = (contact: Contact) => { const getNaoStatusIndicator = (contact: Contact) => {
switch (contact.naoStatus) { switch (contact.naoStatus) {
@ -390,60 +383,47 @@ const ContactListPage = () => {
borderColor: 'divider', borderColor: 'divider',
'&:hover': !isSelectionMode ? { '&:hover': !isSelectionMode ? {
borderColor: 'primary.main', borderColor: 'primary.main',
boxShadow: theme.shadows[4], boxShadow: theme.shadows[2],
transform: 'translateY(-2px)', transform: 'translateY(-1px)',
} : {}, } : {},
}} }}
> >
<CardContent sx={{ p: { xs: 2, md: 3 } }}> <CardContent sx={{ p: 3 }}>
<Box sx={{ {/* Desktop Layout */}
display: 'flex', <Box sx={{ display: { xs: 'none', md: 'flex' }, alignItems: 'flex-start', width: '100%' }}>
flexDirection: { xs: 'column', md: 'row' }, {/* Avatar */}
alignItems: { xs: 'stretch', md: 'center' },
gap: { xs: 2, md: 2 }
}}>
{/* Top row on mobile: Avatar + Name + Action Button */}
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
width: { xs: '100%', md: 'auto' },
flexShrink: 0
}}>
<Box <Box
sx={{ sx={{
width: { xs: 48, md: 64 }, width: 56,
height: { xs: 48, md: 64 }, height: 56,
borderRadius: '50%', borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none', backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: contact.profileImage ? 'transparent' : 'primary.main', backgroundColor: contact.profileImage ? 'transparent' : 'primary.main',
color: 'white', color: 'white',
fontSize: { xs: '1.2rem', md: '1.5rem' }, fontSize: '1.25rem',
fontWeight: 'bold', fontWeight: 600,
flexShrink: 0 flexShrink: 0,
mr: 3
}} }}
> >
{!contact.profileImage && contact.name.charAt(0)} {!contact.profileImage && contact.name.charAt(0)}
</Box> </Box>
<Box sx={{ flexGrow: 1, minWidth: 0 }}> {/* Left Column - Basic Info */}
<Box sx={{ width: 280, flexShrink: 0, mr: 3 }}>
{/* Name with source icon inline */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography <Typography
variant="h6" variant="h6"
component="div"
sx={{ sx={{
fontWeight: 600, fontWeight: 600,
fontSize: { xs: '1rem', md: '1.25rem' }, fontSize: '1.125rem',
overflow: 'hidden', color: 'text.primary'
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1
}} }}
> >
{contact.name} {contact.name}
@ -451,70 +431,323 @@ const ContactListPage = () => {
{getSourceIcon(contact.source)} {getSourceIcon(contact.source)}
</Box> </Box>
{/* Job Title & Company */}
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 1, lineHeight: 1.4 }}
>
{contact.position}{contact.company && ` at ${contact.company}`}
</Typography>
{/* Email */}
<Typography <Typography
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ sx={{
fontSize: { xs: '0.75rem', md: '0.875rem' }, fontSize: '0.875rem',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
> >
{contact.position} {contact.company && `at ${contact.company}`} {contact.email}
</Typography> </Typography>
</Box> </Box>
{/* Action buttons - always visible on the right */} {/* Middle Column - Vouch & Praise Counts + Tags */}
<Box sx={{ flexShrink: 0 }}> <Box sx={{
{/* Select button for selection mode */} display: 'flex',
{isSelectionMode && ( flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
gap: 1,
flexGrow: 1,
pt: 0.5,
minWidth: 200
}}>
{/* Vouches and Praises Row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, height: 24 }}>
{(() => {
const counts = getVouchPraiseCounts(contact);
const totalVouches = counts.vouchesSent + counts.vouchesReceived;
const totalPraises = counts.praisesSent + counts.praisesReceived;
return (
<>
<Chip
icon={<VerifiedUser sx={{ fontSize: 14 }} />}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{counts.vouchesReceived > 0 && (
<>
<ArrowDownward sx={{ fontSize: 10 }} />
<span>{counts.vouchesReceived}</span>
</>
)}
{counts.vouchesSent > 0 && counts.vouchesReceived > 0 && (
<span style={{ fontSize: '0.6rem', opacity: 0.6 }}></span>
)}
{counts.vouchesSent > 0 && (
<>
<ArrowUpward sx={{ fontSize: 10 }} />
<span>{counts.vouchesSent}</span>
</>
)}
{totalVouches === 0 && <span>0</span>}
</Box>
}
size="small"
variant="outlined"
title={contact.naoStatus === 'member'
? `Vouches: ${counts.vouchesReceived} received, ${counts.vouchesSent} sent`
: `Vouches: ${counts.vouchesSent} sent (hidden until they join)`}
sx={{
fontSize: '0.75rem',
height: 24,
borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main',
fontWeight: 500,
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}}
/>
<Chip
icon={<Favorite sx={{ fontSize: 14 }} />}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{counts.praisesReceived > 0 && (
<>
<ArrowDownward sx={{ fontSize: 10 }} />
<span>{counts.praisesReceived}</span>
</>
)}
{counts.praisesSent > 0 && counts.praisesReceived > 0 && (
<span style={{ fontSize: '0.6rem', opacity: 0.6 }}></span>
)}
{counts.praisesSent > 0 && (
<>
<ArrowUpward sx={{ fontSize: 10 }} />
<span>{counts.praisesSent}</span>
</>
)}
{totalPraises === 0 && <span>0</span>}
</Box>
}
size="small"
variant="outlined"
title={contact.naoStatus === 'member'
? `Praises: ${counts.praisesReceived} received, ${counts.praisesSent} sent`
: `Praises: ${counts.praisesSent} sent (hidden until they join)`}
sx={{
fontSize: '0.75rem',
height: 24,
borderRadius: 1,
backgroundColor: alpha('#f8bbd9', 0.3),
borderColor: alpha('#d81b60', 0.3),
color: '#d81b60',
fontWeight: 500,
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}}
/>
</>
);
})()}
</Box>
{/* Tags Row */}
{contact.tags && contact.tags.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', justifyContent: 'flex-start' }}>
{contact.tags.slice(0, 3).map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
backgroundColor: alpha(theme.palette.grey[500], 0.04),
borderColor: alpha(theme.palette.grey[500], 0.12),
color: 'text.secondary',
fontWeight: 500,
fontSize: '0.65rem',
height: 18
}}
/>
))}
{contact.tags.length > 3 && (
<Chip
label={`+${contact.tags.length - 3}`}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
backgroundColor: alpha(theme.palette.grey[500], 0.04),
borderColor: alpha(theme.palette.grey[500], 0.12),
color: 'text.secondary',
fontSize: '0.65rem',
height: 18
}}
/>
)}
</Box>
)}
</Box>
{/* Right Column - Network Status Only (Push to Far Right) */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
justifyContent: 'center',
gap: 1,
flexShrink: 0,
minWidth: 140,
height: 56,
ml: 'auto'
}}>
{(() => {
const naoStatus = getNaoStatusIndicator(contact);
if (isSelectionMode) {
return (
<Button <Button
variant="contained" variant="contained"
onClick={(e) => { size="small"
e.stopPropagation(); onClick={() => handleSelectContact(contact)}
handleSelectContact(contact);
}}
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
textTransform: 'none', px: 3,
minWidth: { xs: 60, md: 80 }, py: 1,
fontSize: { xs: '0.75rem', md: '0.875rem' } fontWeight: 600,
fontSize: '0.75rem',
height: 28,
minWidth: 80
}} }}
> >
Select Select
</Button> </Button>
)} );
} else if (contact.naoStatus === 'not_invited') {
{/* Invite to NAO button for non-members (not in selection mode) */} return (
{!isSelectionMode && contact.naoStatus === 'not_invited' && (
<Button <Button
variant="outlined" variant="outlined"
startIcon={<Send sx={{ fontSize: 16 }} />} size="small"
onClick={(e) => { startIcon={<Send sx={{ fontSize: 14 }} />}
e.stopPropagation(); onClick={() => handleInviteToNao(contact)}
handleInviteToNao(contact);
}}
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
textTransform: 'none', px: 2,
minWidth: { xs: 60, md: 80 }, py: 1,
fontSize: { xs: '0.75rem', md: '0.875rem' }, fontWeight: 500,
py: 0.5, fontSize: '0.75rem',
px: { xs: 1, md: 1.5 } height: 28,
minWidth: 80
}} }}
> >
Invite Invite
</Button> </Button>
);
} else {
return naoStatus ? (
<Chip
icon={naoStatus.icon}
label={naoStatus.label}
size="small"
sx={{
backgroundColor: naoStatus.bgColor,
borderColor: naoStatus.borderColor,
color: naoStatus.color,
fontWeight: 500,
fontSize: '0.75rem',
height: 28,
minWidth: 80,
'& .MuiChip-icon': { fontSize: 14 }
}}
/>
) : null;
}
})()}
{contact.groupIds && contact.groupIds.length > 0 && (
<Chip
icon={<Group sx={{ fontSize: 14 }} />}
label={`${contact.groupIds.length} group${contact.groupIds.length > 1 ? 's' : ''}`}
size="small"
variant="outlined"
sx={{
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.2),
color: 'primary.main',
fontWeight: 500,
fontSize: '0.75rem',
height: 24
}}
/>
)} )}
</Box> </Box>
</Box> </Box>
{/* Bottom section: Email + Status chips + Tags */} {/* Mobile Layout */}
<Box sx={{ <Box sx={{ display: { xs: 'flex', md: 'none' }, alignItems: 'flex-start', gap: 2 }}>
width: '100%', {/* Avatar */}
display: { xs: 'block', md: 'none' } // Only show on mobile <Box
}}> sx={{
width: 48,
height: 48,
borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: contact.profileImage ? 'transparent' : 'primary.main',
color: 'white',
fontSize: '1.125rem',
fontWeight: 600,
flexShrink: 0
}}
>
{!contact.profileImage && contact.name.charAt(0)}
</Box>
{/* Main Content */}
<Box sx={{ flex: 1, minWidth: 0 }}>
{/* Name with source icon */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography
variant="h6"
sx={{
fontWeight: 600,
fontSize: '1rem',
color: 'text.primary'
}}
>
{contact.name}
</Typography>
{getSourceIcon(contact.source)}
</Box>
{/* Job Title & Company */}
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 0.5, lineHeight: 1.3, fontSize: '0.8rem' }}
>
{contact.position}{contact.company && ` at ${contact.company}`}
</Typography>
{/* Email */}
<Typography <Typography
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
@ -529,49 +762,27 @@ const ContactListPage = () => {
{contact.email} {contact.email}
</Typography> </Typography>
{/* Compact status and metrics chips */} {/* Info Chips Row (excluding NAO status) */}
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center', mb: 1 }}>
{/* Groups Count */}
{contact.groupIds && contact.groupIds.length > 0 && ( {contact.groupIds && contact.groupIds.length > 0 && (
<Chip <Chip
icon={<Group sx={{ fontSize: 12 }} />} icon={<Group sx={{ fontSize: 12 }} />}
label={contact.groupIds.length} label={`${contact.groupIds.length} group${contact.groupIds.length > 1 ? 's' : ''}`}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.2),
color: 'primary.main',
fontWeight: 500,
fontSize: '0.65rem', fontSize: '0.65rem',
height: 18, height: 20
borderRadius: 1,
backgroundColor: alpha(theme.palette.success.main, 0.04),
borderColor: alpha(theme.palette.success.main, 0.12),
color: 'success.main',
'& .MuiChip-icon': { fontSize: 12 },
}} }}
/> />
)} )}
{/* NAO Status Indicator */} {/* Vouch & Praise Counts */}
{(() => {
const naoStatus = getNaoStatusIndicator(contact);
return naoStatus ? (
<Chip
icon={naoStatus.icon}
label={naoStatus.label}
size="small"
variant="outlined"
sx={{
fontSize: '0.65rem',
height: 18,
borderRadius: 1,
backgroundColor: naoStatus.bgColor,
borderColor: naoStatus.borderColor,
color: naoStatus.color,
'& .MuiChip-icon': { fontSize: 12 },
}}
/>
) : null;
})()}
{/* Simplified Vouch and Praise counts */}
{(() => { {(() => {
const counts = getVouchPraiseCounts(contact); const counts = getVouchPraiseCounts(contact);
const totalVouches = counts.vouchesSent + counts.vouchesReceived; const totalVouches = counts.vouchesSent + counts.vouchesReceived;
@ -581,35 +792,35 @@ const ContactListPage = () => {
<> <>
{totalVouches > 0 && ( {totalVouches > 0 && (
<Chip <Chip
icon={<VerifiedUser sx={{ fontSize: 12 }} />} icon={<VerifiedUser sx={{ fontSize: 10 }} />}
label={totalVouches} label={totalVouches}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
fontSize: '0.65rem', fontSize: '0.6rem',
height: 18, height: 18,
borderRadius: 1, borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04), backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12), borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main', color: 'primary.main',
'& .MuiChip-icon': { fontSize: 12 }, '& .MuiChip-icon': { fontSize: 10 },
}} }}
/> />
)} )}
{totalPraises > 0 && ( {totalPraises > 0 && (
<Chip <Chip
icon={<Favorite sx={{ fontSize: 12 }} />} icon={<Favorite sx={{ fontSize: 10 }} />}
label={totalPraises} label={totalPraises}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
fontSize: '0.65rem', fontSize: '0.6rem',
height: 18, height: 18,
borderRadius: 1, borderRadius: 1,
backgroundColor: alpha('#f8bbd9', 0.3), backgroundColor: alpha('#f8bbd9', 0.3),
borderColor: alpha('#d81b60', 0.3), borderColor: alpha('#d81b60', 0.3),
color: '#d81b60', color: '#d81b60',
'& .MuiChip-icon': { fontSize: 12 }, '& .MuiChip-icon': { fontSize: 10 },
}} }}
/> />
)} )}
@ -618,7 +829,7 @@ const ContactListPage = () => {
})()} })()}
</Box> </Box>
{/* Tags - only show first 2 on mobile */} {/* Tags Row */}
{contact.tags && contact.tags.length > 0 && ( {contact.tags && contact.tags.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{contact.tags.slice(0, 2).map((tag) => ( {contact.tags.slice(0, 2).map((tag) => (
@ -629,12 +840,12 @@ const ContactListPage = () => {
variant="outlined" variant="outlined"
sx={{ sx={{
borderRadius: 1, borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04), backgroundColor: alpha(theme.palette.grey[500], 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12), borderColor: alpha(theme.palette.grey[500], 0.12),
color: 'primary.main', color: 'text.secondary',
fontWeight: 500, fontWeight: 500,
fontSize: '0.65rem', fontSize: '0.6rem',
height: 18 height: 16
}} }}
/> />
))} ))}
@ -648,8 +859,8 @@ const ContactListPage = () => {
backgroundColor: alpha(theme.palette.grey[500], 0.04), backgroundColor: alpha(theme.palette.grey[500], 0.04),
borderColor: alpha(theme.palette.grey[500], 0.12), borderColor: alpha(theme.palette.grey[500], 0.12),
color: 'text.secondary', color: 'text.secondary',
fontSize: '0.65rem', fontSize: '0.6rem',
height: 18 height: 16
}} }}
/> />
)} )}
@ -657,173 +868,79 @@ const ContactListPage = () => {
)} )}
</Box> </Box>
{/* Desktop layout (hidden on mobile) */} {/* Right Column - Status & Action */}
<Box sx={{ <Box sx={{
flexGrow: 1, display: 'flex',
display: { xs: 'none', md: 'block' } // Only show on desktop flexDirection: 'column',
alignItems: 'flex-end',
justifyContent: 'center',
gap: 1,
flexShrink: 0,
minWidth: 80,
height: 48
}}> }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, flexWrap: 'wrap' }}> {(() => {
{contact.groupIds && contact.groupIds.length > 0 && ( const naoStatus = getNaoStatusIndicator(contact);
<Chip if (isSelectionMode) {
icon={<Group sx={{ fontSize: 14 }} />} return (
label={contact.groupIds.length} <Button
variant="contained"
size="small" size="small"
onClick={() => handleSelectContact(contact)}
sx={{
borderRadius: 2,
px: 2,
py: 0.5,
fontWeight: 600,
fontSize: '0.7rem',
height: 24,
minWidth: 60
}}
>
Select
</Button>
);
} else if (contact.naoStatus === 'not_invited') {
return (
<Button
variant="outlined" variant="outlined"
size="small"
startIcon={<Send sx={{ fontSize: 12 }} />}
onClick={() => handleInviteToNao(contact)}
sx={{ sx={{
fontSize: '0.75rem', borderRadius: 2,
height: 20, px: 1.5,
borderRadius: 1, py: 0.5,
backgroundColor: alpha(theme.palette.success.main, 0.04), fontWeight: 500,
borderColor: alpha(theme.palette.success.main, 0.12), fontSize: '0.7rem',
color: 'success.main', height: 24,
'& .MuiChip-icon': { fontSize: 14 }, minWidth: 60
}} }}
/> >
)} Invite
</Button>
{/* NAO Status Indicator */} );
{(() => { } else {
const naoStatus = getNaoStatusIndicator(contact);
return naoStatus ? ( return naoStatus ? (
<Chip <Chip
icon={naoStatus.icon} icon={naoStatus.icon}
label={naoStatus.label} label={naoStatus.label}
size="small" size="small"
variant="outlined"
sx={{ sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: naoStatus.bgColor, backgroundColor: naoStatus.bgColor,
borderColor: naoStatus.borderColor, borderColor: naoStatus.borderColor,
color: naoStatus.color, color: naoStatus.color,
'& .MuiChip-icon': { fontSize: 14 }, fontWeight: 500,
fontSize: '0.65rem',
height: 24,
minWidth: 60,
'& .MuiChip-icon': { fontSize: 12 }
}} }}
/> />
) : null; ) : null;
})()}
{/* Vouch and Praise Indicators with directional arrows - desktop only */}
{(() => {
const counts = getVouchPraiseCounts(contact);
const totalVouches = counts.vouchesSent + counts.vouchesReceived;
const totalPraises = counts.praisesSent + counts.praisesReceived;
return (
<>
<Chip
icon={<VerifiedUser sx={{ fontSize: 14 }} />}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{counts.vouchesReceived > 0 && (
<>
<ArrowDownward sx={{ fontSize: 10 }} />
<span>{counts.vouchesReceived}</span>
</>
)}
{counts.vouchesSent > 0 && counts.vouchesReceived > 0 && (
<span style={{ fontSize: '0.6rem', opacity: 0.6 }}></span>
)}
{counts.vouchesSent > 0 && (
<>
<ArrowUpward sx={{ fontSize: 10 }} />
<span>{counts.vouchesSent}</span>
</>
)}
{totalVouches === 0 && <span>0</span>}
</Box>
} }
size="small"
variant="outlined"
title={contact.naoStatus === 'member'
? `Vouches: ${counts.vouchesReceived} received, ${counts.vouchesSent} sent`
: `Vouches: ${counts.vouchesSent} sent (hidden until they join)`}
sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main',
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}}
/>
<Chip
icon={<Favorite sx={{ fontSize: 14 }} />}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{counts.praisesReceived > 0 && (
<>
<ArrowDownward sx={{ fontSize: 10 }} />
<span>{counts.praisesReceived}</span>
</>
)}
{counts.praisesSent > 0 && counts.praisesReceived > 0 && (
<span style={{ fontSize: '0.6rem', opacity: 0.6 }}></span>
)}
{counts.praisesSent > 0 && (
<>
<ArrowUpward sx={{ fontSize: 10 }} />
<span>{counts.praisesSent}</span>
</>
)}
{totalPraises === 0 && <span>0</span>}
</Box>
}
size="small"
variant="outlined"
title={contact.naoStatus === 'member'
? `Praises: ${counts.praisesReceived} received, ${counts.praisesSent} sent`
: `Praises: ${counts.praisesSent} sent (hidden until they join)`}
sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: alpha('#f8bbd9', 0.3),
borderColor: alpha('#d81b60', 0.3),
color: '#d81b60',
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}}
/>
</>
);
})()} })()}
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{contact.email}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
{contact.tags?.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main',
fontWeight: 500,
}}
/>
))}
</Box>
<Typography variant="caption" color="text.secondary">
Added {formatDate(contact.createdAt)}
</Typography>
</Box>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>

@ -8,11 +8,23 @@ export const dataService = {
try { try {
const response = await fetch('/contacts.json'); const response = await fetch('/contacts.json');
const contactsData = await response.json(); const contactsData = await response.json();
const contacts = contactsData.map((contact: any) => ({ const contacts = contactsData.map((contact: any) => {
const processedContact = {
...contact, ...contact,
createdAt: new Date(contact.createdAt), createdAt: new Date(contact.createdAt),
updatedAt: new Date(contact.updatedAt) updatedAt: new Date(contact.updatedAt)
})); };
// Convert optional date fields if they exist
if (contact.joinedAt) {
processedContact.joinedAt = new Date(contact.joinedAt);
}
if (contact.invitedAt) {
processedContact.invitedAt = new Date(contact.invitedAt);
}
return processedContact;
});
resolve(contacts); resolve(contacts);
} catch (error) { } catch (error) {
console.error('Failed to load contacts:', error); console.error('Failed to load contacts:', error);
@ -30,11 +42,21 @@ export const dataService = {
const contactsData = await response.json(); const contactsData = await response.json();
const contact = contactsData.find((c: any) => c.id === id); const contact = contactsData.find((c: any) => c.id === id);
if (contact) { if (contact) {
resolve({ const processedContact = {
...contact, ...contact,
createdAt: new Date(contact.createdAt), createdAt: new Date(contact.createdAt),
updatedAt: new Date(contact.updatedAt) updatedAt: new Date(contact.updatedAt)
}); };
// Convert optional date fields if they exist
if (contact.joinedAt) {
processedContact.joinedAt = new Date(contact.joinedAt);
}
if (contact.invitedAt) {
processedContact.invitedAt = new Date(contact.invitedAt);
}
resolve(processedContact);
} else { } else {
resolve(undefined); resolve(undefined);
} }

Loading…
Cancel
Save