FEATURES: - New GroupJoinPage for existing NAO members to select relationship type (rCard) - Toggle on invitation page to specify if recipient is existing member - Automatic routing: existing members → rCard selection, new users → full onboarding - Privacy-focused UI explaining information sharing based on selected rCard COMPONENTS: - GroupJoinPage: Beautiful rCard selection interface with privacy explanations - Updated InvitationPage: Added "Recipient is already a NAO member" toggle - Updated SocialContractPage: Routes existing members to GroupJoinPage - Updated GroupDetailPage: Handles rCard selection data and URL cleanup FLOW: New Users: Invitation → Social Contract → Onboarding → Group Existing Users: Invitation → rCard Selection → Group (with chosen privacy level) rCard determines information sharing level (Business, Colleague, Friend, etc.) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
parent
3b5653ad36
commit
c28d62fb78
@ -0,0 +1,288 @@ |
|||||||
|
import { useState, useEffect } from 'react'; |
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom'; |
||||||
|
import { |
||||||
|
Container, |
||||||
|
Typography, |
||||||
|
Box, |
||||||
|
Paper, |
||||||
|
Button, |
||||||
|
Avatar, |
||||||
|
Card, |
||||||
|
CardContent, |
||||||
|
Chip, |
||||||
|
alpha, |
||||||
|
useTheme |
||||||
|
} from '@mui/material'; |
||||||
|
import { |
||||||
|
Business, |
||||||
|
PersonOutline, |
||||||
|
Groups, |
||||||
|
FamilyRestroom, |
||||||
|
Favorite, |
||||||
|
Home, |
||||||
|
ArrowBack, |
||||||
|
CheckCircle |
||||||
|
} from '@mui/icons-material'; |
||||||
|
import { dataService } from '../services/dataService'; |
||||||
|
import { DEFAULT_RCARDS } from '../types/notification'; |
||||||
|
import type { Group } from '../types/group'; |
||||||
|
|
||||||
|
const GroupJoinPage = () => { |
||||||
|
const [group, setGroup] = useState<Group | null>(null); |
||||||
|
const [selectedRCard, setSelectedRCard] = useState<string>(''); |
||||||
|
const [inviterName, setInviterName] = useState<string>(''); |
||||||
|
const [isLoading, setIsLoading] = useState(true); |
||||||
|
const navigate = useNavigate(); |
||||||
|
const [searchParams] = useSearchParams(); |
||||||
|
const theme = useTheme(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const loadGroupData = async () => { |
||||||
|
const groupId = searchParams.get('groupId'); |
||||||
|
const inviter = searchParams.get('inviterName') || 'Someone'; |
||||||
|
|
||||||
|
setInviterName(inviter); |
||||||
|
|
||||||
|
if (groupId) { |
||||||
|
try { |
||||||
|
const groupData = await dataService.getGroup(groupId); |
||||||
|
setGroup(groupData || null); |
||||||
|
} catch (error) { |
||||||
|
console.error('Failed to load group:', error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setIsLoading(false); |
||||||
|
}; |
||||||
|
|
||||||
|
loadGroupData(); |
||||||
|
}, [searchParams]); |
||||||
|
|
||||||
|
const getRCardIcon = (iconName: string) => { |
||||||
|
const iconMap: Record<string, React.ReactElement> = { |
||||||
|
Business: <Business />, |
||||||
|
PersonOutline: <PersonOutline />, |
||||||
|
Groups: <Groups />, |
||||||
|
FamilyRestroom: <FamilyRestroom />, |
||||||
|
Favorite: <Favorite />, |
||||||
|
Home: <Home />, |
||||||
|
}; |
||||||
|
return iconMap[iconName] || <PersonOutline />; |
||||||
|
}; |
||||||
|
|
||||||
|
const handleRCardSelect = (rCardName: string) => { |
||||||
|
setSelectedRCard(rCardName); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleJoinGroup = () => { |
||||||
|
if (!selectedRCard || !group) return; |
||||||
|
|
||||||
|
// Store the selected rCard for this group membership
|
||||||
|
sessionStorage.setItem(`groupRCard_${group.id}`, selectedRCard); |
||||||
|
|
||||||
|
// Navigate to group with member parameters
|
||||||
|
const params = new URLSearchParams({ |
||||||
|
newMember: 'true', |
||||||
|
fromInvite: 'true', |
||||||
|
existingMember: 'true', |
||||||
|
rCard: selectedRCard |
||||||
|
}); |
||||||
|
navigate(`/groups/${group.id}?${params.toString()}`); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleBack = () => { |
||||||
|
navigate(-1); |
||||||
|
}; |
||||||
|
|
||||||
|
if (isLoading) { |
||||||
|
return ( |
||||||
|
<Container maxWidth="md" sx={{ py: 4, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}> |
||||||
|
<Typography variant="h6" color="text.secondary"> |
||||||
|
Loading... |
||||||
|
</Typography> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (!group) { |
||||||
|
return ( |
||||||
|
<Container maxWidth="md" sx={{ py: 4, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}> |
||||||
|
<Typography variant="h6" color="text.secondary"> |
||||||
|
Group not found |
||||||
|
</Typography> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container maxWidth="md" sx={{ py: 4, minHeight: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> |
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
textAlign: 'center', |
||||||
|
background: `linear-gradient(135deg, ${alpha(theme.palette.primary.main, 0.05)} 0%, ${alpha(theme.palette.secondary.main, 0.05)} 100%)`, |
||||||
|
border: 1, |
||||||
|
borderColor: 'divider' |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* Back Button */} |
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, justifyContent: 'flex-start' }}> |
||||||
|
<Button |
||||||
|
onClick={handleBack} |
||||||
|
startIcon={<ArrowBack />} |
||||||
|
variant="text" |
||||||
|
sx={{ color: 'text.secondary' }} |
||||||
|
> |
||||||
|
Back |
||||||
|
</Button> |
||||||
|
</Box> |
||||||
|
|
||||||
|
{/* Header */} |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="h3" component="h1" gutterBottom> |
||||||
|
Join {group.name} |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Typography variant="h6" color="primary" gutterBottom> |
||||||
|
Choose your relationship type |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: '600px', mx: 'auto' }}> |
||||||
|
{inviterName} has invited you to join <strong>{group.name}</strong>.
|
||||||
|
Select how you'd like to connect with this group. This determines what personal information will be visible to group members. |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
|
||||||
|
{/* Group Info */} |
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, mb: 4 }}> |
||||||
|
<Avatar |
||||||
|
src={group.image} |
||||||
|
alt={group.name} |
||||||
|
sx={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
bgcolor: 'white', |
||||||
|
border: 1, |
||||||
|
borderColor: 'primary.main', |
||||||
|
color: 'primary.main' |
||||||
|
}} |
||||||
|
> |
||||||
|
{group.name.charAt(0)} |
||||||
|
</Avatar> |
||||||
|
<Box> |
||||||
|
<Typography variant="h5" sx={{ fontWeight: 600 }}> |
||||||
|
{group.name} |
||||||
|
</Typography> |
||||||
|
{group.isPrivate && ( |
||||||
|
<Chip label="Private Group" size="small" variant="outlined" sx={{ mt: 1 }} /> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
|
||||||
|
{/* rCard Selection */} |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}> |
||||||
|
<CheckCircle color="primary" /> |
||||||
|
Select Your Relationship Type |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: '1fr 1fr 1fr' },
|
||||||
|
gap: 2,
|
||||||
|
mt: 3, |
||||||
|
maxWidth: '800px', |
||||||
|
mx: 'auto' |
||||||
|
}}> |
||||||
|
{DEFAULT_RCARDS.map((rCard) => ( |
||||||
|
<Card |
||||||
|
key={rCard.name} |
||||||
|
onClick={() => handleRCardSelect(rCard.name)} |
||||||
|
sx={{ |
||||||
|
cursor: 'pointer', |
||||||
|
transition: 'all 0.2s ease-in-out', |
||||||
|
border: 2, |
||||||
|
borderColor: selectedRCard === rCard.name ? rCard.color : 'divider', |
||||||
|
backgroundColor: selectedRCard === rCard.name
|
||||||
|
? alpha(rCard.color || theme.palette.primary.main, 0.08) |
||||||
|
: 'background.paper', |
||||||
|
'&:hover': { |
||||||
|
borderColor: rCard.color, |
||||||
|
transform: 'translateY(-2px)', |
||||||
|
boxShadow: theme.shadows[4], |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
<CardContent sx={{ p: 3, textAlign: 'center' }}> |
||||||
|
<Avatar |
||||||
|
sx={{ |
||||||
|
bgcolor: rCard.color || theme.palette.primary.main, |
||||||
|
width: 48, |
||||||
|
height: 48, |
||||||
|
mx: 'auto', |
||||||
|
mb: 2, |
||||||
|
'& .MuiSvgIcon-root': { fontSize: 28 } |
||||||
|
}} |
||||||
|
> |
||||||
|
{getRCardIcon(rCard.icon || 'PersonOutline')} |
||||||
|
</Avatar> |
||||||
|
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 1 }}> |
||||||
|
{rCard.name} |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.875rem' }}> |
||||||
|
{rCard.description} |
||||||
|
</Typography> |
||||||
|
|
||||||
|
{selectedRCard === rCard.name && ( |
||||||
|
<CheckCircle
|
||||||
|
sx={{
|
||||||
|
color: rCard.color,
|
||||||
|
mt: 1, |
||||||
|
fontSize: 20
|
||||||
|
}}
|
||||||
|
/> |
||||||
|
)} |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
))} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
|
||||||
|
{/* Privacy Info */} |
||||||
|
<Box sx={{ mb: 4, p: 3, backgroundColor: alpha(theme.palette.info.main, 0.04), borderRadius: 2, border: 1, borderColor: alpha(theme.palette.info.main, 0.12) }}> |
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> |
||||||
|
<strong>Privacy Note:</strong> Your selected relationship type determines: |
||||||
|
</Typography> |
||||||
|
<Typography component="ul" variant="body2" color="text.secondary" sx={{ textAlign: 'left', maxWidth: '500px', mx: 'auto', pl: 3 }}> |
||||||
|
<li>What profile information is visible to group members</li> |
||||||
|
<li>How you appear in group member directories</li> |
||||||
|
<li>What data is shared for group activities and collaboration</li> |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
|
||||||
|
{/* Action Button */} |
||||||
|
<Button |
||||||
|
variant="contained" |
||||||
|
size="large" |
||||||
|
onClick={handleJoinGroup} |
||||||
|
disabled={!selectedRCard} |
||||||
|
sx={{
|
||||||
|
px: 4,
|
||||||
|
py: 1.5, |
||||||
|
borderRadius: 2, |
||||||
|
textTransform: 'none', |
||||||
|
fontSize: '1.1rem', |
||||||
|
minWidth: 200 |
||||||
|
}} |
||||||
|
> |
||||||
|
Join {group.name} |
||||||
|
</Button> |
||||||
|
</Paper> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default GroupJoinPage; |
Loading…
Reference in new issue