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. 118
      src/components/layout/DashboardLayout.tsx
  4. 46
      src/pages/AccountPage.tsx
  5. 679
      src/pages/GroupDetailPage.tsx
  6. 118
      src/pages/GroupJoinPage.tsx

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

@ -166,7 +166,7 @@ const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) =>
</Box>
<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>
{/* Key Recovery & Trust Settings */}

@ -271,72 +271,74 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
{navItems.map((item) => renderNavItem(item))}
</List>
{/* Relationship Categories */}
<Box sx={{ px: 2, pb: 2, flexGrow: 1, overflow: 'auto' }}>
<Divider sx={{ mb: 2 }} />
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600, color: 'text.secondary', px: 1 }}>
Relationship Categories
</Typography>
<Typography variant="caption" sx={{ mb: 2, color: 'text.secondary', px: 1, fontSize: '0.7rem', lineHeight: 1.2, display: 'block' }}>
Drag and drop contacts into a category to automatically set sharing permissions.
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1 }}>
{relationshipCategories.map((category) => (
<Box
key={category.id}
onDragOver={(e) => handleDragOver(e, category.id)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, category.id)}
sx={{
minHeight: 80,
border: 2,
borderColor: dragOverCategory === category.id ? category.color : 'divider',
borderStyle: dragOverCategory === category.id ? 'solid' : 'dashed',
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 1,
cursor: 'pointer',
backgroundColor: dragOverCategory === category.id ? `${category.color}10` : 'transparent',
transition: 'all 0.2s ease-in-out',
'&:hover': {
borderColor: category.color,
backgroundColor: `${category.color}08`,
},
}}
>
<Box sx={{ color: category.color, mb: 0.5 }}>
{category.icon}
</Box>
<Typography
variant="caption"
{/* Relationship Categories - Only show on Network tab */}
{location.pathname === '/contacts' && (
<Box sx={{ px: 2, pb: 2, flexGrow: 1, overflow: 'auto' }}>
<Divider sx={{ mb: 2 }} />
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600, color: 'text.secondary', px: 1 }}>
Relationship Categories
</Typography>
<Typography variant="caption" sx={{ mb: 2, color: 'text.secondary', px: 1, fontSize: '0.7rem', lineHeight: 1.2, display: 'block' }}>
Drag and drop contacts into a category to automatically set sharing permissions.
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1 }}>
{relationshipCategories.map((category) => (
<Box
key={category.id}
onDragOver={(e) => handleDragOver(e, category.id)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, category.id)}
sx={{
textAlign: 'center',
fontSize: '0.7rem',
fontWeight: 500,
lineHeight: 1.2
minHeight: 80,
border: 2,
borderColor: dragOverCategory === category.id ? category.color : 'divider',
borderStyle: dragOverCategory === category.id ? 'solid' : 'dashed',
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 1,
cursor: 'pointer',
backgroundColor: dragOverCategory === category.id ? `${category.color}10` : 'transparent',
transition: 'all 0.2s ease-in-out',
'&:hover': {
borderColor: category.color,
backgroundColor: `${category.color}08`,
},
}}
>
{category.name}
</Typography>
{category.count > 0 && (
<Box sx={{ color: category.color, mb: 0.5 }}>
{category.icon}
</Box>
<Typography
variant="caption"
sx={{
color: 'text.secondary',
fontSize: '0.6rem',
mt: 0.5
textAlign: 'center',
fontSize: '0.7rem',
fontWeight: 500,
lineHeight: 1.2
}}
>
{category.count}
{category.name}
</Typography>
)}
</Box>
))}
{category.count > 0 && (
<Typography
variant="caption"
sx={{
color: 'text.secondary',
fontSize: '0.6rem',
mt: 0.5
}}
>
{category.count}
</Typography>
)}
</Box>
))}
</Box>
</Box>
</Box>
)}
</Box>
);
@ -480,9 +482,9 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
<Toolbar />
<Box sx={{
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 },
pl: { xs: 0, md: 3 },
pl: { xs: 0, md: 1.5 }, // Reduced left padding from 3 to 1.5
minHeight: 'calc(100vh - 64px)',
overflow: 'visible',
width: '100%',

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Typography,
Box,
@ -57,10 +58,19 @@ const TabPanel = ({ children, value, index }: TabPanelProps) => {
const AccountPage = () => {
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 [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null);
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 [personhoodCredentials] = useState<PersonhoodCredentials>({
userId: 'user-123',
@ -163,6 +173,18 @@ const AccountPage = () => {
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) => {
setTabValue(newValue);
};
@ -202,6 +224,23 @@ const AccountPage = () => {
setRCards(prev => [...prev, 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) => {
@ -404,7 +443,7 @@ const AccountPage = () => {
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Relationship Cards
Profile Cards
</Typography>
<IconButton size="small" color="primary" onClick={handleCreateRCard}>
<Add />
@ -492,7 +531,7 @@ const AccountPage = () => {
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary">
Select an rCard to view privacy settings
Select a profile card to view privacy settings
</Typography>
</CardContent>
</Card>
@ -584,6 +623,7 @@ const AccountPage = () => {
onSave={handleRCardSave}
onDelete={handleRCardDelete}
editingRCard={editingRCard || undefined}
isGroupJoinContext={!!returnToUrl}
/>
</Box>
);

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

@ -10,6 +10,7 @@ import {
Card,
CardContent,
Chip,
IconButton,
alpha,
useTheme
} from '@mui/material';
@ -23,7 +24,8 @@ import {
ArrowBack,
CheckCircle,
LocationOn,
Public
Public,
Settings
} from '@mui/icons-material';
import { dataService } from '../services/dataService';
import { DEFAULT_RCARDS } from '../types/notification';
@ -34,6 +36,7 @@ const GroupJoinPage = () => {
const [selectedProfileCard, setSelectedProfileCard] = useState<string>('');
const [inviterName, setInviterName] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [customProfileCard, setCustomProfileCard] = useState<any>(null);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const theme = useTheme();
@ -42,16 +45,29 @@ const GroupJoinPage = () => {
const loadGroupData = async () => {
const groupId = searchParams.get('groupId');
const inviter = searchParams.get('inviterName') || 'Someone';
const customProfileCardParam = searchParams.get('customProfileCard');
// Debug logging
console.log('GroupJoinPage - URL Parameters:', {
groupId,
inviter,
customProfileCardParam,
allParams: Object.fromEntries(searchParams.entries())
});
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) {
try {
const groupData = await dataService.getGroup(groupId);
@ -85,6 +101,15 @@ const GroupJoinPage = () => {
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 = () => {
if (!selectedProfileCard || !group) return;
@ -195,13 +220,71 @@ const GroupJoinPage = () => {
<Box sx={{
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,
mt: 3,
maxWidth: '1000px',
mx: 'auto'
maxWidth: customProfileCard ? '400px' : '1000px',
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
key={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
sx={{
bgcolor: profileCard.color || theme.palette.primary.main,
@ -253,7 +356,8 @@ const GroupJoinPage = () => {
)}
</CardContent>
</Card>
))}
))
)}
</Box>
</Box>

Loading…
Cancel
Save