From 2c7482d06bc083a516287b8d599d2b79e1afc6e0 Mon Sep 17 00:00:00 2001 From: Claude Code Assistant Date: Wed, 30 Jul 2025 13:14:39 +0100 Subject: [PATCH] Major GroupDetailPage layout improvements and fullscreen functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/account/RCardManagement.tsx | 22 +- .../account/RCardPrivacySettings.tsx | 2 +- src/components/layout/DashboardLayout.tsx | 120 +-- src/pages/AccountPage.tsx | 46 +- src/pages/GroupDetailPage.tsx | 685 +++++++++++++----- src/pages/GroupJoinPage.tsx | 118 ++- 6 files changed, 742 insertions(+), 251 deletions(-) diff --git a/src/components/account/RCardManagement.tsx b/src/components/account/RCardManagement.tsx index ae7f921..3c52111 100644 --- a/src/components/account/RCardManagement.tsx +++ b/src/components/account/RCardManagement.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 = ({ - {editingRCard ? 'Edit Relationship Card' : 'Create New Relationship Card'} + {editingRCard ? 'Edit Profile Card' : 'Create New Profile Card'} @@ -195,10 +197,10 @@ const RCardManagement = ({ {getIconComponent(formData.icon)} - {formData.name || 'rCard Name'} + {formData.name || 'Profile Card Name'} - {formData.description || 'rCard description'} + {formData.description || 'Profile Card description'} {editingRCard?.isDefault && ( @@ -211,7 +213,7 @@ const RCardManagement = ({ setFormData(prev => ({ ...prev, name: e.target.value }))} error={!!errors.name} @@ -296,8 +298,7 @@ const RCardManagement = ({ {editingRCard?.isDefault && ( - Note: This is a default rCard. You can edit its name, description, and appearance, - but it cannot be deleted. + Note: This is a default profile card. You can edit its name, description, and settings to create a new profile card. )} @@ -313,7 +314,7 @@ const RCardManagement = ({ startIcon={} onClick={handleDelete} > - Delete rCard + Delete Profile Card )} @@ -326,7 +327,10 @@ const RCardManagement = ({ onClick={handleSubmit} startIcon={editingRCard ? : undefined} > - {editingRCard ? 'Save Changes' : 'Create rCard'} + {isGroupJoinContext + ? 'Save and use this profile' + : (editingRCard ? 'Save Changes' : 'Create Profile Card') + } diff --git a/src/components/account/RCardPrivacySettings.tsx b/src/components/account/RCardPrivacySettings.tsx index cad140a..5081e8f 100644 --- a/src/components/account/RCardPrivacySettings.tsx +++ b/src/components/account/RCardPrivacySettings.tsx @@ -166,7 +166,7 @@ const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) => - Configure what information is shared with contacts assigned to this rCard category. + Configure what information is shared with contacts assigned to this profile. {/* Key Recovery & Trust Settings */} diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index f9592f0..5ac5d6b 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -271,72 +271,74 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { {navItems.map((item) => renderNavItem(item))} - {/* Relationship Categories */} - - - - Relationship Categories - - - Drag and drop contacts into a category to automatically set sharing permissions. - - - {relationshipCategories.map((category) => ( - 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`, - }, - }} - > - - {category.icon} - - + + + Relationship Categories + + + Drag and drop contacts into a category to automatically set sharing permissions. + + + {relationshipCategories.map((category) => ( + 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`, + }, }} > - {category.name} - - {category.count > 0 && ( + + {category.icon} + - {category.count} + {category.name} - )} - - ))} + {category.count > 0 && ( + + {category.count} + + )} + + ))} + - + )} ); @@ -480,9 +482,9 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { { 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([]); const [selectedRCard, setSelectedRCard] = useState(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(null); const [personhoodCredentials] = useState({ 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 = () => { - Relationship Cards + Profile Cards @@ -492,7 +531,7 @@ const AccountPage = () => { - Select an rCard to view privacy settings + Select a profile card to view privacy settings @@ -584,6 +623,7 @@ const AccountPage = () => { onSave={handleRCardSave} onDelete={handleRCardDelete} editingRCard={editingRCard || undefined} + isGroupJoinContext={!!returnToUrl} /> ); diff --git a/src/pages/GroupDetailPage.tsx b/src/pages/GroupDetailPage.tsx index 072a268..8b639b4 100644 --- a/src/pages/GroupDetailPage.tsx +++ b/src/pages/GroupDetailPage.tsx @@ -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(null); const [posts, setPosts] = useState([]); const [links, setLinks] = useState([]); - 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('all'); const [selectedTopicFilter, setSelectedTopicFilter] = useState('all'); const [expandedPosts, setExpandedPosts] = useState>(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 = () => { ); }; - return ( - - {/* Filter Controls */} + // Handle fullscreen rendering + if (fullscreenSection) { + return ( + {fullscreenSection === 'activity' && ( + + + + Activity Feed - Fullscreen + + handleFullscreenToggle('activity')}> + + + + + {/* Filter Controls */} + + + + Filter by Person + + + + + Filter by Topic + + + + + + + {filteredPosts.length === 0 ? ( + + + + No posts match your filters + + + Try adjusting your filter selection to see more posts. + + + + ) : ( + + {filteredPosts.map(renderPost)} + + )} + + + )} + + {fullscreenSection === 'network' && ( + + + + Network - Fullscreen + + handleFullscreenToggle('network')}> + + + + + {renderNetworkView(members, '100%')} + + + )} + + {fullscreenSection === 'map' && ( + + + + Member Locations - Fullscreen + + handleFullscreenToggle('map')}> + + + + + {renderMapView(members, true)} + + + )} + + ); + } + + return ( + + {/* Desktop Layout: Activity left, Network top-right, Map bottom-right */} + + {/* Activity Feed - Left Column (60% width) */} - {/* Person Filter */} - - Filter by Person - - + + Activity Feed + - {/* Topic Filter */} - + + + Filter by Person + + + + + Filter by Topic + + + + + + {/* Posts Feed - Takes remaining height */} + - Filter by Topic - - + + + + + {/* + Button positioned within Activity Feed */} + + + - - - {/* Posts Feed */} - {filteredPosts.length === 0 ? ( - - - - No posts match your filters + + {/* Right Column (40% width) - Network and Map stacked */} + + {/* Network View - Takes 60% of the height */} + + + Network - - Try adjusting your filter selection to see more posts. + + {renderNetworkView(members, '100%')} + {/* Fullscreen expand icon */} + 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 + }} + > + + + + + + {/* Map View - Takes 40% of the height */} + + + Member Locations - - - ) : ( - - {filteredPosts.map(renderPost)} + + {renderMapView(members, true)} + {/* Fullscreen expand icon */} + 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 + }} + > + + + + - )} + + + {/* Mobile Layout: Activity first, then Network, then Map - all naturally scrollable */} + + {/* Activity View - First on mobile */} + + Activity Feed + + + {/* Filter Controls */} + + + + Person + + + + + Topic + + + + + + {/* Posts Feed - Natural height, no scroll container */} + + {filteredPosts.length === 0 ? ( + + + + No posts match your filters + + + Try adjusting your filter selection. + + + + ) : ( + + {filteredPosts.map(renderPost)} + + )} + + + {/* Network View - Fixed height on mobile */} + + Network + + + {renderNetworkView(members, '300px')} + + + {/* Map View - Fixed height on mobile */} + + Member Locations + + + {renderMapView(members, true)} + + ); }; - 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 = () => { {/* SVG Network Graph */} @@ -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 ( - + {/* World map view */} {/* Member locations */} @@ -1198,82 +1556,86 @@ const GroupDetailPage = () => { {member.initials} - {/* Name tooltip */} + {/* Name tooltip - smaller in compact mode */} - {member.name} + {compact ? member.name.split(' ')[0] : member.name} ); })} - {/* Map legend */} + {/* Map legend - smaller in compact mode */} - + Location Sharing - + - - {visibleMembers.length} members visible + + {visibleMembers.length} visible - - {members.length - visibleMembers.length} members private + + {members.length - visibleMembers.length} private - {/* Privacy notice */} - - - 📍 Location Privacy: Only members who have enabled location sharing are visible on the map. - - + {/* Privacy notice - hide in compact mode */} + {!compact && ( + + + 📍 Location Privacy: Only members who have enabled location sharing are visible on the map. + + + )} ); }; @@ -1420,21 +1782,18 @@ const GroupDetailPage = () => { return ( - {/* Header - Hidden for network tab to maximize space */} + {/* Header */} {/* Top row with back button, avatar, and title/description */} { {/* Navigation Tabs */} { } }} > - } label="Network" /> - } label="Activity" /> - } label="Map" /> + } label="Overview" /> } label="Chat" /> } label="Files" /> } label="Links" /> @@ -1572,33 +1926,20 @@ const GroupDetailPage = () => { {/* Tab Content */} - {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()} - - {/* Show Post Create Button only on Activity tab */} - {tabValue === 1 && ( - - )} {/* Group Tour */} diff --git a/src/pages/GroupJoinPage.tsx b/src/pages/GroupJoinPage.tsx index dce406e..54dd419 100644 --- a/src/pages/GroupJoinPage.tsx +++ b/src/pages/GroupJoinPage.tsx @@ -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(''); const [inviterName, setInviterName] = useState(''); const [isLoading, setIsLoading] = useState(true); + const [customProfileCard, setCustomProfileCard] = useState(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 = () => { - {DEFAULT_RCARDS.map((profileCard) => ( + {customProfileCard ? ( + // Show only the custom profile card + 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], + }, + }} + > + + + {getProfileCardIcon(customProfileCard.icon || 'PersonOutline')} + + + + {customProfileCard.name} + + + + {customProfileCard.description} + + + {selectedProfileCard === customProfileCard.name && ( + + )} + + + ) : ( + // Show default profile cards + DEFAULT_RCARDS.map((profileCard) => ( handleProfileCardSelect(profileCard.name)} @@ -220,7 +303,27 @@ const GroupJoinPage = () => { }, }} > - + + {/* Edit Settings Button */} + 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', + }} + > + + + { )} - ))} + )) + )}