Add NAO membership status indicators and invite functionality to contacts

FEATURES:
- NAO membership status indicators with distinct visual styling:
  * Green "NAO Member" chip for existing members
  * Orange "Invited" chip for pending invitations
  * No indicator for contacts not yet invited
- Invite buttons for non-members to easily send NAO invitations
- Smart button placement that doesn't interfere with selection mode

VISUAL DESIGN:
- Color-coded status chips with icons (CheckCircle, Schedule)
- Consistent styling with existing contact chips
- Invite button with Send icon for clear action indication
- Proper spacing and alignment in contact cards

DATA STRUCTURE:
- Extended Contact interface with naoStatus, invitedAt, joinedAt fields
- Sample data includes different membership states for testing
- Auto-navigation to invitation page with contact details pre-filled

FUNCTIONALITY:
- Invite button navigates to /invite with contact name and email pre-filled
- Status indicators help users quickly identify network coverage
- Maintains existing selection mode functionality for group invitations

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

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent 7c31fe221a
commit d71bc03451
  1. 7
      public/contacts.json
  2. 118
      src/pages/ContactListPage.tsx
  3. 3
      src/types/contact.ts

@ -11,6 +11,7 @@
"linkedinUrl": "https://linkedin.com/in/johnsmith", "linkedinUrl": "https://linkedin.com/in/johnsmith",
"notes": "Met at React conference 2023", "notes": "Met at React conference 2023",
"tags": ["developer", "react", "frontend"], "tags": ["developer", "react", "frontend"],
"naoStatus": "not_invited",
"createdAt": "2023-10-01T10:00:00Z", "createdAt": "2023-10-01T10:00:00Z",
"updatedAt": "2023-10-15T14:30:00Z" "updatedAt": "2023-10-15T14:30:00Z"
}, },
@ -26,6 +27,8 @@
"linkedinUrl": "https://linkedin.com/in/sarahjohnson", "linkedinUrl": "https://linkedin.com/in/sarahjohnson",
"notes": "Potential collaboration on new project", "notes": "Potential collaboration on new project",
"tags": ["product", "startup", "management"], "tags": ["product", "startup", "management"],
"naoStatus": "member",
"joinedAt": "2023-08-15T10:00:00Z",
"createdAt": "2023-09-15T09:00:00Z", "createdAt": "2023-09-15T09:00:00Z",
"updatedAt": "2023-10-20T16:45:00Z" "updatedAt": "2023-10-20T16:45:00Z"
}, },
@ -40,6 +43,8 @@
"profileImage": "https://i.pravatar.cc/150?img=3", "profileImage": "https://i.pravatar.cc/150?img=3",
"notes": "Design consultant for mobile app", "notes": "Design consultant for mobile app",
"tags": ["design", "ux", "mobile"], "tags": ["design", "ux", "mobile"],
"naoStatus": "invited",
"invitedAt": "2023-12-01T15:30:00Z",
"createdAt": "2023-08-20T11:30:00Z", "createdAt": "2023-08-20T11:30:00Z",
"updatedAt": "2023-09-10T13:15:00Z" "updatedAt": "2023-09-10T13:15:00Z"
}, },
@ -55,6 +60,8 @@
"linkedinUrl": "https://linkedin.com/in/emilyrodriguez", "linkedinUrl": "https://linkedin.com/in/emilyrodriguez",
"notes": "Former colleague from previous company", "notes": "Former colleague from previous company",
"tags": ["engineering", "management", "scaling"], "tags": ["engineering", "management", "scaling"],
"naoStatus": "member",
"joinedAt": "2023-07-20T12:00:00Z",
"createdAt": "2023-07-10T08:00:00Z", "createdAt": "2023-07-10T08:00:00Z",
"updatedAt": "2023-08-25T10:20:00Z" "updatedAt": "2023-08-25T10:20:00Z"
}, },

@ -29,7 +29,10 @@ import {
Group, Group,
CloudDownload, CloudDownload,
VerifiedUser, VerifiedUser,
Favorite Favorite,
CheckCircle,
Schedule,
Send
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Contact } from '../types/contact'; import type { Contact } from '../types/contact';
@ -121,6 +124,34 @@ const ContactListPage = () => {
}).format(date); }).format(date);
}; };
const getNaoStatusIndicator = (contact: Contact) => {
switch (contact.naoStatus) {
case 'member':
return {
icon: <CheckCircle sx={{ fontSize: 16 }} />,
label: 'NAO Member',
color: theme.palette.success.main,
bgColor: alpha(theme.palette.success.main, 0.08),
borderColor: alpha(theme.palette.success.main, 0.2)
};
case 'invited':
return {
icon: <Schedule sx={{ fontSize: 16 }} />,
label: 'Invited',
color: theme.palette.warning.main,
bgColor: alpha(theme.palette.warning.main, 0.08),
borderColor: alpha(theme.palette.warning.main, 0.2)
};
default:
return null;
}
};
const handleInviteToNao = (contact: Contact) => {
// Navigate to invitation page with contact pre-filled
navigate(`/invite?inviteeName=${encodeURIComponent(contact.name)}&inviteeEmail=${encodeURIComponent(contact.email)}`);
};
return ( return (
<Box sx={{ height: '100%' }}> <Box sx={{ height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
@ -293,6 +324,31 @@ const ContactListPage = () => {
}} }}
/> />
)} )}
{/* NAO Status Indicator */}
{(() => {
const naoStatus = getNaoStatusIndicator(contact);
return naoStatus ? (
<Chip
icon={naoStatus.icon}
label={naoStatus.label}
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: naoStatus.bgColor,
borderColor: naoStatus.borderColor,
color: naoStatus.color,
'& .MuiChip-icon': {
fontSize: 14,
},
}}
/>
) : null;
})()}
{/* Vouch and Praise Indicators */} {/* Vouch and Praise Indicators */}
<Chip <Chip
icon={<VerifiedUser sx={{ fontSize: 14 }} />} icon={<VerifiedUser sx={{ fontSize: 14 }} />}
@ -361,24 +417,48 @@ const ContactListPage = () => {
</Typography> </Typography>
</Box> </Box>
{/* Select button for selection mode */} {/* Action buttons */}
{isSelectionMode && ( <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, alignSelf: 'center' }}>
<Button {/* Select button for selection mode */}
variant="contained" {isSelectionMode && (
onClick={(e) => { <Button
e.stopPropagation(); variant="contained"
handleSelectContact(contact); onClick={(e) => {
}} e.stopPropagation();
sx={{ handleSelectContact(contact);
borderRadius: 2, }}
textTransform: 'none', sx={{
alignSelf: 'center', borderRadius: 2,
minWidth: 80 textTransform: 'none',
}} minWidth: 80
> }}
Select >
</Button> 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: 80,
fontSize: '0.875rem',
py: 0.5,
px: 1.5
}}
>
Invite
</Button>
)}
</Box>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>

@ -11,6 +11,9 @@ export interface Contact {
notes?: string; notes?: string;
tags?: string[]; tags?: string[];
groupIds?: string[]; groupIds?: string[];
naoStatus?: 'member' | 'invited' | 'not_invited';
invitedAt?: Date;
joinedAt?: Date;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }

Loading…
Cancel
Save