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,76 +431,21 @@ const ContactListPage = () => {
{getSourceIcon(contact.source)} {getSourceIcon(contact.source)}
</Box> </Box>
{/* Job Title & Company */}
<Typography <Typography
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ sx={{ mb: 1, lineHeight: 1.4 }}
fontSize: { xs: '0.75rem', md: '0.875rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
> >
{contact.position}{contact.company && ` at ${contact.company}`} {contact.position}{contact.company && ` at ${contact.company}`}
</Typography> </Typography>
</Box>
{/* Action buttons - always visible on the right */} {/* Email */}
<Box sx={{ flexShrink: 0 }}>
{/* Select button for selection mode */}
{isSelectionMode && (
<Button
variant="contained"
onClick={(e) => {
e.stopPropagation();
handleSelectContact(contact);
}}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: { xs: 60, md: 80 },
fontSize: { xs: '0.75rem', md: '0.875rem' }
}}
>
Select
</Button>
)}
{/* Invite to NAO button for non-members (not in selection mode) */}
{!isSelectionMode && contact.naoStatus === 'not_invited' && (
<Button
variant="outlined"
startIcon={<Send sx={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
handleInviteToNao(contact);
}}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: { xs: 60, md: 80 },
fontSize: { xs: '0.75rem', md: '0.875rem' },
py: 0.5,
px: { xs: 1, md: 1.5 }
}}
>
Invite
</Button>
)}
</Box>
</Box>
{/* Bottom section: Email + Status chips + Tags */}
<Box sx={{
width: '100%',
display: { xs: 'block', md: 'none' } // Only show on mobile
}}>
<Typography <Typography
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ sx={{
mb: 1, fontSize: '0.875rem',
fontSize: '0.75rem',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
@ -528,50 +453,21 @@ const ContactListPage = () => {
> >
{contact.email} {contact.email}
</Typography> </Typography>
</Box>
{/* Compact status and metrics chips */} {/* Middle Column - Vouch & Praise Counts + Tags */}
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}> <Box sx={{
{contact.groupIds && contact.groupIds.length > 0 && ( display: 'flex',
<Chip flexDirection: 'column',
icon={<Group sx={{ fontSize: 12 }} />} alignItems: 'flex-start',
label={contact.groupIds.length} justifyContent: 'flex-start',
size="small" gap: 1,
variant="outlined" flexGrow: 1,
sx={{ pt: 0.5,
fontSize: '0.65rem', minWidth: 200
height: 18, }}>
borderRadius: 1, {/* Vouches and Praises Row */}
backgroundColor: alpha(theme.palette.success.main, 0.04), <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, height: 24 }}>
borderColor: alpha(theme.palette.success.main, 0.12),
color: 'success.main',
'& .MuiChip-icon': { fontSize: 12 },
}}
/>
)}
{/* NAO Status Indicator */}
{(() => {
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;
@ -579,49 +475,100 @@ const ContactListPage = () => {
return ( return (
<> <>
{totalVouches > 0 && (
<Chip <Chip
icon={<VerifiedUser sx={{ fontSize: 12 }} />} icon={<VerifiedUser sx={{ fontSize: 14 }} />}
label={totalVouches} 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" size="small"
variant="outlined" variant="outlined"
title={contact.naoStatus === 'member'
? `Vouches: ${counts.vouchesReceived} received, ${counts.vouchesSent} sent`
: `Vouches: ${counts.vouchesSent} sent (hidden until they join)`}
sx={{ sx={{
fontSize: '0.65rem', fontSize: '0.75rem',
height: 18, height: 24,
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 }, fontWeight: 500,
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}} }}
/> />
)}
{totalPraises > 0 && (
<Chip <Chip
icon={<Favorite sx={{ fontSize: 12 }} />} icon={<Favorite sx={{ fontSize: 14 }} />}
label={totalPraises} 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" size="small"
variant="outlined" variant="outlined"
title={contact.naoStatus === 'member'
? `Praises: ${counts.praisesReceived} received, ${counts.praisesSent} sent`
: `Praises: ${counts.praisesSent} sent (hidden until they join)`}
sx={{ sx={{
fontSize: '0.65rem', fontSize: '0.75rem',
height: 18, height: 24,
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 }, fontWeight: 500,
'& .MuiChip-icon': { fontSize: 14 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}} }}
/> />
)}
</> </>
); );
})()} })()}
</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', justifyContent: 'flex-start' }}>
{contact.tags.slice(0, 2).map((tag) => ( {contact.tags.slice(0, 3).map((tag) => (
<Chip <Chip
key={tag} key={tag}
label={tag} label={tag}
@ -629,18 +576,18 @@ 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.65rem',
height: 18 height: 18
}} }}
/> />
))} ))}
{contact.tags.length > 2 && ( {contact.tags.length > 3 && (
<Chip <Chip
label={`+${contact.tags.length - 2}`} label={`+${contact.tags.length - 3}`}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
@ -657,53 +604,185 @@ const ContactListPage = () => {
)} )}
</Box> </Box>
{/* Desktop layout (hidden on mobile) */} {/* Right Column - Network Status Only (Push to Far Right) */}
<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: 140,
height: 56,
ml: 'auto'
}}> }}>
<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: 3,
py: 1,
fontWeight: 600,
fontSize: '0.75rem',
height: 28,
minWidth: 80
}}
>
Select
</Button>
);
} else if (contact.naoStatus === 'not_invited') {
return (
<Button
variant="outlined" variant="outlined"
size="small"
startIcon={<Send sx={{ fontSize: 14 }} />}
onClick={() => handleInviteToNao(contact)}
sx={{ sx={{
borderRadius: 2,
px: 2,
py: 1,
fontWeight: 500,
fontSize: '0.75rem', fontSize: '0.75rem',
height: 20, height: 28,
borderRadius: 1, minWidth: 80
backgroundColor: alpha(theme.palette.success.main, 0.04),
borderColor: alpha(theme.palette.success.main, 0.12),
color: 'success.main',
'& .MuiChip-icon': { fontSize: 14 },
}} }}
/> >
)} 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.75rem',
height: 28,
minWidth: 80,
'& .MuiChip-icon': { fontSize: 14 }
}} }}
/> />
) : null; ) : null;
}
})()} })()}
{/* Vouch and Praise Indicators with directional arrows - desktop only */} {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>
{/* Mobile Layout */}
<Box sx={{ display: { xs: 'flex', md: 'none' }, alignItems: 'flex-start', gap: 2 }}>
{/* Avatar */}
<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
variant="body2"
color="text.secondary"
sx={{
mb: 1,
fontSize: '0.75rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{contact.email}
</Typography>
{/* Info Chips Row (excluding NAO status) */}
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center', mb: 1 }}>
{/* Groups Count */}
{contact.groupIds && contact.groupIds.length > 0 && (
<Chip
icon={<Group sx={{ fontSize: 12 }} />}
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.65rem',
height: 20
}}
/>
)}
{/* Vouch & Praise Counts */}
{(() => { {(() => {
const counts = getVouchPraiseCounts(contact); const counts = getVouchPraiseCounts(contact);
const totalVouches = counts.vouchesSent + counts.vouchesReceived; const totalVouches = counts.vouchesSent + counts.vouchesReceived;
@ -711,99 +790,49 @@ const ContactListPage = () => {
return ( return (
<> <>
{totalVouches > 0 && (
<Chip <Chip
icon={<VerifiedUser sx={{ fontSize: 14 }} />} icon={<VerifiedUser sx={{ fontSize: 10 }} />}
label={ label={totalVouches}
<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" size="small"
variant="outlined" variant="outlined"
title={contact.naoStatus === 'member'
? `Vouches: ${counts.vouchesReceived} received, ${counts.vouchesSent} sent`
: `Vouches: ${counts.vouchesSent} sent (hidden until they join)`}
sx={{ sx={{
fontSize: '0.75rem', fontSize: '0.6rem',
height: 20, 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: 14 }, '& .MuiChip-icon': { fontSize: 10 },
'& .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>} {totalPraises > 0 && (
</Box> <Chip
} icon={<Favorite sx={{ fontSize: 10 }} />}
label={totalPraises}
size="small" size="small"
variant="outlined" variant="outlined"
title={contact.naoStatus === 'member'
? `Praises: ${counts.praisesReceived} received, ${counts.praisesSent} sent`
: `Praises: ${counts.praisesSent} sent (hidden until they join)`}
sx={{ sx={{
fontSize: '0.75rem', fontSize: '0.6rem',
height: 20, 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: 14 }, '& .MuiChip-icon': { fontSize: 10 },
'& .MuiChip-label': {
fontSize: '0.75rem',
padding: '0 4px',
},
}} }}
/> />
)}
</> </>
); );
})()} })()}
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> {/* Tags Row */}
{contact.email} {contact.tags && contact.tags.length > 0 && (
</Typography> <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{contact.tags.slice(0, 2).map((tag) => (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
{contact.tags?.map((tag) => (
<Chip <Chip
key={tag} key={tag}
label={tag} label={tag}
@ -811,18 +840,106 @@ 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.6rem',
height: 16
}} }}
/> />
))} ))}
{contact.tags.length > 2 && (
<Chip
label={`+${contact.tags.length - 2}`}
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.6rem',
height: 16
}}
/>
)}
</Box>
)}
</Box> </Box>
<Typography variant="caption" color="text.secondary"> {/* Right Column - Status & Action */}
Added {formatDate(contact.createdAt)} <Box sx={{
</Typography> display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
justifyContent: 'center',
gap: 1,
flexShrink: 0,
minWidth: 80,
height: 48
}}>
{(() => {
const naoStatus = getNaoStatusIndicator(contact);
if (isSelectionMode) {
return (
<Button
variant="contained"
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"
size="small"
startIcon={<Send sx={{ fontSize: 12 }} />}
onClick={() => handleInviteToNao(contact)}
sx={{
borderRadius: 2,
px: 1.5,
py: 0.5,
fontWeight: 500,
fontSize: '0.7rem',
height: 24,
minWidth: 60
}}
>
Invite
</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.65rem',
height: 24,
minWidth: 60,
'& .MuiChip-icon': { fontSize: 12 }
}}
/>
) : null;
}
})()}
</Box> </Box>
</Box> </Box>
</CardContent> </CardContent>

@ -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