Remove Card wrappers and implement full-width layout with Group Info page

Major layout changes:
- Remove Card wrappers and padding from all main pages for full-width content
- Add proper spacing between tab bars and content (24px)
- Fix mobile layout issues and button positioning

Header improvements:
- Replace avatar dropdown with direct Person icon link to My Account
- Move Settings and Logout to My Account page as new tab and bottom button
- Clean up unused menu components and imports

New Group Info functionality:
- Create GroupInfoPage with full group description, member list, and invite button
- Make Info icon in GroupDetailPage navigate to new info page
- Maintain all existing invite functionality (type name/email or select from network)
- Add proper routing and contact selection flow

Layout fixes:
- Remove horizontal padding from main content areas
- Fix vertical alignment issues in group headers on mobile
- Add right margin to prevent buttons from touching screen edge
- Simplify map view by removing duplicate frames and titles

Code cleanup:
- Remove unused imports (Paper, People, Container, formatDate)
- Remove unused functions (handleInviteToGroup, renderFeedContent, renderActivityView)
- Fix TypeScript errors and ensure clean build
- Remove @ts-nocheck and improve type safety

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

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent 3cbed466a0
commit 052b383c0f
  1. 46
      public/groups.json
  2. 2
      src/App.tsx
  3. 22
      src/components/account/PersonhoodCredentials.tsx
  4. 130
      src/components/layout/DashboardLayout.tsx
  5. 74
      src/pages/AccountPage.tsx
  6. 20
      src/pages/ContactListPage.tsx
  7. 4
      src/pages/FeedPage.tsx
  8. 387
      src/pages/GroupDetailPage.tsx
  9. 469
      src/pages/GroupInfoPage.tsx
  10. 239
      src/pages/GroupPage.tsx
  11. 39
      src/pages/MessagesPage.tsx
  12. 6
      src/pages/NotificationsPage.tsx
  13. 37
      src/services/dataService.ts
  14. 43
      src/theme/theme.ts
  15. 4
      src/types/group.ts

@ -10,7 +10,11 @@
"updatedAt": "2024-12-15T18:00:00Z",
"isPrivate": false,
"tags": ["nao", "genesis", "governance", "culture", "legal", "architecture"],
"image": "/naog1-butterfly-logo.svg"
"image": "/naog1-butterfly-logo.svg",
"latestPost": "Just uploaded the updated governance framework for review. Looking forward to everyone's feedback on the new proposal distribution mechanism.",
"latestPostAt": "2024-12-15T16:30:00Z",
"latestPostAuthor": "Oliver Sylvester-Bradley",
"unreadCount": 3
},
{
"id": "8",
@ -23,7 +27,11 @@
"updatedAt": "2024-12-20T14:30:00Z",
"isPrivate": true,
"tags": ["community", "gardening", "sustainability", "local", "education", "environment"],
"image": "/community-garden-logo.svg"
"image": "/community-garden-logo.svg",
"latestPost": "Winter planning meeting this Saturday at 10am! We'll be discussing the greenhouse expansion and seed ordering for spring planting. 🌱",
"latestPostAt": "2024-12-20T12:15:00Z",
"latestPostAuthor": "Tree Willard",
"unreadCount": 7
},
{
"id": "1",
@ -36,7 +44,10 @@
"updatedAt": "2023-10-15T14:30:00Z",
"isPrivate": false,
"tags": ["react", "frontend", "development"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/react/react-original.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/react/react-original.svg",
"latestPost": "Has anyone tried the new React 19 features yet? The compiler changes look really promising for performance optimization.",
"latestPostAt": "2023-10-15T12:45:00Z",
"latestPostAuthor": "Alex Lion Yes!"
},
{
"id": "2",
@ -49,7 +60,11 @@
"updatedAt": "2023-10-20T16:45:00Z",
"isPrivate": false,
"tags": ["product", "management", "strategy"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/trello/trello-plain.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/trello/trello-plain.svg",
"latestPost": "Sharing a great framework for prioritizing feature requests based on user impact and development effort. Link in comments!",
"latestPostAt": "2023-10-20T14:20:00Z",
"latestPostAuthor": "Ruben Daniels",
"unreadCount": 2
},
{
"id": "3",
@ -62,7 +77,11 @@
"updatedAt": "2023-09-25T13:15:00Z",
"isPrivate": false,
"tags": ["design", "ux", "ui"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/figma/figma-original.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/figma/figma-original.svg",
"latestPost": "Looking for feedback on this mobile-first navigation pattern. Does the bottom tab bar feel intuitive for enterprise users?",
"latestPostAt": "2023-09-25T11:30:00Z",
"latestPostAuthor": "Ariana Bahrami",
"unreadCount": 1
},
{
"id": "4",
@ -75,7 +94,10 @@
"updatedAt": "2023-08-30T10:20:00Z",
"isPrivate": true,
"tags": ["engineering", "leadership", "management"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg",
"latestPost": "Q3 performance reviews are coming up. Any recommendations for structuring technical growth conversations with senior engineers?",
"latestPostAt": "2023-08-30T09:45:00Z",
"latestPostAuthor": "Brad de Graf"
},
{
"id": "5",
@ -88,7 +110,11 @@
"updatedAt": "2023-07-20T09:30:00Z",
"isPrivate": false,
"tags": ["business", "strategy", "consulting"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linkedin/linkedin-original.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linkedin/linkedin-original.svg",
"latestPost": "Interesting case study on how a SaaS company pivoted their pricing model and increased LTV by 40%. Worth a read for B2B strategies.",
"latestPostAt": "2023-07-20T08:15:00Z",
"latestPostAuthor": "David Thomson",
"unreadCount": 4
},
{
"id": "6",
@ -101,6 +127,10 @@
"updatedAt": "2023-06-25T15:45:00Z",
"isPrivate": false,
"tags": ["marketing", "digital", "growth"],
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/google/google-original.svg"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/google/google-original.svg",
"latestPost": "The latest iOS privacy updates are affecting our attribution models. Anyone found good workarounds for measuring campaign effectiveness?",
"latestPostAt": "2023-06-25T13:30:00Z",
"latestPostAuthor": "Kevin Triplett",
"unreadCount": 5
}
]

@ -10,6 +10,7 @@ import ContactListPage from './pages/ContactListPage';
import ContactViewPage from './pages/ContactViewPage';
import GroupPage from './pages/GroupPage';
import GroupDetailPage from './pages/GroupDetailPage';
import GroupInfoPage from './pages/GroupInfoPage';
import InvitationPage from './pages/InvitationPage';
import OnboardingPage from './pages/OnboardingPage';
import FeedPage from './pages/FeedPage';
@ -44,6 +45,7 @@ function App() {
<Route path="/contacts/:id" element={<ContactViewPage />} />
<Route path="/groups" element={<GroupPage />} />
<Route path="/groups/:groupId" element={<GroupDetailPage />} />
<Route path="/groups/:groupId/info" element={<GroupInfoPage />} />
<Route path="/posts" element={<PostsOffersPage />} />
<Route path="/messages" element={<MessagesPage />} />
<Route path="/notifications" element={<NotificationsPage />} />

@ -42,12 +42,14 @@ interface PersonhoodCredentialsProps {
credentials: PersonhoodCredentials;
onGenerateQR: () => void;
onRefreshCredentials: () => void;
userName?: string;
}
const PersonhoodCredentialsComponent = ({
credentials,
onGenerateQR,
onRefreshCredentials
onRefreshCredentials,
userName = "User"
}: PersonhoodCredentialsProps) => {
const theme = useTheme();
const [showQRDialog, setShowQRDialog] = useState(false);
@ -225,10 +227,14 @@ const PersonhoodCredentialsComponent = ({
{/* QR Code Dialog */}
<Dialog open={showQRDialog} onClose={() => setShowQRDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6">My Verification QR Code</Typography>
<IconButton onClick={() => setShowQRDialog(false)} size="small">
<DialogTitle sx={{ pb: 1 }}>
<Box sx={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="h6" sx={{ textAlign: 'center' }}>{userName}</Typography>
<IconButton
onClick={() => setShowQRDialog(false)}
size="small"
sx={{ position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' }}
>
<Close />
</IconButton>
</Box>
@ -249,7 +255,7 @@ const PersonhoodCredentialsComponent = ({
Scan to Verify My Personhood
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Ask trusted contacts to scan this QR code to verify that you are a real human being. Each verification strengthens your credibility score.
Ask trusted contacts to scan this QR code to verify that you are a real human being. Each verification strengthens your identity.
</Typography>
<Chip
icon={<Info />}
@ -259,12 +265,12 @@ const PersonhoodCredentialsComponent = ({
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowQRDialog(false)}>Close</Button>
<DialogActions sx={{ justifyContent: 'center', pt: 1 }}>
<Button
variant="contained"
startIcon={<Share />}
onClick={onGenerateQR}
sx={{ fontSize: '0.75rem', py: 0.5, px: 2 }}
>
Share QR Code
</Button>

@ -8,7 +8,6 @@ import {
Toolbar,
List,
Typography,
Divider,
IconButton,
ListItem,
ListItemButton,
@ -16,15 +15,10 @@ import {
ListItemText,
useTheme,
useMediaQuery,
Avatar,
Menu,
MenuItem,
Badge,
Collapse,
} from '@mui/material';
import {
Settings,
Logout,
SearchRounded,
Groups,
Chat,
@ -38,6 +32,7 @@ import {
Business,
Work,
People,
AutoAwesome,
} from '@mui/icons-material';
import BottomNavigation from '../navigation/BottomNavigation';
import { notificationService } from '../../services/notificationService';
@ -69,7 +64,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileOpen, setMobileOpen] = useState(false);
const [profileMenuAnchor, setProfileMenuAnchor] = useState<null | HTMLElement>(null);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['Network']));
const [notificationSummary, setNotificationSummary] = useState<NotificationSummary>({
total: 0,
@ -116,13 +110,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
setMobileOpen(!mobileOpen);
};
const handleProfileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setProfileMenuAnchor(event.currentTarget);
};
const handleProfileMenuClose = () => {
setProfileMenuAnchor(null);
};
const handleNavigation = (path: string) => {
navigate(path);
@ -203,15 +190,20 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
}}
selected={isActive || isParentOfActive}
sx={{
mx: 1,
ml: level > 0 ? 3 : 1,
borderRadius: 2,
mx: 0,
ml: 0,
pl: level > 0 ? 4 : 3,
borderRadius: 0,
minHeight: 48,
borderRight: isActive || isParentOfActive ? 3 : 0,
borderRightColor: 'primary.main',
'&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
ml: level > 0 ? 3 : 1,
mr: 2,
ml: 0,
pl: level > 0 ? 4 : 3,
mr: 0,
borderRight: 0,
'&:hover': {
backgroundColor: 'primary.dark',
},
@ -256,13 +248,21 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
const drawerContent = (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ p: 3, borderBottom: 1, borderColor: 'divider', flexShrink: 0 }}>
<Box sx={{
height: 64,
display: 'flex',
alignItems: 'center',
px: 3,
borderBottom: 1,
borderColor: 'divider',
flexShrink: 0,
}}>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'primary.main' }}>
NAO
</Typography>
</Box>
<List sx={{ py: 2, flexShrink: 0 }}>
<List sx={{ flex: 1, py: 0, overflow: 'hidden' }}>
{navItems.map((item) => renderNavItem(item))}
</List>
@ -347,10 +347,21 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
borderBottom: 1,
borderColor: 'divider',
boxShadow: 'none',
borderRadius: '0 !important',
borderTopLeftRadius: '0 !important',
borderTopRightRadius: '0 !important',
borderBottomLeftRadius: '0 !important',
borderBottomRightRadius: '0 !important',
zIndex: theme.zIndex.drawer + 1,
}}
>
<Toolbar sx={{ justifyContent: 'space-between' }}>
<Toolbar sx={{
justifyContent: 'space-between',
minHeight: 64,
height: 64,
paddingTop: 0,
paddingBottom: 0
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography
variant="h6"
@ -367,6 +378,17 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="large"
color="inherit"
onClick={() => {
// AI Assistant functionality
console.log('AI Assistant clicked');
}}
sx={{ color: 'primary.main' }}
>
<AutoAwesome />
</IconButton>
<IconButton size="large" color="inherit">
<SearchRounded />
</IconButton>
@ -382,18 +404,11 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
<IconButton
size="large"
edge="end"
aria-label="account of current user"
aria-haspopup="true"
onClick={handleProfileMenuOpen}
aria-label="my account"
onClick={() => navigate('/account')}
color="inherit"
>
<Avatar
sx={{ width: 32, height: 32 }}
alt="Profile"
src="/static/images/avatar/2.jpg"
>
U
</Avatar>
<Person />
</IconButton>
</Box>
</Toolbar>
@ -459,10 +474,12 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
>
<Toolbar />
<Box sx={{
p: { xs: 0, md: 3 },
pt: { xs: 0, md: 1 }, // Reduced top padding from 3 (24px) to 1 (8px)
pr: { xs: 0, md: 3 },
pb: { xs: 10, md: 3 },
pl: { xs: 0, md: 3 },
minHeight: 'calc(100vh - 64px)',
overflow: 'visible',
pb: { xs: 10, md: 3 },
width: '100%',
maxWidth: '100%',
boxSizing: 'border-box',
@ -472,51 +489,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
</Box>
</Box>
<Menu
anchorEl={profileMenuAnchor}
open={Boolean(profileMenuAnchor)}
onClose={handleProfileMenuClose}
onClick={handleProfileMenuClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={() => { handleProfileMenuClose(); navigate('/account'); }}>
<Avatar /> My Profile
</MenuItem>
<MenuItem onClick={handleProfileMenuClose}>
<Settings sx={{ mr: 1 }} /> Settings
</MenuItem>
<Divider />
<MenuItem onClick={handleProfileMenuClose}>
<Logout sx={{ mr: 1 }} /> Logout
</MenuItem>
</Menu>
{/* Bottom Navigation for Mobile */}
{isMobile && <BottomNavigation />}

@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import {
Typography,
Box,
Paper,
Tabs,
Tab,
Grid,
@ -28,6 +27,8 @@ import {
Bookmarks,
Language,
Timeline,
Settings,
Logout,
} from '@mui/icons-material';
import { DEFAULT_RCARDS, DEFAULT_PRIVACY_SETTINGS } from '../types/notification';
import type { RCardWithPrivacy } from '../types/notification';
@ -47,7 +48,7 @@ interface TabPanelProps {
const TabPanel = ({ children, value, index }: TabPanelProps) => {
return (
<div hidden={value !== index}>
{value === index && <Box sx={{ p: { xs: 0, md: 3 } }}>{children}</Box>}
{value === index && <Box sx={{ pt: 3, px: 0, pb: 0 }}>{children}</Box>}
</div>
);
};
@ -246,15 +247,16 @@ const AccountPage = () => {
width: '100%',
maxWidth: { xs: '100vw', md: '100%' },
overflow: 'hidden',
boxSizing: 'border-box'
boxSizing: 'border-box',
p: { xs: '10px', md: 0 },
mx: { xs: 0, md: 'auto' }
}}>
{/* Header */}
<Box sx={{
mb: { xs: 2, md: 3 },
mb: { xs: 1, md: 1 },
width: '100%',
overflow: 'hidden',
minWidth: 0,
p: { xs: '10px', md: 0 }
minWidth: 0
}}>
<Typography
variant="h4"
@ -272,11 +274,10 @@ const AccountPage = () => {
</Box>
{/* Navigation Tabs */}
<Paper sx={{
mb: { xs: 0, md: 3 },
<Box sx={{
mb: { xs: 1, md: 3 },
width: '100%',
overflow: 'hidden',
borderRadius: { xs: 0, md: 1 },
boxSizing: 'border-box',
mx: { xs: 0, md: 'auto' }
}}>
@ -306,11 +307,12 @@ const AccountPage = () => {
<Tab icon={<Language />} label="My Webpage" />
<Tab icon={<Timeline />} label="My Stream" />
<Tab icon={<Bookmarks />} label="My Bookmarks" />
<Tab icon={<Settings />} label="Settings" />
</Tabs>
{/* Profile Tab */}
<TabPanel value={tabValue} index={0}>
<Box sx={{ p: { xs: '10px', md: 0 } }}>
<Box>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 4 }}>
<Card>
@ -379,6 +381,7 @@ const AccountPage = () => {
<PersonhoodCredentialsComponent
credentials={personhoodCredentials}
onGenerateQR={handleGenerateQR}
userName="John Doe"
onRefreshCredentials={handleRefreshCredentials}
/>
</Box>
@ -387,7 +390,7 @@ const AccountPage = () => {
{/* My Cards Tab */}
<TabPanel value={tabValue} index={1}>
<Box sx={{ p: { xs: '10px', md: 0 } }}>
<Box>
<Grid container spacing={3}>
{/* rCard List */}
<Grid size={{ xs: 12, md: 4 }}>
@ -495,7 +498,7 @@ const AccountPage = () => {
{/* My Webpage Tab */}
<TabPanel value={tabValue} index={2}>
<Box sx={{ p: { xs: '10px', md: 0 } }}>
<Box>
<Card>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
@ -514,18 +517,59 @@ const AccountPage = () => {
{/* My Stream Tab */}
<TabPanel value={tabValue} index={3}>
<Box sx={{ p: { xs: '10px', md: 0 } }}>
<Box>
<MyHomePage />
</Box>
</TabPanel>
{/* My Bookmarks Tab */}
<TabPanel value={tabValue} index={4}>
<Box sx={{ p: { xs: '10px', md: 0 } }}>
<Box>
<MyCollectionPage />
</Box>
</TabPanel>
</Paper>
{/* Settings Tab */}
<TabPanel value={tabValue} index={5}>
<Box>
<Card>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
Settings
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Account settings and preferences
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Coming soon...
</Typography>
</CardContent>
</Card>
</Box>
</TabPanel>
</Box>
{/* Logout Button - Always visible at bottom */}
<Box sx={{ mt: 3, mb: 2, textAlign: 'center' }}>
<Button
variant="outlined"
startIcon={<Logout />}
onClick={() => {
// Handle logout
console.log('Logout clicked');
}}
sx={{
color: 'error.main',
borderColor: 'error.main',
'&:hover': {
borderColor: 'error.dark',
backgroundColor: 'error.light'
}
}}
>
Logout
</Button>
</Box>
{/* rCard Management Dialog */}
<RCardManagement

@ -180,6 +180,9 @@ const ContactListPage = () => {
if (returnTo === 'group-invite' && groupId) {
// Navigate back to group page with selected contact data
navigate(`/groups/${groupId}?selectedContactName=${encodeURIComponent(contact.name)}&selectedContactEmail=${encodeURIComponent(contact.email)}`);
} else if (returnTo === 'group-info' && groupId) {
// Navigate back to group info page with selected contact data
navigate(`/groups/${groupId}/info?selectedContactName=${encodeURIComponent(contact.name)}&selectedContactEmail=${encodeURIComponent(contact.email)}`);
}
};
@ -356,11 +359,11 @@ const ContactListPage = () => {
}}>
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: { xs: 'stretch', md: 'center' },
mb: { xs: 2, md: 3 },
gap: { xs: 2, md: 0 },
alignItems: 'center',
mb: { xs: 1, md: 1 },
gap: 1,
width: '100%',
overflow: 'hidden',
minWidth: 0
@ -371,7 +374,7 @@ const ContactListPage = () => {
component="h1"
sx={{
fontWeight: 700,
mb: 1,
mb: { xs: 0, md: 0 },
fontSize: { xs: '1.5rem', md: '2.125rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
@ -453,13 +456,12 @@ const ContactListPage = () => {
)}
</Box>
<Card sx={{
<Box sx={{
mb: 3,
width: { xs: 'calc(100% + 20px)', md: '100%' },
maxWidth: { xs: 'calc(100vw - 0px)', md: '100%' },
overflow: 'hidden',
mx: { xs: '-10px', md: 0 }, // Extend to edges on mobile
borderRadius: { xs: 0, md: 1 }, // Remove border radius on mobile
boxSizing: 'border-box'
}}>
<Tabs
@ -491,7 +493,7 @@ const ContactListPage = () => {
</Tabs>
{tabValue === 0 && (
<Box sx={{ p: { xs: '10px', md: 3 } }}>
<Box sx={{ pt: 3, px: 0, pb: 0 }}>
<TextField
fullWidth
placeholder="Search contacts..."
@ -1079,7 +1081,7 @@ const ContactListPage = () => {
)}
</Box>
)}
</Card>
</Box>
</Box>
);
};

@ -12,7 +12,7 @@ const FeedPage = () => {
mx: { xs: 0, md: 'auto' }
}}>
<Box sx={{
mb: { xs: 2, md: 3 },
mb: { xs: 1, md: 1 },
width: '100%',
overflow: 'hidden',
minWidth: 0
@ -22,7 +22,7 @@ const FeedPage = () => {
component="h1"
sx={{
fontWeight: 700,
mb: 1,
mb: { xs: 0, md: 0 },
fontSize: { xs: '1.5rem', md: '2.125rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',

@ -1,4 +1,3 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { getContactPhotoStyles } from '../utils/photoStyles';
@ -13,7 +12,6 @@ import {
IconButton,
Button,
Chip,
Badge,
alpha,
useTheme,
Dialog,
@ -28,7 +26,6 @@ import {
} from '@mui/material';
import {
ArrowBack,
People,
Chat,
Folder,
Link,
@ -38,13 +35,13 @@ import {
MoreVert,
Description,
TableChart,
PersonAdd,
AutoAwesome,
AccountTree,
TrendingUp,
LocationOn,
ExpandMore,
ExpandLess,
Info,
} from '@mui/icons-material';
import { dataService } from '../services/dataService';
import type { Group, GroupPost, GroupLink } from '../types/group';
@ -280,9 +277,6 @@ const GroupDetailPage = () => {
navigate('/groups');
};
const handleInviteToGroup = () => {
setShowInviteForm(true);
};
const handleInviteSubmit = (inviteData: InviteFormData) => {
console.log('Sending invite:', inviteData);
@ -386,78 +380,6 @@ const GroupDetailPage = () => {
}).format(date);
};
const renderFeedTab = () => (
<Box sx={{ mt: 3 }}>
{posts.length === 0 ? (
<Card sx={{ textAlign: 'center', py: 8 }}>
<CardContent>
<Typography variant="h6" color="text.secondary" gutterBottom>
No posts yet
</Typography>
<Typography variant="body2" color="text.secondary">
Be the first to share something with the group!
</Typography>
</CardContent>
</Card>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{posts.map((post) => (
<Card key={post.id} sx={{ border: 1, borderColor: 'divider' }}>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Avatar src={post.authorAvatar} alt={post.authorName} sx={{ width: 40, height: 40 }}>
{post.authorName.charAt(0)}
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{post.authorName}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(post.createdAt)}
</Typography>
</Box>
<IconButton size="small">
<MoreVert />
</IconButton>
</Box>
<Typography variant="body1" sx={{ mb: 3 }}>
{post.content}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
startIcon={<ThumbUp />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
{post.likes}
</Button>
<Button
startIcon={<Comment />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
{post.comments}
</Button>
<Button
startIcon={<Share />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
Share
</Button>
</Box>
</CardContent>
</Card>
))}
</Box>
)}
</Box>
);
// Real NAO member data with relationships, activities, and locations
const getMockMembers = () => [
@ -741,8 +663,6 @@ const GroupDetailPage = () => {
};
const renderActivityTab = () => {
const members = getMockMembers();
// 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)))];
@ -994,89 +914,9 @@ const GroupDetailPage = () => {
const renderMapTab = () => {
const members = getMockMembers();
return (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LocationOn color="primary" />
Member Locations
</Typography>
<Card>
<CardContent sx={{ p: 3, minHeight: 500 }}>
{renderMapView(members)}
</CardContent>
</Card>
</Box>
);
return renderMapView(members);
};
const renderFeedContent = () => (
posts.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h6" color="text.secondary" gutterBottom>
No posts yet
</Typography>
<Typography variant="body2" color="text.secondary">
Be the first to share something with the group!
</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{posts.map((post) => (
<Card key={post.id} sx={{ border: 1, borderColor: 'divider' }}>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Avatar src={post.authorAvatar} alt={post.authorName} sx={{ width: 40, height: 40 }}>
{post.authorName.charAt(0)}
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{post.authorName}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(post.createdAt)}
</Typography>
</Box>
<IconButton size="small">
<MoreVert />
</IconButton>
</Box>
<Typography variant="body1" sx={{ mb: 3 }}>
{post.content}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
startIcon={<ThumbUp />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
{post.likes}
</Button>
<Button
startIcon={<Comment />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
{post.comments}
</Button>
<Button
startIcon={<Share />}
size="small"
variant="text"
sx={{ color: 'text.secondary' }}
>
Share
</Button>
</Box>
</CardContent>
</Card>
))}
</Box>
)
);
const renderNetworkView = (members: any[]) => {
// Shared position calculation function for perfect alignment
@ -1284,112 +1124,12 @@ const GroupDetailPage = () => {
);
};
const renderActivityView = (members: any[]) => {
// Get all unique topics and their activity leaders
const topicAnalysis: any = {};
members.forEach(member => {
member.activities?.forEach((activity: any) => {
if (!topicAnalysis[activity.topic]) {
topicAnalysis[activity.topic] = [];
}
topicAnalysis[activity.topic].push({
member,
count: activity.count,
lastActive: activity.lastActive
});
});
});
// Sort topics by total activity
const sortedTopics = Object.entries(topicAnalysis)
.map(([topic, activities]: [string, any]) => ({
topic,
activities: activities.sort((a: any, b: any) => b.count - a.count),
totalActivity: activities.reduce((sum: number, a: any) => sum + a.count, 0)
}))
.sort((a, b) => b.totalActivity - a.totalActivity);
return (
<Box>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendingUp color="primary" />
Topic Activity Leaders
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, mt: 3 }}>
{sortedTopics.map(({ topic, activities, totalActivity }) => (
<Card key={topic} variant="outlined">
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{topic}
</Typography>
<Chip
label={`${totalActivity} total posts`}
size="small"
color="primary"
variant="outlined"
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{activities.slice(0, 3).map((activity: any, index: number) => (
<Box key={activity.member.id} sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="body2" sx={{ minWidth: 20, fontWeight: 600, color: 'primary.main' }}>
#{index + 1}
</Typography>
<Avatar
src={activity.member.avatar}
sx={{ width: 32, height: 32, fontSize: '0.875rem' }}
>
{activity.member.initials}
</Avatar>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{activity.member.name}
</Typography>
<Typography variant="caption" color="text.secondary">
Last active: {activity.lastActive}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{activity.count} posts
</Typography>
<Box
sx={{
width: Math.max(activity.count * 2, 20),
height: 6,
backgroundColor: alpha(theme.palette.success.main, 0.3 + (index === 0 ? 0.4 : index === 1 ? 0.2 : 0.1)),
borderRadius: 3
}}
/>
</Box>
</Box>
))}
</Box>
</CardContent>
</Card>
))}
</Box>
</Box>
);
};
const renderMapView = (members: any[]) => {
const visibleMembers = members.filter(m => m.location?.visible);
return (
<Box>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LocationOn color="primary" />
Anyville Community Garden Network
</Typography>
{/* World map view */}
<Box
sx={{
@ -1681,12 +1421,13 @@ const GroupDetailPage = () => {
maxWidth: { xs: '100vw', md: '100%' },
overflow: tabValue === 0 ? 'hidden' : 'hidden', // Prevent scrolling for network tab
boxSizing: 'border-box',
p: { xs: '10px', md: 0 }, // Normal padding
pt: { xs: 1.5, md: 2 }, // Reduced top padding
pb: 0, // No bottom padding
mx: { xs: 0, md: 'auto' }
}}>
{/* Header - Hidden for network tab to maximize space */}
<Box sx={{
mb: tabValue === 0 ? 0 : 3,
mb: { xs: 1.5, md: 2 }, // Reduced bottom margin to match top
width: '100%',
overflow: 'hidden',
display: 'block' // Always show header
@ -1709,8 +1450,8 @@ const GroupDetailPage = () => {
src={group.image}
alt={group.name}
sx={{
width: { xs: 48, md: 64 },
height: { xs: 48, md: 64 },
width: { xs: 40, md: 56 },
height: { xs: 40, md: 56 },
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
@ -1735,41 +1476,6 @@ const GroupDetailPage = () => {
>
{group.name}
</Typography>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 1, md: 2 },
mt: 1,
flexWrap: 'wrap',
minWidth: 0
}}>
<Badge badgeContent={group.memberCount} color="primary">
<People sx={{ fontSize: 20, color: 'text.secondary' }} />
</Badge>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 0 }}>
{group.memberCount} members
</Typography>
{group.isPrivate && (
<Chip label="Private" size="small" variant="outlined" />
)}
</Box>
{group.description && (
<Typography
variant="body1"
color="text.secondary"
sx={{
mt: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: { xs: 2, md: 3 },
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word'
}}
>
{group.description}
</Typography>
)}
</Box>
{/* Desktop buttons */}
<Box sx={{
@ -1778,74 +1484,47 @@ const GroupDetailPage = () => {
alignItems: 'flex-start',
flexShrink: 0
}}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => handleStartAIAssistant()}
sx={{ borderRadius: 2 }}
>
AI Assistant
</Button>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInviteToGroup}
sx={{ borderRadius: 2 }}
<IconButton
onClick={() => navigate(`/groups/${groupId}/info`)}
sx={{
border: 1,
borderColor: 'grey.400',
borderRadius: 2
}}
>
Invite to Group
</Button>
</Box>
<Info />
</IconButton>
</Box>
{/* Mobile buttons row */}
{/* Mobile: Info icon in header */}
<Box sx={{
display: { xs: 'flex', md: 'none' },
gap: 1,
justifyContent: 'space-between',
mt: 2,
width: '100%',
maxWidth: '100%',
overflow: 'hidden'
alignItems: 'center',
flexShrink: 0
}}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => handleStartAIAssistant()}
sx={{
borderRadius: 2,
flex: 1,
minWidth: 0,
fontSize: '0.7rem',
px: 0.5,
maxWidth: 'calc(50% - 4px)'
}}
>
AI Assistant
</Button>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInviteToGroup}
<IconButton
onClick={() => navigate(`/groups/${groupId}/info`)}
sx={{
border: 1,
borderColor: 'grey.400',
borderRadius: 2,
flex: 1,
minWidth: 0,
fontSize: '0.7rem',
px: 0.5,
maxWidth: 'calc(50% - 4px)'
width: 40,
height: 40,
mr: 1 // Add right margin to prevent smashing against edge
}}
>
Invite
</Button>
<Info sx={{ fontSize: 20 }} />
</IconButton>
</Box>
</Box>
</Box>
{/* Navigation Tabs */}
<Card sx={{
<Box sx={{
width: tabValue === 0 ? '100vw' : { xs: 'calc(100% + 20px)', md: '100%' },
maxWidth: tabValue === 0 ? '100vw' : { xs: 'calc(100vw - 0px)', md: '100%' },
overflow: 'hidden',
mx: tabValue === 0 ? 0 : { xs: '-10px', md: 0 }, // Full width for network tab
borderRadius: tabValue === 0 ? 0 : { xs: 0, md: 1 }, // No border radius for network tab
boxSizing: 'border-box',
backgroundColor: 'white', // Force white background
height: 'auto' // Normal height
@ -1889,7 +1568,9 @@ const GroupDetailPage = () => {
{/* Tab Content */}
<Box sx={{
p: tabValue === 0 ? 0 : { xs: '10px', md: 3 }, // Mobile: 10px padding, Desktop: 24px padding
pt: 3, // Add top padding for space between tabs and content
px: 0, // No horizontal padding
pb: 0, // No bottom padding
width: '100%',
maxWidth: '100%',
overflow: 'visible', // Let page scroll instead
@ -1905,7 +1586,7 @@ const GroupDetailPage = () => {
{tabValue === 4 && renderFilesTab()}
{tabValue === 5 && renderLinksTab()}
</Box>
</Card>
</Box>
{/* Show Post Create Button only on Activity tab */}
{tabValue === 1 && (

@ -0,0 +1,469 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { getContactPhotoStyles } from '../utils/photoStyles';
import {
Typography,
Box,
Avatar,
Button,
Card,
CardContent,
IconButton,
Chip,
List,
ListItem,
ListItemAvatar,
ListItemText,
alpha,
useTheme,
} from '@mui/material';
import {
ArrowBack,
PersonAdd,
Public,
Lock,
} from '@mui/icons-material';
import { dataService } from '../services/dataService';
import type { Group } from '../types/group';
import InviteForm, { type InviteFormData } from '../components/invite/InviteForm';
// Real NAO member data
const getMockMembers = () => [
{
id: 'oli-sb',
name: 'Oliver Sylvester-Bradley',
avatar: '/images/Oli.jpg',
role: 'Admin',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365), // 1 year ago
},
{
id: 'ruben-daniels',
name: 'Ruben Daniels',
avatar: '/images/Ruben.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 300), // 300 days ago
},
{
id: 'margeigh-novotny',
name: 'Margeigh Novotny',
avatar: '/images/Margeigh.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 280), // 280 days ago
},
{
id: 'alex-lion',
name: 'Alex Lion Yes!',
avatar: '/images/Alex.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 250), // 250 days ago
},
{
id: 'day-waterbury',
name: 'Day Waterbury',
avatar: '/images/Day.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 200), // 200 days ago
},
{
id: 'kevin-triplett',
name: 'Kevin Triplett',
avatar: '/images/Kevin.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 180), // 180 days ago
},
{
id: 'tim-bansemer',
name: 'Tim Bansemer',
avatar: '/images/Tim.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 150), // 150 days ago
},
{
id: 'aza-mafi',
name: 'Aza Mafi',
avatar: '/images/Aza.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 120), // 120 days ago
},
{
id: 'duke-dorje',
name: 'Duke Dorje',
avatar: '/images/Duke.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 100), // 100 days ago
},
{
id: 'david-thomson',
name: 'David Thomson',
avatar: '/images/David.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 80), // 80 days ago
},
{
id: 'samuel-gbafa',
name: 'Samuel Gbafa',
avatar: '/images/Sam.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 60), // 60 days ago
},
{
id: 'meena-seshamani',
name: 'Meena Seshamani',
avatar: '/images/Meena.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 40), // 40 days ago
},
{
id: 'niko-bonnieure',
name: 'Niko Bonnieure',
avatar: '/images/Niko.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), // 30 days ago
},
{
id: 'tree-willard',
name: 'Tree Willard',
avatar: '/images/Tree.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 20), // 20 days ago
},
{
id: 'stephane-bancel',
name: 'Stephane Bancel',
avatar: '/images/Stephane.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 15), // 15 days ago
},
{
id: 'joscha-raue',
name: 'Joscha Raue',
avatar: '/images/Joscha.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10), // 10 days ago
},
{
id: 'drummond-reed',
name: 'Drummond Reed',
avatar: '/images/Drummond.jpg',
role: 'Member',
joinedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), // 5 days ago
},
];
const GroupInfoPage = () => {
const { groupId } = useParams<{ groupId: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const theme = useTheme();
const [group, setGroup] = useState<Group | null>(null);
const [members] = useState(getMockMembers());
const [isLoading, setIsLoading] = useState(true);
const [showInviteForm, setShowInviteForm] = useState(false);
const [selectedContact, setSelectedContact] = useState<{name: string; email: string} | undefined>();
useEffect(() => {
const loadGroupData = async () => {
if (!groupId) return;
setIsLoading(true);
try {
const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null);
// Handle returning from contact selection
const selectedContactName = searchParams.get('selectedContactName');
const selectedContactEmail = searchParams.get('selectedContactEmail');
if (selectedContactName && selectedContactEmail) {
setSelectedContact({
name: selectedContactName,
email: selectedContactEmail
});
setShowInviteForm(true);
// Clean up selection parameters
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete('selectedContactName');
newSearchParams.delete('selectedContactEmail');
setSearchParams(newSearchParams);
}
} catch (error) {
console.error('Failed to load group data:', error);
} finally {
setIsLoading(false);
}
};
loadGroupData();
}, [groupId, searchParams, setSearchParams]);
const handleBack = () => {
navigate(`/groups/${groupId}`);
};
const handleInviteToGroup = () => {
setShowInviteForm(true);
};
const handleInviteSubmit = (inviteData: InviteFormData) => {
console.log('Sending invite:', inviteData);
// TODO: Generate personalized invitation link and send email
// For now, navigate to invite page with the data
const inviteParams = new URLSearchParams({
groupId: groupId!,
inviteeName: inviteData.inviteeName,
inviterName: inviteData.inviterName,
relationshipType: inviteData.relationshipType,
});
setShowInviteForm(false);
navigate(`/invite?${inviteParams.toString()}`);
};
const handleSelectFromNetwork = () => {
// Navigate to contacts page with selection mode and return context
setShowInviteForm(false);
navigate(`/contacts?mode=select&returnTo=group-info&groupId=${groupId}`);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
const getPrivacyIcon = (isPrivate: boolean) => {
return isPrivate ? (
<Lock sx={{ fontSize: 20, color: '#ff9800' }} />
) : (
<Public sx={{ fontSize: 20, color: '#4caf50' }} />
);
};
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
<Typography variant="h6" color="text.secondary">
Loading group...
</Typography>
</Box>
);
}
if (!group) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
<Typography variant="h6" color="text.secondary">
Group not found
</Typography>
</Box>
);
}
return (
<Box sx={{
height: '100%',
width: '100%',
maxWidth: { xs: '100vw', md: '100%' },
overflow: 'hidden',
boxSizing: 'border-box',
pt: { xs: 1.5, md: 2 },
pb: 0,
mx: { xs: 0, md: 'auto' }
}}>
{/* Header */}
<Box sx={{
mb: { xs: 1.5, md: 2 },
width: '100%',
overflow: 'hidden',
px: { xs: '10px', md: 0 }
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: { xs: 1, md: 2 },
mb: { xs: 2, md: 3 },
width: '100%',
maxWidth: '100%',
overflow: 'hidden',
minWidth: 0
}}>
<IconButton onClick={handleBack} size="large" sx={{ flexShrink: 0 }}>
<ArrowBack />
</IconButton>
<Avatar
src={group.image}
alt={group.name}
sx={{
width: { xs: 48, md: 64 },
height: { xs: 48, md: 64 },
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
color: 'primary.main',
flexShrink: 0
}}
>
{group.name.charAt(0)}
</Avatar>
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
fontSize: { xs: '1.5rem', md: '2.125rem' },
lineHeight: 1.2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{group.name}
</Typography>
{getPrivacyIcon(group.isPrivate)}
</Box>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInviteToGroup}
sx={{
borderRadius: 2,
px: { xs: 1.5, md: 3 },
py: { xs: 0.5, md: 1 },
fontSize: { xs: '0.75rem', md: '0.875rem' },
flexShrink: 0,
minWidth: { xs: 'auto', md: 'auto' },
mr: { xs: 0.5, md: 0 } // Add right margin on mobile
}}
>
Invite
</Button>
</Box>
</Box>
</Box>
</Box>
{/* Content */}
<Box sx={{ px: { xs: '10px', md: 0 } }}>
{/* Description Card */}
<Card sx={{ mb: 3 }}>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
About this group
</Typography>
<Typography variant="body1" sx={{ mb: 3, lineHeight: 1.6 }}>
{group.description}
</Typography>
{group.tags && group.tags.length > 0 && (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 3 }}>
{group.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main',
fontWeight: 500,
}}
/>
))}
</Box>
)}
</CardContent>
</Card>
{/* Members List */}
<Card>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Members ({members.length})
</Typography>
</Box>
<List sx={{ width: '100%' }}>
{members.map((member, index) => (
<ListItem
key={member.id}
sx={{
px: 0,
py: 1,
borderBottom: index === members.length - 1 ? 'none' : '1px solid',
borderColor: 'divider',
}}
>
<ListItemAvatar>
<Avatar
src={member.avatar}
sx={{
width: 48,
height: 48,
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
color: 'primary.main',
backgroundSize: member.avatar ? getContactPhotoStyles(member.name).backgroundSize : 'cover',
backgroundPosition: member.avatar ? getContactPhotoStyles(member.name).backgroundPosition : 'center',
}}
>
{!member.avatar && member.name.split(' ').map(n => n[0]).join('')}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{member.name}
</Typography>
{member.role === 'Admin' && (
<Chip
label="Admin"
size="small"
color="primary"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
</Box>
}
secondary={
<Typography variant="body2" color="text.secondary">
Joined {formatDate(member.joinedAt)}
</Typography>
}
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
</Box>
{/* Invite Form */}
{group && (
<InviteForm
open={showInviteForm}
onClose={() => {
setShowInviteForm(false);
setSelectedContact(undefined);
}}
onSubmit={handleInviteSubmit}
onSelectFromNetwork={handleSelectFromNetwork}
group={group}
prefilledContact={selectedContact}
/>
)}
</Box>
);
};
export default GroupInfoPage;

@ -76,14 +76,6 @@ const GroupPage = () => {
);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
return (
<Box sx={{
height: '100%',
@ -98,7 +90,7 @@ const GroupPage = () => {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: { xs: 2, md: 3 },
mb: { xs: 1, md: 1 },
width: '100%',
overflow: 'hidden',
minWidth: 0
@ -109,7 +101,7 @@ const GroupPage = () => {
component="h1"
sx={{
fontWeight: 700,
mb: 1,
mb: { xs: 0, md: 0 },
fontSize: { xs: '1.5rem', md: '2.125rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
@ -126,8 +118,9 @@ const GroupPage = () => {
onClick={handleCreateGroup}
sx={{
borderRadius: 2,
fontSize: { xs: '0.75rem', md: '0.875rem' },
px: { xs: 1, md: 2 }
fontSize: { xs: '0.7rem', md: '0.875rem' },
px: { xs: 0.75, md: 2 },
py: { xs: 0.5, md: 0.75 }
}}
>
Create Group
@ -135,16 +128,155 @@ const GroupPage = () => {
</Box>
</Box>
<Card sx={{
mb: { xs: 0, md: 3 },
width: { xs: 'calc(100% + 20px)', md: '100%' },
maxWidth: { xs: 'calc(100vw - 0px)', md: '100%' },
{/* Mobile: Compact list without cards */}
<Box sx={{
display: { xs: 'block', md: 'none' },
width: '100%',
px: 0
}}>
<TextField
fullWidth
placeholder="Search groups..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
sx={{
mb: 2,
'& .MuiInputBase-root': {
height: 40
}
}}
/>
{isLoading ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1" color="text.secondary">
Loading groups...
</Typography>
</Box>
) : filteredGroups.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1" color="text.secondary" gutterBottom>
{searchQuery ? 'No groups found' : 'No groups yet'}
</Typography>
<Typography variant="body2" color="text.secondary">
{searchQuery ? 'Try adjusting your search terms.' : 'Create your first group to get started!'}
</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{filteredGroups.map((group, index) => (
<Box
key={group.id}
onClick={() => handleGroupClick(group.id)}
sx={{
cursor: 'pointer',
p: 2,
borderBottom: index === filteredGroups.length - 1 ? 'none' : '1px solid',
borderColor: 'divider',
'&:active': {
backgroundColor: alpha(theme.palette.primary.main, 0.08),
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar
src={group.image}
alt={group.name}
sx={{
width: 40,
height: 40,
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
color: 'primary.main'
}}
>
<Group />
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
<Typography
variant="subtitle1"
component="div"
sx={{
fontWeight: 700,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{group.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
<People sx={{ fontSize: 14, color: 'text.secondary' }} />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
{group.memberCount}
</Typography>
</Box>
{getPrivacyIcon(group.isPrivate)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
{group.unreadCount && group.unreadCount > 0 && (
<Badge
badgeContent={group.unreadCount}
color="primary"
sx={{
'& .MuiBadge-badge': {
fontSize: '0.65rem',
height: 16,
minWidth: 16,
borderRadius: '8px'
}
}}
>
<Box sx={{ width: 8, height: 8 }} />
</Badge>
)}
</Box>
</Box>
{group.latestPost && (
<Typography
variant="body2"
color="text.secondary"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '0.75rem',
fontStyle: 'italic',
fontWeight: 600
}}
>
{group.latestPostAuthor && `${group.latestPostAuthor.split(' ')[0]}: `}{group.latestPost}
</Typography>
)}
</Box>
</Box>
</Box>
))}
</Box>
)}
</Box>
{/* Desktop: Keep original card layout */}
<Box sx={{
display: { xs: 'none', md: 'block' },
mb: 3,
width: '100%',
overflow: 'hidden',
mx: { xs: '-10px', md: 0 }, // Extend to edges on mobile
borderRadius: { xs: 0, md: 1 }, // Remove border radius on mobile
boxSizing: 'border-box'
}}>
<Box sx={{ p: { xs: '10px', md: 3 } }}>
<TextField
fullWidth
placeholder="Search groups..."
@ -215,43 +347,57 @@ const GroupPage = () => {
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 600 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 700 }}>
{group.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
<People sx={{ fontSize: 16, color: 'text.secondary' }} />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
{group.memberCount}
</Typography>
</Box>
{getPrivacyIcon(group.isPrivate)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{group.unreadCount && group.unreadCount > 0 && (
<Badge
badgeContent={group.memberCount}
badgeContent={group.unreadCount}
color="primary"
sx={{
'& .MuiBadge-badge': {
position: 'static',
transform: 'none',
fontSize: '0.75rem',
minWidth: 'auto',
height: 20,
borderRadius: 1
fontSize: '0.65rem',
height: 16,
minWidth: 16,
borderRadius: '8px'
}
}}
>
<People sx={{ fontSize: 16, color: 'text.secondary' }} />
<Box sx={{ width: 8, height: 8 }} />
</Badge>
<Typography variant="body2" color="text.secondary">
members
</Typography>
)}
</Box>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2, minHeight: 40 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{
mb: 2,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{group.description}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
{group.tags?.map((tag) => (
{group.tags?.slice(0, 3).map((tag) => (
<Chip
key={tag}
label={tag}
@ -268,9 +414,27 @@ const GroupPage = () => {
))}
</Box>
<Typography variant="caption" color="text.secondary">
Created {formatDate(group.createdAt)}
{group.latestPost && (
<Box>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 500, display: 'block', mb: 0.5 }}>
Latest post:
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontStyle: 'italic',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '0.8rem',
fontWeight: 600
}}
>
{group.latestPostAuthor && `${group.latestPostAuthor.split(' ')[0]}: `}{group.latestPost}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
@ -278,7 +442,6 @@ const GroupPage = () => {
</Grid>
)}
</Box>
</Card>
</Box>
);

@ -1,17 +1,46 @@
import { Box, Typography, Container } from '@mui/material';
import { Box, Typography } from '@mui/material';
const MessagesPage = () => {
return (
<Container maxWidth="lg">
<Box sx={{ py: 4 }}>
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}>
<Box sx={{
height: '100%',
width: '100%',
maxWidth: { xs: '100vw', md: '100%' },
overflow: 'hidden',
boxSizing: 'border-box',
p: { xs: '10px', md: 0 },
mx: { xs: 0, md: 'auto' }
}}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: { xs: 1, md: 1 },
width: '100%',
overflow: 'hidden',
minWidth: 0
}}>
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: 700,
mb: { xs: 0, md: 0 },
fontSize: { xs: '1.5rem', md: '2.125rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
Messages
</Typography>
</Box>
</Box>
<Typography variant="body1" color="text.secondary">
Your messages and conversations will appear here.
</Typography>
</Box>
</Container>
);
};

@ -184,7 +184,7 @@ const NotificationsPage = () => {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: { xs: 2, md: 3 },
mb: { xs: 1, md: 1 },
width: '100%',
overflow: 'hidden',
minWidth: 0
@ -195,7 +195,7 @@ const NotificationsPage = () => {
component="h1"
sx={{
fontWeight: 700,
mb: 1,
mb: { xs: 0, md: 0 },
fontSize: { xs: '1.5rem', md: '2.125rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
@ -233,7 +233,7 @@ const NotificationsPage = () => {
maxWidth: { xs: 'calc(100vw - 0px)', md: '100%' },
overflow: 'hidden',
mx: { xs: '-10px', md: 0 },
borderRadius: { xs: 0, md: 1 },
borderRadius: 0,
boxSizing: 'border-box'
}}>
<Box sx={{ p: { xs: '10px', md: 3 } }}>

@ -111,11 +111,20 @@ export const dataService = {
try {
const response = await fetch('/groups.json');
const groupsData = await response.json();
const groups = groupsData.map((group: any) => ({
const groups = groupsData.map((group: any) => {
const processedGroup = {
...group,
createdAt: new Date(group.createdAt),
updatedAt: new Date(group.updatedAt)
}));
};
// Convert optional date fields if they exist
if (group.latestPostAt) {
processedGroup.latestPostAt = new Date(group.latestPostAt);
}
return processedGroup;
});
resolve(groups);
} catch (error) {
console.error('Failed to load groups:', error);
@ -133,11 +142,18 @@ export const dataService = {
const groupsData = await response.json();
const group = groupsData.find((g: any) => g.id === id);
if (group) {
resolve({
const processedGroup = {
...group,
createdAt: new Date(group.createdAt),
updatedAt: new Date(group.updatedAt)
});
};
// Convert optional date fields if they exist
if (group.latestPostAt) {
processedGroup.latestPostAt = new Date(group.latestPostAt);
}
resolve(processedGroup);
} else {
resolve(undefined);
}
@ -157,11 +173,20 @@ export const dataService = {
const groupsData = await response.json();
const userGroups = groupsData
.filter((group: any) => group.memberIds.includes(userId))
.map((group: any) => ({
.map((group: any) => {
const processedGroup = {
...group,
createdAt: new Date(group.createdAt),
updatedAt: new Date(group.updatedAt)
}));
};
// Convert optional date fields if they exist
if (group.latestPostAt) {
processedGroup.latestPostAt = new Date(group.latestPostAt);
}
return processedGroup;
});
resolve(userGroups);
} catch (error) {
console.error('Failed to load user groups:', error);

@ -326,8 +326,36 @@ export const createAppTheme = (mode: PaletteMode) => {
root: {
backgroundColor: isDark ? '#1e293b' : '#ffffff',
color: isDark ? '#e2e8f0' : '#334155',
boxShadow: 'none',
boxShadow: 'none !important',
borderRadius: '0 !important',
borderBottom: `1px solid ${isDark ? alpha('#e2e8f0', 0.08) : alpha('#334155', 0.08)}`,
height: 64,
minHeight: 64,
'&::before': {
borderRadius: '0 !important',
},
'&::after': {
borderRadius: '0 !important',
},
'& > *': {
borderRadius: '0 !important',
},
'&.MuiPaper-elevation': {
boxShadow: 'none !important',
},
'&.MuiPaper-elevation4': {
boxShadow: 'none !important',
},
},
},
},
MuiToolbar: {
styleOverrides: {
root: {
minHeight: '64px !important',
height: '64px !important',
paddingTop: '0 !important',
paddingBottom: '0 !important',
},
},
},
@ -335,15 +363,22 @@ export const createAppTheme = (mode: PaletteMode) => {
styleOverrides: {
paper: {
backgroundColor: isDark ? '#0f172a' : '#ffffff',
borderRight: `1px solid ${isDark ? alpha('#e2e8f0', 0.08) : alpha('#334155', 0.08)}`,
borderRadius: 0,
border: 'none',
},
docked: {
'& .MuiDrawer-paper': {
border: 'none',
borderRight: 'none',
},
},
},
},
MuiListItem: {
styleOverrides: {
root: {
borderRadius: 8,
margin: '2px 8px',
borderRadius: 0,
margin: 0,
'&:hover': {
backgroundColor: isDark ? alpha('#e2e8f0', 0.04) : alpha('#334155', 0.04),
},

@ -10,6 +10,10 @@ export interface Group {
isPrivate: boolean;
tags?: string[];
image?: string;
latestPost?: string;
latestPostAuthor?: string;
latestPostAt?: Date;
unreadCount?: number;
}
export interface GroupMember {

Loading…
Cancel
Save