Major GroupDetailPage layout improvements and fullscreen functionality

- Restructure desktop layout to use full screen width and height efficiently
- Change column proportions to 60% Activity Feed, 40% right column (60% Network, 40% Map)
- Fix viewport height constraints that prevented proper scrolling
- Add fullscreen functionality with proper 4-direction arrow icons for all three sections
- Center network graph properly within its container
- Fix map container sizing to prevent overflow
- Move + button to Activity Feed section where it belongs contextually
- Hide Relationship Categories sidebar except on Network tab (/contacts)
- Reduce main container padding to maximize content space
- Add proper fullscreen modes with exit functionality and dedicated layouts
- Improve mobile layout with natural scrolling for all sections
- Fix activity feed scrolling with contained scroll areas on desktop

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

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent 598d1505ab
commit 2c7482d06b
  1. 22
      src/components/account/RCardManagement.tsx
  2. 2
      src/components/account/RCardPrivacySettings.tsx
  3. 8
      src/components/layout/DashboardLayout.tsx
  4. 46
      src/pages/AccountPage.tsx
  5. 587
      src/pages/GroupDetailPage.tsx
  6. 118
      src/pages/GroupJoinPage.tsx

@ -39,6 +39,7 @@ interface RCardManagementProps {
onSave: (rCard: RCardWithPrivacy) => void; onSave: (rCard: RCardWithPrivacy) => void;
onDelete?: (rCardId: string) => void; onDelete?: (rCardId: string) => void;
editingRCard?: RCardWithPrivacy; editingRCard?: RCardWithPrivacy;
isGroupJoinContext?: boolean;
} }
const AVAILABLE_ICONS = [ const AVAILABLE_ICONS = [
@ -72,7 +73,8 @@ const RCardManagement = ({
onClose, onClose,
onSave, onSave,
onDelete, onDelete,
editingRCard editingRCard,
isGroupJoinContext = false
}: RCardManagementProps) => { }: RCardManagementProps) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editingRCard?.name || '', name: editingRCard?.name || '',
@ -167,7 +169,7 @@ const RCardManagement = ({
<DialogTitle> <DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6"> <Typography variant="h6">
{editingRCard ? 'Edit Relationship Card' : 'Create New Relationship Card'} {editingRCard ? 'Edit Profile Card' : 'Create New Profile Card'}
</Typography> </Typography>
<IconButton onClick={handleClose} size="small"> <IconButton onClick={handleClose} size="small">
<Close /> <Close />
@ -195,10 +197,10 @@ const RCardManagement = ({
{getIconComponent(formData.icon)} {getIconComponent(formData.icon)}
</Avatar> </Avatar>
<Typography variant="h6" sx={{ fontWeight: 600 }}> <Typography variant="h6" sx={{ fontWeight: 600 }}>
{formData.name || 'rCard Name'} {formData.name || 'Profile Card Name'}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{formData.description || 'rCard description'} {formData.description || 'Profile Card description'}
</Typography> </Typography>
{editingRCard?.isDefault && ( {editingRCard?.isDefault && (
<Chip label="Default" size="small" variant="outlined" sx={{ mt: 1 }} /> <Chip label="Default" size="small" variant="outlined" sx={{ mt: 1 }} />
@ -211,7 +213,7 @@ const RCardManagement = ({
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<TextField <TextField
fullWidth fullWidth
label="rCard Name" label="Profile Card Name"
value={formData.name} value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
error={!!errors.name} error={!!errors.name}
@ -296,8 +298,7 @@ const RCardManagement = ({
{editingRCard?.isDefault && ( {editingRCard?.isDefault && (
<Box sx={{ mt: 3, p: 2, bgcolor: 'warning.50', borderRadius: 1, border: 1, borderColor: 'warning.200' }}> <Box sx={{ mt: 3, p: 2, bgcolor: 'warning.50', borderRadius: 1, border: 1, borderColor: 'warning.200' }}>
<Typography variant="body2" color="warning.dark"> <Typography variant="body2" color="warning.dark">
<strong>Note:</strong> This is a default rCard. You can edit its name, description, and appearance, <strong>Note:</strong> This is a default profile card. You can edit its name, description, and settings to create a new profile card.
but it cannot be deleted.
</Typography> </Typography>
</Box> </Box>
)} )}
@ -313,7 +314,7 @@ const RCardManagement = ({
startIcon={<Delete />} startIcon={<Delete />}
onClick={handleDelete} onClick={handleDelete}
> >
Delete rCard Delete Profile Card
</Button> </Button>
)} )}
</Box> </Box>
@ -326,7 +327,10 @@ const RCardManagement = ({
onClick={handleSubmit} onClick={handleSubmit}
startIcon={editingRCard ? <Edit /> : undefined} startIcon={editingRCard ? <Edit /> : undefined}
> >
{editingRCard ? 'Save Changes' : 'Create rCard'} {isGroupJoinContext
? 'Save and use this profile'
: (editingRCard ? 'Save Changes' : 'Create Profile Card')
}
</Button> </Button>
</Box> </Box>
</Box> </Box>

@ -166,7 +166,7 @@ const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) =>
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
Configure what information is shared with contacts assigned to this rCard category. Configure what information is shared with contacts assigned to this profile.
</Typography> </Typography>
{/* Key Recovery & Trust Settings */} {/* Key Recovery & Trust Settings */}

@ -271,7 +271,8 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
{navItems.map((item) => renderNavItem(item))} {navItems.map((item) => renderNavItem(item))}
</List> </List>
{/* Relationship Categories */} {/* Relationship Categories - Only show on Network tab */}
{location.pathname === '/contacts' && (
<Box sx={{ px: 2, pb: 2, flexGrow: 1, overflow: 'auto' }}> <Box sx={{ px: 2, pb: 2, flexGrow: 1, overflow: 'auto' }}>
<Divider sx={{ mb: 2 }} /> <Divider sx={{ mb: 2 }} />
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600, color: 'text.secondary', px: 1 }}> <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600, color: 'text.secondary', px: 1 }}>
@ -337,6 +338,7 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
))} ))}
</Box> </Box>
</Box> </Box>
)}
</Box> </Box>
); );
@ -480,9 +482,9 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
<Toolbar /> <Toolbar />
<Box sx={{ <Box sx={{
pt: { xs: 0, md: 1 }, // Reduced top padding from 3 (24px) to 1 (8px) pt: { xs: 0, md: 1 }, // Reduced top padding from 3 (24px) to 1 (8px)
pr: { xs: 0, md: 3 }, pr: { xs: 0, md: 1.5 }, // Reduced right padding from 3 to 1.5
pb: { xs: 10, md: 3 }, pb: { xs: 10, md: 3 },
pl: { xs: 0, md: 3 }, pl: { xs: 0, md: 1.5 }, // Reduced left padding from 3 to 1.5
minHeight: 'calc(100vh - 64px)', minHeight: 'calc(100vh - 64px)',
overflow: 'visible', overflow: 'visible',
width: '100%', width: '100%',

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { import {
Typography, Typography,
Box, Box,
@ -57,10 +58,19 @@ const TabPanel = ({ children, value, index }: TabPanelProps) => {
const AccountPage = () => { const AccountPage = () => {
const theme = useTheme(); const theme = useTheme();
const [tabValue, setTabValue] = useState(0); const [searchParams] = useSearchParams();
// Initialize tab from URL parameter
const initialTab = parseInt(searchParams.get('tab') || '0', 10);
const [tabValue, setTabValue] = useState(initialTab);
const [rCards, setRCards] = useState<RCardWithPrivacy[]>([]); const [rCards, setRCards] = useState<RCardWithPrivacy[]>([]);
const [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null); const [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null);
const [showRCardManagement, setShowRCardManagement] = useState(false); const [showRCardManagement, setShowRCardManagement] = useState(false);
// Handle edit card from URL parameter
const editCardName = searchParams.get('editCard');
const returnToUrl = searchParams.get('returnTo');
const [editingRCard, setEditingRCard] = useState<RCardWithPrivacy | null>(null); const [editingRCard, setEditingRCard] = useState<RCardWithPrivacy | null>(null);
const [personhoodCredentials] = useState<PersonhoodCredentials>({ const [personhoodCredentials] = useState<PersonhoodCredentials>({
userId: 'user-123', userId: 'user-123',
@ -163,6 +173,18 @@ const AccountPage = () => {
setSelectedRCard(initialRCards[0]); setSelectedRCard(initialRCards[0]);
}, []); }, []);
// Handle editCard parameter when rCards are loaded
useEffect(() => {
if (editCardName && rCards.length > 0) {
const cardToEdit = rCards.find(card => card.name === editCardName);
if (cardToEdit) {
setSelectedRCard(cardToEdit);
setEditingRCard(cardToEdit);
setShowRCardManagement(true);
}
}
}, [editCardName, rCards]);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue); setTabValue(newValue);
}; };
@ -202,6 +224,23 @@ const AccountPage = () => {
setRCards(prev => [...prev, rCard]); setRCards(prev => [...prev, rCard]);
setSelectedRCard(rCard); setSelectedRCard(rCard);
} }
// If we have a return URL, navigate back with profile card info
if (returnToUrl) {
const decodedUrl = decodeURIComponent(returnToUrl);
const url = new URL(decodedUrl);
// Add the new profile card information to the URL
url.searchParams.set('customProfileCard', encodeURIComponent(JSON.stringify({
id: rCard.id,
name: rCard.name,
description: rCard.description,
color: rCard.color,
icon: rCard.icon
})));
window.location.href = url.toString();
}
}; };
const handleRCardDelete = (rCardId: string) => { const handleRCardDelete = (rCardId: string) => {
@ -404,7 +443,7 @@ const AccountPage = () => {
<CardContent> <CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}> <Typography variant="h6" sx={{ fontWeight: 600 }}>
Relationship Cards Profile Cards
</Typography> </Typography>
<IconButton size="small" color="primary" onClick={handleCreateRCard}> <IconButton size="small" color="primary" onClick={handleCreateRCard}>
<Add /> <Add />
@ -492,7 +531,7 @@ const AccountPage = () => {
<Card> <Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}> <CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary"> <Typography variant="h6" color="text.secondary">
Select an rCard to view privacy settings Select a profile card to view privacy settings
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@ -584,6 +623,7 @@ const AccountPage = () => {
onSave={handleRCardSave} onSave={handleRCardSave}
onDelete={handleRCardDelete} onDelete={handleRCardDelete}
editingRCard={editingRCard || undefined} editingRCard={editingRCard || undefined}
isGroupJoinContext={!!returnToUrl}
/> />
</Box> </Box>
); );

@ -36,12 +36,12 @@ import {
Description, Description,
TableChart, TableChart,
AutoAwesome, AutoAwesome,
AccountTree,
TrendingUp,
LocationOn,
ExpandMore, ExpandMore,
ExpandLess, ExpandLess,
Info, Info,
Dashboard,
Fullscreen,
FullscreenExit,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Group, GroupPost, GroupLink } from '../types/group'; import type { Group, GroupPost, GroupLink } from '../types/group';
@ -75,7 +75,7 @@ const GroupDetailPage = () => {
const [group, setGroup] = useState<Group | null>(null); const [group, setGroup] = useState<Group | null>(null);
const [posts, setPosts] = useState<GroupPost[]>([]); const [posts, setPosts] = useState<GroupPost[]>([]);
const [links, setLinks] = useState<GroupLink[]>([]); const [links, setLinks] = useState<GroupLink[]>([]);
const [tabValue, setTabValue] = useState(0); // Default to Network tab const [tabValue, setTabValue] = useState(0); // Default to combined view
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showAIAssistant, setShowAIAssistant] = useState(false); const [showAIAssistant, setShowAIAssistant] = useState(false);
const [showGroupTour, setShowGroupTour] = useState(false); const [showGroupTour, setShowGroupTour] = useState(false);
@ -89,6 +89,7 @@ const GroupDetailPage = () => {
const [selectedPersonFilter, setSelectedPersonFilter] = useState<string>('all'); const [selectedPersonFilter, setSelectedPersonFilter] = useState<string>('all');
const [selectedTopicFilter, setSelectedTopicFilter] = useState<string>('all'); const [selectedTopicFilter, setSelectedTopicFilter] = useState<string>('all');
const [expandedPosts, setExpandedPosts] = useState<Set<string>>(new Set()); const [expandedPosts, setExpandedPosts] = useState<Set<string>>(new Set());
const [fullscreenSection, setFullscreenSection] = useState<'activity' | 'network' | 'map' | null>(null);
useEffect(() => { useEffect(() => {
const loadGroupData = async () => { const loadGroupData = async () => {
@ -374,6 +375,14 @@ const GroupDetailPage = () => {
// TODO: Implement group post creation logic // TODO: Implement group post creation logic
}; };
const handleFullscreenToggle = (section: 'activity' | 'network' | 'map') => {
if (fullscreenSection === section) {
setFullscreenSection(null); // Exit fullscreen
} else {
setFullscreenSection(section); // Enter fullscreen
}
};
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
@ -661,12 +670,9 @@ const GroupDetailPage = () => {
} }
]; ];
const renderNetworkTab = () => { const renderCombinedView = () => {
const members = getMockMembers(); const members = getMockMembers();
return renderNetworkView(members);
};
const renderActivityTab = () => {
// Get all unique people and topics for filters // Get all unique people and topics for filters
const allPeople = ['all', ...Array.from(new Set(posts.map((p: any) => p.authorName)))]; const allPeople = ['all', ...Array.from(new Set(posts.map((p: any) => p.authorName)))];
const allTopics = ['all', ...Array.from(new Set(posts.map((p: any) => p.topic).filter(Boolean)))]; const allTopics = ['all', ...Array.from(new Set(posts.map((p: any) => p.topic).filter(Boolean)))];
@ -823,37 +829,171 @@ const GroupDetailPage = () => {
); );
}; };
// Handle fullscreen rendering
if (fullscreenSection) {
return ( return (
<Box>
{/* Filter Controls */}
<Box sx={{ <Box sx={{
mb: 3,
mt: { xs: 0, md: 0 },
pt: { xs: '16px', md: 0 },
width: '100%', width: '100%',
overflow: 'visible', // Change to visible so labels can show height: 'calc(100vh - 180px)',
backgroundColor: 'white', px: { xs: 2, md: 0 }
zIndex: 10, // Ensure it's above other elements
position: 'relative' // Required for z-index
}}> }}>
{fullscreenSection === 'activity' && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: 'primary.main' }}>
Activity Feed - Fullscreen
</Typography>
<IconButton onClick={() => handleFullscreenToggle('activity')}>
<FullscreenExit />
</IconButton>
</Box>
{/* Filter Controls */}
<Box sx={{ mb: 3, flexShrink: 0 }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<FormControl sx={{ minWidth: 140 }} size="small">
<InputLabel id="person-filter-label-fs">Filter by Person</InputLabel>
<Select
labelId="person-filter-label-fs"
value={selectedPersonFilter}
label="Filter by Person"
onChange={(e) => setSelectedPersonFilter(e.target.value)}
>
{allPeople.map(person => (
<MenuItem key={person} value={person}>
{person === 'all' ? 'All People' : person}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ minWidth: 140 }} size="small">
<InputLabel id="topic-filter-label-fs">Filter by Topic</InputLabel>
<Select
labelId="topic-filter-label-fs"
value={selectedTopicFilter}
label="Filter by Topic"
onChange={(e) => setSelectedTopicFilter(e.target.value)}
>
{allTopics.map(topic => (
<MenuItem key={topic} value={topic}>
{topic === 'all' ? 'All Topics' : topic}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</Box>
<Box sx={{ <Box sx={{
flex: 1,
overflowY: 'auto',
border: 1,
borderColor: 'divider',
borderRadius: 2,
backgroundColor: 'grey.50',
p: 2
}}>
{filteredPosts.length === 0 ? (
<Card sx={{ textAlign: 'center', py: 6 }}>
<CardContent>
<Typography variant="h6" color="text.secondary" gutterBottom>
No posts match your filters
</Typography>
<Typography variant="body2" color="text.secondary">
Try adjusting your filter selection to see more posts.
</Typography>
</CardContent>
</Card>
) : (
<Box>
{filteredPosts.map(renderPost)}
</Box>
)}
</Box>
</Box>
)}
{fullscreenSection === 'network' && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: 'primary.main' }}>
Network - Fullscreen
</Typography>
<IconButton onClick={() => handleFullscreenToggle('network')}>
<FullscreenExit />
</IconButton>
</Box>
<Box sx={{
flex: 1,
border: 1,
borderColor: 'divider',
borderRadius: 2,
overflow: 'hidden',
display: 'flex', display: 'flex',
gap: { xs: 1, md: 2 }, alignItems: 'center',
flexWrap: 'wrap', justifyContent: 'center'
alignItems: 'flex-start', // Change to flex-start so labels have room }}>
{renderNetworkView(members, '100%')}
</Box>
</Box>
)}
{fullscreenSection === 'map' && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: 'primary.main' }}>
Member Locations - Fullscreen
</Typography>
<IconButton onClick={() => handleFullscreenToggle('map')}>
<FullscreenExit />
</IconButton>
</Box>
<Box sx={{
flex: 1,
border: 1,
borderColor: 'divider',
borderRadius: 2,
overflow: 'hidden'
}}>
{renderMapView(members, true)}
</Box>
</Box>
)}
</Box>
);
}
return (
<Box sx={{
width: '100%', width: '100%',
position: 'relative', maxWidth: '100%',
zIndex: 10 px: { xs: 2, md: 0 }
}}> }}>
{/* Person Filter */} {/* Desktop Layout: Activity left, Network top-right, Map bottom-right */}
<FormControl sx={{ <Box sx={{
minWidth: { xs: 100, md: 200 }, display: { xs: 'none', md: 'flex' },
flex: { xs: 1, md: 'none' }, gap: 4,
maxWidth: { xs: 'calc(50% - 8px)', md: 'none' }, width: '100%',
width: { xs: 'calc(50% - 8px)', md: 'auto' }, maxWidth: '100%',
zIndex: 20, height: 'calc(100vh - 180px)' // Full height minus header and tabs
position: 'relative' }}>
}} size="small"> {/* Activity Feed - Left Column (60% width) */}
<Box sx={{
width: '60%',
display: 'flex',
flexDirection: 'column',
height: '100%',
position: 'relative' // Add position relative for the + button
}}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main', flexShrink: 0 }}>
Activity Feed
</Typography>
{/* Filter Controls */}
<Box sx={{ mb: 3, flexShrink: 0 }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<FormControl sx={{ minWidth: 140 }} size="small">
<InputLabel id="person-filter-label">Filter by Person</InputLabel> <InputLabel id="person-filter-label">Filter by Person</InputLabel>
<Select <Select
labelId="person-filter-label" labelId="person-filter-label"
@ -869,15 +1009,7 @@ const GroupDetailPage = () => {
</Select> </Select>
</FormControl> </FormControl>
{/* Topic Filter */} <FormControl sx={{ minWidth: 140 }} size="small">
<FormControl sx={{
minWidth: { xs: 100, md: 200 },
flex: { xs: 1, md: 'none' },
maxWidth: { xs: 'calc(50% - 8px)', md: 'none' },
width: { xs: 'calc(50% - 8px)', md: 'auto' },
zIndex: 20,
position: 'relative'
}} size="small">
<InputLabel id="topic-filter-label">Filter by Topic</InputLabel> <InputLabel id="topic-filter-label">Filter by Topic</InputLabel>
<Select <Select
labelId="topic-filter-label" labelId="topic-filter-label"
@ -895,9 +1027,19 @@ const GroupDetailPage = () => {
</Box> </Box>
</Box> </Box>
{/* Posts Feed */} {/* Posts Feed - Takes remaining height */}
<Box sx={{
flex: 1,
overflowY: 'auto',
pr: 1,
border: 1,
borderColor: 'divider',
borderRadius: 2,
backgroundColor: 'grey.50',
position: 'relative'
}}>
{filteredPosts.length === 0 ? ( {filteredPosts.length === 0 ? (
<Card sx={{ textAlign: 'center', py: 6 }}> <Card sx={{ textAlign: 'center', py: 6, m: 2 }}>
<CardContent> <CardContent>
<Typography variant="h6" color="text.secondary" gutterBottom> <Typography variant="h6" color="text.secondary" gutterBottom>
No posts match your filters No posts match your filters
@ -907,26 +1049,238 @@ const GroupDetailPage = () => {
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
) : (
<Box sx={{ p: 2 }}>
{filteredPosts.map(renderPost)}
</Box>
)}
{/* Fullscreen expand icon - positioned to not conflict with + button */}
<IconButton
size="small"
onClick={() => handleFullscreenToggle('activity')}
sx={{
position: 'absolute',
bottom: 8,
left: 8,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 1)',
},
zIndex: 10
}}
>
<Fullscreen fontSize="small" />
</IconButton>
</Box>
{/* + Button positioned within Activity Feed */}
<Box sx={{
position: 'absolute',
bottom: 24,
right: 24,
zIndex: 1000,
'& .MuiFab-root': {
position: 'relative !important',
bottom: 'auto !important',
right: 'auto !important'
}
}}>
<PostCreateButton
groupId={groupId}
onCreatePost={handleCreatePost}
/>
</Box>
</Box>
{/* Right Column (40% width) - Network and Map stacked */}
<Box sx={{
width: '40%',
display: 'flex',
flexDirection: 'column',
height: '100%',
gap: 3
}}>
{/* Network View - Takes 60% of the height */}
<Box sx={{ flex: 0.6, display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main', flexShrink: 0 }}>
Network
</Typography>
<Box sx={{
flex: 1,
border: 1,
borderColor: 'divider',
borderRadius: 2,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative'
}}>
{renderNetworkView(members, '100%')}
{/* Fullscreen expand icon */}
<IconButton
size="small"
onClick={() => handleFullscreenToggle('network')}
sx={{
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 1)',
},
zIndex: 10
}}
>
<Fullscreen fontSize="small" />
</IconButton>
</Box>
</Box>
{/* Map View - Takes 40% of the height */}
<Box sx={{ flex: 0.4, display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main', flexShrink: 0 }}>
Member Locations
</Typography>
<Box sx={{
flex: 1,
border: 1,
borderColor: 'divider',
borderRadius: 2,
overflow: 'hidden',
position: 'relative'
}}>
{renderMapView(members, true)}
{/* Fullscreen expand icon */}
<IconButton
size="small"
onClick={() => handleFullscreenToggle('map')}
sx={{
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 1)',
},
zIndex: 10
}}
>
<Fullscreen fontSize="small" />
</IconButton>
</Box>
</Box>
</Box>
</Box>
{/* Mobile Layout: Activity first, then Network, then Map - all naturally scrollable */}
<Box sx={{
display: { xs: 'block', md: 'none' }
}}>
{/* Activity View - First on mobile */}
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main' }}>
Activity Feed
</Typography>
{/* Filter Controls */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<FormControl sx={{ minWidth: 120, flex: 1 }} size="small">
<InputLabel id="person-filter-label-mobile">Person</InputLabel>
<Select
labelId="person-filter-label-mobile"
value={selectedPersonFilter}
label="Person"
onChange={(e) => setSelectedPersonFilter(e.target.value)}
>
{allPeople.map(person => (
<MenuItem key={person} value={person}>
{person === 'all' ? 'All' : person.split(' ')[0]}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ minWidth: 120, flex: 1 }} size="small">
<InputLabel id="topic-filter-label-mobile">Topic</InputLabel>
<Select
labelId="topic-filter-label-mobile"
value={selectedTopicFilter}
label="Topic"
onChange={(e) => setSelectedTopicFilter(e.target.value)}
>
{allTopics.map(topic => (
<MenuItem key={topic} value={topic}>
{topic === 'all' ? 'All' : topic}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</Box>
{/* Posts Feed - Natural height, no scroll container */}
<Box sx={{ mb: 4 }}>
{filteredPosts.length === 0 ? (
<Card sx={{ textAlign: 'center', py: 6 }}>
<CardContent>
<Typography variant="h6" color="text.secondary" gutterBottom>
No posts match your filters
</Typography>
<Typography variant="body2" color="text.secondary">
Try adjusting your filter selection.
</Typography>
</CardContent>
</Card>
) : ( ) : (
<Box> <Box>
{filteredPosts.map(renderPost)} {filteredPosts.map(renderPost)}
</Box> </Box>
)} )}
</Box> </Box>
{/* Network View - Fixed height on mobile */}
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main' }}>
Network
</Typography>
<Box sx={{
height: '300px',
overflow: 'hidden',
mb: 4,
border: 1,
borderColor: 'divider',
borderRadius: 2
}}>
{renderNetworkView(members, '300px')}
</Box>
{/* Map View - Fixed height on mobile */}
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: 'primary.main' }}>
Member Locations
</Typography>
<Box sx={{
height: '300px',
overflow: 'hidden',
mb: 4,
border: 1,
borderColor: 'divider',
borderRadius: 2
}}>
{renderMapView(members, true)}
</Box>
</Box>
</Box>
); );
}; };
const renderMapTab = () => {
const members = getMockMembers();
return renderMapView(members);
};
const renderNetworkView = (members: any[]) => { const renderNetworkView = (members: any[], height?: string) => {
// Shared position calculation function for perfect alignment // Shared position calculation function for perfect alignment
const getNodePosition = (member: any) => { const getNodePosition = (member: any) => {
const centerX = 290; // Moved 40px right from previous position (250 + 40 = 290) const centerX = 400; // Center of 800px viewBox
const centerY = 375; // Moved 25px up from previous position (400 - 25 = 375) const centerY = 400; // Center of 800px viewBox
const scale = 1.8; // Increased scale for better visibility const scale = 1.8; // Increased scale for better visibility
const x = centerX + member.position.x * scale; const x = centerX + member.position.x * scale;
@ -939,19 +1293,22 @@ const GroupDetailPage = () => {
<Box <Box
sx={{ sx={{
position: 'relative', position: 'relative',
height: 'calc(100vh - 57px)', // Full viewport height minus tabs header height: '100%',
backgroundColor: 'grey.50', backgroundColor: 'grey.50',
overflow: 'visible', overflow: 'hidden',
width: '100%', width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
m: 0, m: 0,
p: '5px 0 15px 0' // 5px top, 15px bottom padding p: 1
}} }}
> >
{/* SVG Network Graph */} {/* SVG Network Graph */}
<svg <svg
width="100%" width="95%"
height="calc(100vh - 77px)" // Full viewport height minus tabs header and padding height="95%"
style={{ display: 'block', paddingBottom: '60px' }} style={{ display: 'block' }}
viewBox="0 0 800 800" viewBox="0 0 800 800"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
> >
@ -1129,22 +1486,23 @@ const GroupDetailPage = () => {
}; };
const renderMapView = (members: any[]) => { const renderMapView = (members: any[], compact?: boolean) => {
const visibleMembers = members.filter(m => m.location?.visible); const visibleMembers = members.filter(m => m.location?.visible);
return ( return (
<Box> <Box sx={{ height: '100%', width: '100%' }}>
{/* World map view */} {/* World map view */}
<Box <Box
sx={{ sx={{
position: 'relative', position: 'relative',
height: 'calc(100vh - 200px)', height: '100%',
borderRadius: 2, width: '100%',
overflow: 'hidden', overflow: 'hidden',
backgroundImage: `url('/images/world-map.png')`, backgroundImage: `url('/images/world-map.png')`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat' backgroundRepeat: 'no-repeat',
backgroundColor: '#f5f5f5' // Light grey fallback to show container
}} }}
> >
{/* Member locations */} {/* Member locations */}
@ -1198,82 +1556,86 @@ const GroupDetailPage = () => {
<Avatar <Avatar
src={member.avatar} src={member.avatar}
sx={{ sx={{
width: 40, width: compact ? 32 : 40,
height: 40, height: compact ? 32 : 40,
border: 3, border: compact ? 2 : 3,
borderColor: member.id === 'current-user' ? 'primary.main' : 'success.main', borderColor: member.id === 'current-user' ? 'primary.main' : 'success.main',
boxShadow: member.id === 'current-user' boxShadow: member.id === 'current-user'
? `0 0 15px ${alpha(theme.palette.primary.main, 0.6)}` ? `0 0 ${compact ? 10 : 15}px ${alpha(theme.palette.primary.main, 0.6)}`
: `0 0 10px ${alpha(theme.palette.success.main, 0.4)}`, : `0 0 ${compact ? 6 : 10}px ${alpha(theme.palette.success.main, 0.4)}`,
fontSize: '0.9rem', fontSize: compact ? '0.75rem' : '0.9rem',
fontWeight: 600 fontWeight: 600,
backgroundSize: member.avatar ? getContactPhotoStyles(member.name).backgroundSize : 'cover',
backgroundPosition: member.avatar ? getContactPhotoStyles(member.name).backgroundPosition : 'center',
}} }}
> >
{member.initials} {member.initials}
</Avatar> </Avatar>
{/* Name tooltip */} {/* Name tooltip - smaller in compact mode */}
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: -40, top: compact ? -32 : -40,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
backgroundColor: 'white', backgroundColor: 'white',
px: 1, px: compact ? 0.5 : 1,
py: 0.5, py: compact ? 0.25 : 0.5,
borderRadius: 1, borderRadius: 1,
border: 1, border: 1,
borderColor: 'divider', borderColor: 'divider',
boxShadow: 2, boxShadow: compact ? 1 : 2,
fontSize: '0.75rem', fontSize: compact ? '0.65rem' : '0.75rem',
fontWeight: 500, fontWeight: 500,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
pointerEvents: 'none' pointerEvents: 'none'
}} }}
> >
{member.name} {compact ? member.name.split(' ')[0] : member.name}
</Box> </Box>
</Box> </Box>
); );
})} })}
{/* Map legend */} {/* Map legend - smaller in compact mode */}
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
bottom: 16, bottom: compact ? 8 : 16,
left: 16, left: compact ? 8 : 16,
backgroundColor: 'white', backgroundColor: 'white',
p: 2, p: compact ? 1 : 2,
borderRadius: 2, borderRadius: 2,
border: 1, border: 1,
borderColor: 'divider', borderColor: 'divider',
boxShadow: 2 boxShadow: 2
}} }}
> >
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 600 }}> <Typography variant={compact ? "caption" : "subtitle2"} gutterBottom sx={{ fontWeight: 600 }}>
Location Sharing Location Sharing
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: compact ? 0.5 : 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ width: 12, height: 12, backgroundColor: 'success.main', borderRadius: '50%' }} /> <Box sx={{ width: compact ? 8 : 12, height: compact ? 8 : 12, backgroundColor: 'success.main', borderRadius: '50%' }} />
<Typography variant="caption">{visibleMembers.length} members visible</Typography> <Typography variant="caption">{visibleMembers.length} visible</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ width: 12, height: 12, backgroundColor: 'grey.400', borderRadius: '50%' }} /> <Box sx={{ width: compact ? 8 : 12, height: compact ? 8 : 12, backgroundColor: 'grey.400', borderRadius: '50%' }} />
<Typography variant="caption">{members.length - visibleMembers.length} members private</Typography> <Typography variant="caption">{members.length - visibleMembers.length} private</Typography>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
{/* Privacy notice */} {/* Privacy notice - hide in compact mode */}
{!compact && (
<Box sx={{ mt: 2, p: 2, backgroundColor: alpha(theme.palette.info.main, 0.1), borderRadius: 2 }}> <Box sx={{ mt: 2, p: 2, backgroundColor: alpha(theme.palette.info.main, 0.1), borderRadius: 2 }}>
<Typography variant="body2" color="info.main" sx={{ fontWeight: 500 }}> <Typography variant="body2" color="info.main" sx={{ fontWeight: 500 }}>
📍 Location Privacy: Only members who have enabled location sharing are visible on the map. 📍 Location Privacy: Only members who have enabled location sharing are visible on the map.
</Typography> </Typography>
</Box> </Box>
)}
</Box> </Box>
); );
}; };
@ -1420,21 +1782,18 @@ const GroupDetailPage = () => {
return ( return (
<Box sx={{ <Box sx={{
height: '100%', // Normal height
width: '100%', width: '100%',
maxWidth: { xs: '100vw', md: '100%' }, maxWidth: { xs: '100vw', md: '100%' },
overflow: tabValue === 0 ? 'hidden' : 'hidden', // Prevent scrolling for network tab
boxSizing: 'border-box', boxSizing: 'border-box',
pt: { xs: 1.5, md: 2 }, // Reduced top padding mx: { xs: 0, md: 'auto' },
pb: 0, // No bottom padding minHeight: '100vh' // Ensure minimum height but allow scrolling
mx: { xs: 0, md: 'auto' }
}}> }}>
{/* Header - Hidden for network tab to maximize space */} {/* Header */}
<Box sx={{ <Box sx={{
mb: { xs: 1.5, md: 2 }, // Reduced bottom margin to match top pt: { xs: 1.5, md: 2 },
pb: { xs: 1, md: 1.5 },
width: '100%', width: '100%',
overflow: 'hidden', overflow: 'hidden'
display: 'block' // Always show header
}}> }}>
{/* Top row with back button, avatar, and title/description */} {/* Top row with back button, avatar, and title/description */}
<Box sx={{ <Box sx={{
@ -1525,13 +1884,10 @@ const GroupDetailPage = () => {
{/* Navigation Tabs */} {/* Navigation Tabs */}
<Box sx={{ <Box sx={{
width: tabValue === 0 ? '100vw' : { xs: 'calc(100% + 20px)', md: '100%' }, width: '100%',
maxWidth: tabValue === 0 ? '100vw' : { xs: 'calc(100vw - 0px)', md: '100%' },
overflow: 'hidden', overflow: 'hidden',
mx: tabValue === 0 ? 0 : { xs: '-10px', md: 0 }, // Full width for network tab
boxSizing: 'border-box', boxSizing: 'border-box',
backgroundColor: 'white', // Force white background backgroundColor: 'white'
height: 'auto' // Normal height
}}> }}>
<Tabs <Tabs
value={tabValue} value={tabValue}
@ -1562,9 +1918,7 @@ const GroupDetailPage = () => {
} }
}} }}
> >
<Tab icon={<AccountTree />} label="Network" /> <Tab icon={<Dashboard />} label="Overview" />
<Tab icon={<TrendingUp />} label="Activity" />
<Tab icon={<LocationOn />} label="Map" />
<Tab icon={<Chat />} label="Chat" /> <Tab icon={<Chat />} label="Chat" />
<Tab icon={<Folder />} label="Files" /> <Tab icon={<Folder />} label="Files" />
<Tab icon={<Link />} label="Links" /> <Tab icon={<Link />} label="Links" />
@ -1572,34 +1926,21 @@ const GroupDetailPage = () => {
{/* Tab Content */} {/* Tab Content */}
<Box sx={{ <Box sx={{
pt: 3, // Add top padding for space between tabs and content pt: 3,
px: 0, // No horizontal padding px: 0,
pb: 0, // No bottom padding pb: 4,
width: '100%', width: '100%',
maxWidth: '100%', maxWidth: '100%',
overflow: 'visible', // Let page scroll instead
boxSizing: 'border-box', boxSizing: 'border-box',
height: tabValue === 0 ? 'calc(100vh - 200px)' : 'auto', // Use viewport height for network tab only backgroundColor: 'white'
backgroundColor: 'white', // Force white background
minHeight: tabValue === 1 ? 'calc(100vh - 200px)' : 'auto' // Normal min height
}}> }}>
{tabValue === 0 && renderNetworkTab()} {tabValue === 0 && renderCombinedView()}
{tabValue === 1 && renderActivityTab()} {tabValue === 1 && renderChatTab()}
{tabValue === 2 && renderMapTab()} {tabValue === 2 && renderFilesTab()}
{tabValue === 3 && renderChatTab()} {tabValue === 3 && renderLinksTab()}
{tabValue === 4 && renderFilesTab()}
{tabValue === 5 && renderLinksTab()}
</Box> </Box>
</Box> </Box>
{/* Show Post Create Button only on Activity tab */}
{tabValue === 1 && (
<PostCreateButton
groupId={groupId}
onCreatePost={handleCreatePost}
/>
)}
{/* Group Tour */} {/* Group Tour */}
<GroupTour <GroupTour

@ -10,6 +10,7 @@ import {
Card, Card,
CardContent, CardContent,
Chip, Chip,
IconButton,
alpha, alpha,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
@ -23,7 +24,8 @@ import {
ArrowBack, ArrowBack,
CheckCircle, CheckCircle,
LocationOn, LocationOn,
Public Public,
Settings
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import { DEFAULT_RCARDS } from '../types/notification'; import { DEFAULT_RCARDS } from '../types/notification';
@ -34,6 +36,7 @@ const GroupJoinPage = () => {
const [selectedProfileCard, setSelectedProfileCard] = useState<string>(''); const [selectedProfileCard, setSelectedProfileCard] = useState<string>('');
const [inviterName, setInviterName] = useState<string>(''); const [inviterName, setInviterName] = useState<string>('');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [customProfileCard, setCustomProfileCard] = useState<any>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const theme = useTheme(); const theme = useTheme();
@ -42,16 +45,29 @@ const GroupJoinPage = () => {
const loadGroupData = async () => { const loadGroupData = async () => {
const groupId = searchParams.get('groupId'); const groupId = searchParams.get('groupId');
const inviter = searchParams.get('inviterName') || 'Someone'; const inviter = searchParams.get('inviterName') || 'Someone';
const customProfileCardParam = searchParams.get('customProfileCard');
// Debug logging // Debug logging
console.log('GroupJoinPage - URL Parameters:', { console.log('GroupJoinPage - URL Parameters:', {
groupId, groupId,
inviter, inviter,
customProfileCardParam,
allParams: Object.fromEntries(searchParams.entries()) allParams: Object.fromEntries(searchParams.entries())
}); });
setInviterName(inviter); setInviterName(inviter);
// Handle custom profile card
if (customProfileCardParam) {
try {
const customCard = JSON.parse(decodeURIComponent(customProfileCardParam));
setCustomProfileCard(customCard);
setSelectedProfileCard(customCard.name);
} catch (error) {
console.error('Failed to parse custom profile card:', error);
}
}
if (groupId) { if (groupId) {
try { try {
const groupData = await dataService.getGroup(groupId); const groupData = await dataService.getGroup(groupId);
@ -85,6 +101,15 @@ const GroupJoinPage = () => {
setSelectedProfileCard(profileCardName); setSelectedProfileCard(profileCardName);
}; };
const handleEditProfileCard = (profileCardName: string, event: React.MouseEvent) => {
// Prevent card selection when clicking edit button
event.stopPropagation();
// Navigate to profile card settings with return context
const currentUrl = encodeURIComponent(window.location.href);
navigate(`/account?tab=1&editCard=${encodeURIComponent(profileCardName)}&returnTo=${currentUrl}`);
};
const handleJoinGroup = () => { const handleJoinGroup = () => {
if (!selectedProfileCard || !group) return; if (!selectedProfileCard || !group) return;
@ -195,13 +220,71 @@ const GroupJoinPage = () => {
<Box sx={{ <Box sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: 'repeat(4, 1fr)' }, gridTemplateColumns: customProfileCard
? { xs: '1fr', sm: '1fr', md: '1fr' }
: { xs: '1fr', sm: '1fr 1fr', md: 'repeat(4, 1fr)' },
gap: 2, gap: 2,
mt: 3, mt: 3,
maxWidth: '1000px', maxWidth: customProfileCard ? '400px' : '1000px',
mx: 'auto' mx: 'auto',
justifyItems: customProfileCard ? 'center' : 'stretch'
}}> }}>
{DEFAULT_RCARDS.map((profileCard) => ( {customProfileCard ? (
// Show only the custom profile card
<Card
key={customProfileCard.name}
onClick={() => handleProfileCardSelect(customProfileCard.name)}
sx={{
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
border: 2,
borderColor: selectedProfileCard === customProfileCard.name ? customProfileCard.color : 'divider',
backgroundColor: selectedProfileCard === customProfileCard.name
? alpha(customProfileCard.color || theme.palette.primary.main, 0.08)
: 'background.paper',
'&:hover': {
borderColor: customProfileCard.color,
transform: 'translateY(-2px)',
boxShadow: theme.shadows[4],
},
}}
>
<CardContent sx={{ p: 3, textAlign: 'center', position: 'relative' }}>
<Avatar
sx={{
bgcolor: customProfileCard.color || theme.palette.primary.main,
width: 48,
height: 48,
mx: 'auto',
mb: 2,
'& .MuiSvgIcon-root': { fontSize: 28 }
}}
>
{getProfileCardIcon(customProfileCard.icon || 'PersonOutline')}
</Avatar>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 1 }}>
{customProfileCard.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.875rem' }}>
{customProfileCard.description}
</Typography>
{selectedProfileCard === customProfileCard.name && (
<CheckCircle
sx={{
color: customProfileCard.color,
mt: 1,
fontSize: 20
}}
/>
)}
</CardContent>
</Card>
) : (
// Show default profile cards
DEFAULT_RCARDS.map((profileCard) => (
<Card <Card
key={profileCard.name} key={profileCard.name}
onClick={() => handleProfileCardSelect(profileCard.name)} onClick={() => handleProfileCardSelect(profileCard.name)}
@ -220,7 +303,27 @@ const GroupJoinPage = () => {
}, },
}} }}
> >
<CardContent sx={{ p: 3, textAlign: 'center' }}> <CardContent sx={{ p: 3, textAlign: 'center', position: 'relative' }}>
{/* Edit Settings Button */}
<IconButton
size="small"
onClick={(e) => handleEditProfileCard(profileCard.name, e)}
sx={{
position: 'absolute',
top: 8,
right: 8,
bgcolor: 'background.paper',
boxShadow: 1,
'&:hover': {
bgcolor: 'grey.100',
transform: 'scale(1.1)',
},
transition: 'all 0.2s ease-in-out',
}}
>
<Settings sx={{ fontSize: 16 }} />
</IconButton>
<Avatar <Avatar
sx={{ sx={{
bgcolor: profileCard.color || theme.palette.primary.main, bgcolor: profileCard.color || theme.palette.primary.main,
@ -253,7 +356,8 @@ const GroupJoinPage = () => {
)} )}
</CardContent> </CardContent>
</Card> </Card>
))} ))
)}
</Box> </Box>
</Box> </Box>

Loading…
Cancel
Save