Major layout changes: - Remove Card wrappers and padding from all main pages for full-width content - Add proper spacing between tab bars and content (24px) - Fix mobile layout issues and button positioning Header improvements: - Replace avatar dropdown with direct Person icon link to My Account - Move Settings and Logout to My Account page as new tab and bottom button - Clean up unused menu components and imports New Group Info functionality: - Create GroupInfoPage with full group description, member list, and invite button - Make Info icon in GroupDetailPage navigate to new info page - Maintain all existing invite functionality (type name/email or select from network) - Add proper routing and contact selection flow Layout fixes: - Remove horizontal padding from main content areas - Fix vertical alignment issues in group headers on mobile - Add right margin to prevent buttons from touching screen edge - Simplify map view by removing duplicate frames and titles Code cleanup: - Remove unused imports (Paper, People, Container, formatDate) - Remove unused functions (handleInviteToGroup, renderFeedContent, renderActivityView) - Fix TypeScript errors and ensure clean build - Remove @ts-nocheck and improve type safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
parent
3cbed466a0
commit
052b383c0f
@ -0,0 +1,469 @@ |
||||
import { useState, useEffect } from 'react'; |
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; |
||||
import { getContactPhotoStyles } from '../utils/photoStyles'; |
||||
import { |
||||
Typography, |
||||
Box, |
||||
Avatar, |
||||
Button, |
||||
Card, |
||||
CardContent, |
||||
IconButton, |
||||
Chip, |
||||
List, |
||||
ListItem, |
||||
ListItemAvatar, |
||||
ListItemText, |
||||
alpha, |
||||
useTheme, |
||||
} from '@mui/material'; |
||||
import { |
||||
ArrowBack, |
||||
PersonAdd, |
||||
Public, |
||||
Lock, |
||||
} from '@mui/icons-material'; |
||||
import { dataService } from '../services/dataService'; |
||||
import type { Group } from '../types/group'; |
||||
import InviteForm, { type InviteFormData } from '../components/invite/InviteForm'; |
||||
|
||||
// Real NAO member data
|
||||
const getMockMembers = () => [ |
||||
{ |
||||
id: 'oli-sb', |
||||
name: 'Oliver Sylvester-Bradley', |
||||
avatar: '/images/Oli.jpg', |
||||
role: 'Admin', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365), // 1 year ago
|
||||
}, |
||||
{ |
||||
id: 'ruben-daniels', |
||||
name: 'Ruben Daniels', |
||||
avatar: '/images/Ruben.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 300), // 300 days ago
|
||||
}, |
||||
{ |
||||
id: 'margeigh-novotny', |
||||
name: 'Margeigh Novotny', |
||||
avatar: '/images/Margeigh.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 280), // 280 days ago
|
||||
}, |
||||
{ |
||||
id: 'alex-lion', |
||||
name: 'Alex Lion Yes!', |
||||
avatar: '/images/Alex.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 250), // 250 days ago
|
||||
}, |
||||
{ |
||||
id: 'day-waterbury', |
||||
name: 'Day Waterbury', |
||||
avatar: '/images/Day.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 200), // 200 days ago
|
||||
}, |
||||
{ |
||||
id: 'kevin-triplett', |
||||
name: 'Kevin Triplett', |
||||
avatar: '/images/Kevin.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 180), // 180 days ago
|
||||
}, |
||||
{ |
||||
id: 'tim-bansemer', |
||||
name: 'Tim Bansemer', |
||||
avatar: '/images/Tim.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 150), // 150 days ago
|
||||
}, |
||||
{ |
||||
id: 'aza-mafi', |
||||
name: 'Aza Mafi', |
||||
avatar: '/images/Aza.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 120), // 120 days ago
|
||||
}, |
||||
{ |
||||
id: 'duke-dorje', |
||||
name: 'Duke Dorje', |
||||
avatar: '/images/Duke.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 100), // 100 days ago
|
||||
}, |
||||
{ |
||||
id: 'david-thomson', |
||||
name: 'David Thomson', |
||||
avatar: '/images/David.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 80), // 80 days ago
|
||||
}, |
||||
{ |
||||
id: 'samuel-gbafa', |
||||
name: 'Samuel Gbafa', |
||||
avatar: '/images/Sam.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 60), // 60 days ago
|
||||
}, |
||||
{ |
||||
id: 'meena-seshamani', |
||||
name: 'Meena Seshamani', |
||||
avatar: '/images/Meena.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 40), // 40 days ago
|
||||
}, |
||||
{ |
||||
id: 'niko-bonnieure', |
||||
name: 'Niko Bonnieure', |
||||
avatar: '/images/Niko.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), // 30 days ago
|
||||
}, |
||||
{ |
||||
id: 'tree-willard', |
||||
name: 'Tree Willard', |
||||
avatar: '/images/Tree.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 20), // 20 days ago
|
||||
}, |
||||
{ |
||||
id: 'stephane-bancel', |
||||
name: 'Stephane Bancel', |
||||
avatar: '/images/Stephane.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 15), // 15 days ago
|
||||
}, |
||||
{ |
||||
id: 'joscha-raue', |
||||
name: 'Joscha Raue', |
||||
avatar: '/images/Joscha.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10), // 10 days ago
|
||||
}, |
||||
{ |
||||
id: 'drummond-reed', |
||||
name: 'Drummond Reed', |
||||
avatar: '/images/Drummond.jpg', |
||||
role: 'Member', |
||||
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), // 5 days ago
|
||||
}, |
||||
]; |
||||
|
||||
const GroupInfoPage = () => { |
||||
const { groupId } = useParams<{ groupId: string }>(); |
||||
const navigate = useNavigate(); |
||||
const [searchParams, setSearchParams] = useSearchParams(); |
||||
const theme = useTheme(); |
||||
|
||||
const [group, setGroup] = useState<Group | null>(null); |
||||
const [members] = useState(getMockMembers()); |
||||
const [isLoading, setIsLoading] = useState(true); |
||||
const [showInviteForm, setShowInviteForm] = useState(false); |
||||
const [selectedContact, setSelectedContact] = useState<{name: string; email: string} | undefined>(); |
||||
|
||||
useEffect(() => { |
||||
const loadGroupData = async () => { |
||||
if (!groupId) return; |
||||
|
||||
setIsLoading(true); |
||||
try { |
||||
const groupData = await dataService.getGroup(groupId); |
||||
setGroup(groupData || null); |
||||
|
||||
// Handle returning from contact selection
|
||||
const selectedContactName = searchParams.get('selectedContactName'); |
||||
const selectedContactEmail = searchParams.get('selectedContactEmail'); |
||||
if (selectedContactName && selectedContactEmail) { |
||||
setSelectedContact({ |
||||
name: selectedContactName, |
||||
email: selectedContactEmail |
||||
}); |
||||
setShowInviteForm(true); |
||||
|
||||
// Clean up selection parameters
|
||||
const newSearchParams = new URLSearchParams(searchParams); |
||||
newSearchParams.delete('selectedContactName'); |
||||
newSearchParams.delete('selectedContactEmail'); |
||||
setSearchParams(newSearchParams); |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to load group data:', error); |
||||
} finally { |
||||
setIsLoading(false); |
||||
} |
||||
}; |
||||
|
||||
loadGroupData(); |
||||
}, [groupId, searchParams, setSearchParams]); |
||||
|
||||
const handleBack = () => { |
||||
navigate(`/groups/${groupId}`); |
||||
}; |
||||
|
||||
const handleInviteToGroup = () => { |
||||
setShowInviteForm(true); |
||||
}; |
||||
|
||||
const handleInviteSubmit = (inviteData: InviteFormData) => { |
||||
console.log('Sending invite:', inviteData); |
||||
// TODO: Generate personalized invitation link and send email
|
||||
// For now, navigate to invite page with the data
|
||||
const inviteParams = new URLSearchParams({ |
||||
groupId: groupId!, |
||||
inviteeName: inviteData.inviteeName, |
||||
inviterName: inviteData.inviterName, |
||||
relationshipType: inviteData.relationshipType, |
||||
}); |
||||
|
||||
setShowInviteForm(false); |
||||
navigate(`/invite?${inviteParams.toString()}`); |
||||
}; |
||||
|
||||
const handleSelectFromNetwork = () => { |
||||
// Navigate to contacts page with selection mode and return context
|
||||
setShowInviteForm(false); |
||||
navigate(`/contacts?mode=select&returnTo=group-info&groupId=${groupId}`); |
||||
}; |
||||
|
||||
const formatDate = (date: Date) => { |
||||
return new Intl.DateTimeFormat('en-US', { |
||||
year: 'numeric', |
||||
month: 'short', |
||||
day: 'numeric' |
||||
}).format(date); |
||||
}; |
||||
|
||||
const getPrivacyIcon = (isPrivate: boolean) => { |
||||
return isPrivate ? ( |
||||
<Lock sx={{ fontSize: 20, color: '#ff9800' }} /> |
||||
) : ( |
||||
<Public sx={{ fontSize: 20, color: '#4caf50' }} /> |
||||
); |
||||
}; |
||||
|
||||
if (isLoading) { |
||||
return ( |
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}> |
||||
<Typography variant="h6" color="text.secondary"> |
||||
Loading group... |
||||
</Typography> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
if (!group) { |
||||
return ( |
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}> |
||||
<Typography variant="h6" color="text.secondary"> |
||||
Group not found |
||||
</Typography> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<Box sx={{
|
||||
height: '100%', |
||||
width: '100%', |
||||
maxWidth: { xs: '100vw', md: '100%' }, |
||||
overflow: 'hidden', |
||||
boxSizing: 'border-box', |
||||
pt: { xs: 1.5, md: 2 }, |
||||
pb: 0, |
||||
mx: { xs: 0, md: 'auto' } |
||||
}}> |
||||
{/* Header */} |
||||
<Box sx={{
|
||||
mb: { xs: 1.5, md: 2 }, |
||||
width: '100%', |
||||
overflow: 'hidden', |
||||
px: { xs: '10px', md: 0 } |
||||
}}> |
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: { xs: 1, md: 2 },
|
||||
mb: { xs: 2, md: 3 }, |
||||
width: '100%', |
||||
maxWidth: '100%', |
||||
overflow: 'hidden', |
||||
minWidth: 0 |
||||
}}> |
||||
<IconButton onClick={handleBack} size="large" sx={{ flexShrink: 0 }}> |
||||
<ArrowBack /> |
||||
</IconButton> |
||||
<Avatar |
||||
src={group.image} |
||||
alt={group.name} |
||||
sx={{
|
||||
width: { xs: 48, md: 64 },
|
||||
height: { xs: 48, md: 64 },
|
||||
bgcolor: 'white', |
||||
border: 1, |
||||
borderColor: 'primary.main', |
||||
color: 'primary.main', |
||||
flexShrink: 0 |
||||
}} |
||||
> |
||||
{group.name.charAt(0)} |
||||
</Avatar> |
||||
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: 'space-between' }}> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}> |
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700, |
||||
fontSize: { xs: '1.5rem', md: '2.125rem' }, |
||||
lineHeight: 1.2, |
||||
overflow: 'hidden', |
||||
textOverflow: 'ellipsis', |
||||
whiteSpace: 'nowrap' |
||||
}} |
||||
> |
||||
{group.name} |
||||
</Typography> |
||||
{getPrivacyIcon(group.isPrivate)} |
||||
</Box> |
||||
<Button |
||||
variant="contained" |
||||
startIcon={<PersonAdd />} |
||||
onClick={handleInviteToGroup} |
||||
sx={{
|
||||
borderRadius: 2, |
||||
px: { xs: 1.5, md: 3 }, |
||||
py: { xs: 0.5, md: 1 }, |
||||
fontSize: { xs: '0.75rem', md: '0.875rem' }, |
||||
flexShrink: 0, |
||||
minWidth: { xs: 'auto', md: 'auto' }, |
||||
mr: { xs: 0.5, md: 0 } // Add right margin on mobile
|
||||
}} |
||||
> |
||||
Invite |
||||
</Button> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
|
||||
{/* Content */} |
||||
<Box sx={{ px: { xs: '10px', md: 0 } }}> |
||||
{/* Description Card */} |
||||
<Card sx={{ mb: 3 }}> |
||||
<CardContent sx={{ p: 3 }}> |
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> |
||||
About this group |
||||
</Typography> |
||||
<Typography variant="body1" sx={{ mb: 3, lineHeight: 1.6 }}> |
||||
{group.description} |
||||
</Typography> |
||||
|
||||
{group.tags && group.tags.length > 0 && ( |
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 3 }}> |
||||
{group.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> |
||||
)} |
||||
</CardContent> |
||||
</Card> |
||||
|
||||
{/* Members List */} |
||||
<Card> |
||||
<CardContent sx={{ p: 3 }}> |
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> |
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}> |
||||
Members ({members.length}) |
||||
</Typography> |
||||
</Box> |
||||
|
||||
<List sx={{ width: '100%' }}> |
||||
{members.map((member, index) => ( |
||||
<ListItem |
||||
key={member.id} |
||||
sx={{ |
||||
px: 0, |
||||
py: 1, |
||||
borderBottom: index === members.length - 1 ? 'none' : '1px solid', |
||||
borderColor: 'divider', |
||||
}} |
||||
> |
||||
<ListItemAvatar> |
||||
<Avatar |
||||
src={member.avatar} |
||||
sx={{ |
||||
width: 48, |
||||
height: 48, |
||||
bgcolor: 'white', |
||||
border: 1, |
||||
borderColor: 'primary.main', |
||||
color: 'primary.main', |
||||
backgroundSize: member.avatar ? getContactPhotoStyles(member.name).backgroundSize : 'cover', |
||||
backgroundPosition: member.avatar ? getContactPhotoStyles(member.name).backgroundPosition : 'center', |
||||
}} |
||||
> |
||||
{!member.avatar && member.name.split(' ').map(n => n[0]).join('')} |
||||
</Avatar> |
||||
</ListItemAvatar> |
||||
<ListItemText |
||||
primary={ |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}> |
||||
{member.name} |
||||
</Typography> |
||||
{member.role === 'Admin' && ( |
||||
<Chip
|
||||
label="Admin"
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }} |
||||
/> |
||||
)} |
||||
</Box> |
||||
} |
||||
secondary={ |
||||
<Typography variant="body2" color="text.secondary"> |
||||
Joined {formatDate(member.joinedAt)} |
||||
</Typography> |
||||
} |
||||
/> |
||||
</ListItem> |
||||
))} |
||||
</List> |
||||
</CardContent> |
||||
</Card> |
||||
</Box> |
||||
|
||||
{/* Invite Form */} |
||||
{group && ( |
||||
<InviteForm |
||||
open={showInviteForm} |
||||
onClose={() => { |
||||
setShowInviteForm(false); |
||||
setSelectedContact(undefined); |
||||
}} |
||||
onSubmit={handleInviteSubmit} |
||||
onSelectFromNetwork={handleSelectFromNetwork} |
||||
group={group} |
||||
prefilledContact={selectedContact} |
||||
/> |
||||
)} |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default GroupInfoPage; |
@ -1,17 +1,46 @@ |
||||
import { Box, Typography, Container } from '@mui/material'; |
||||
import { Box, Typography } from '@mui/material'; |
||||
|
||||
const MessagesPage = () => { |
||||
return ( |
||||
<Container maxWidth="lg"> |
||||
<Box sx={{ py: 4 }}> |
||||
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}> |
||||
Messages |
||||
</Typography> |
||||
<Typography variant="body1" color="text.secondary"> |
||||
Your messages and conversations will appear here. |
||||
</Typography> |
||||
<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={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: { xs: 1, md: 1 }, |
||||
width: '100%', |
||||
overflow: 'hidden', |
||||
minWidth: 0 |
||||
}}> |
||||
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}> |
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
mb: { xs: 0, md: 0 }, |
||||
fontSize: { xs: '1.5rem', md: '2.125rem' }, |
||||
overflow: 'hidden', |
||||
textOverflow: 'ellipsis', |
||||
whiteSpace: 'nowrap' |
||||
}} |
||||
> |
||||
Messages |
||||
</Typography> |
||||
</Box> |
||||
</Box> |
||||
</Container> |
||||
<Typography variant="body1" color="text.secondary"> |
||||
Your messages and conversations will appear here. |
||||
</Typography> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
|
Loading…
Reference in new issue