Merge pull request #3 from TinyCloudLabs/feature/post-button-redesign

Redesign post creation with floating action button and type selector...
plus lots more screens!
main
olisb 2 months ago committed by GitHub
commit f4110af77e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      public/groups.json
  2. 41
      src/App.tsx
  3. 173
      src/components/PostCreateButton.tsx
  4. 1
      src/components/layout/DashboardLayout.tsx
  5. 1
      src/components/navigation/BottomNavigation.tsx
  6. 30
      src/pages/FeedPage.tsx
  7. 462
      src/pages/GroupDetailPage.tsx
  8. 141
      src/pages/InvitationPage.tsx
  9. 312
      src/pages/SocialContractPage.tsx
  10. 26
      src/types/group.ts

@ -10,7 +10,7 @@
"updatedAt": "2023-10-15T14:30:00Z",
"isPrivate": false,
"tags": ["react", "frontend", "development"],
"image": "https://i.pravatar.cc/200?img=react"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/react/react-original.svg"
},
{
"id": "2",
@ -23,7 +23,7 @@
"updatedAt": "2023-10-20T16:45:00Z",
"isPrivate": false,
"tags": ["product", "management", "strategy"],
"image": "https://i.pravatar.cc/200?img=product"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/trello/trello-plain.svg"
},
{
"id": "3",
@ -36,7 +36,7 @@
"updatedAt": "2023-09-25T13:15:00Z",
"isPrivate": false,
"tags": ["design", "ux", "ui"],
"image": "https://i.pravatar.cc/200?img=design"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/figma/figma-original.svg"
},
{
"id": "4",
@ -49,7 +49,7 @@
"updatedAt": "2023-08-30T10:20:00Z",
"isPrivate": true,
"tags": ["engineering", "leadership", "management"],
"image": "https://i.pravatar.cc/200?img=leadership"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg"
},
{
"id": "5",
@ -62,7 +62,7 @@
"updatedAt": "2023-07-20T09:30:00Z",
"isPrivate": false,
"tags": ["business", "strategy", "consulting"],
"image": "https://i.pravatar.cc/200?img=business"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linkedin/linkedin-original.svg"
},
{
"id": "6",
@ -75,6 +75,6 @@
"updatedAt": "2023-06-25T15:45:00Z",
"isPrivate": false,
"tags": ["marketing", "digital", "growth"],
"image": "https://i.pravatar.cc/200?img=marketing"
"image": "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/google/google-original.svg"
}
]

@ -3,10 +3,12 @@ import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { OnboardingProvider } from './context/OnboardingContext';
import DashboardLayout from './components/layout/DashboardLayout';
import SocialContractPage from './pages/SocialContractPage';
import ImportPage from './pages/ImportPage';
import ContactListPage from './pages/ContactListPage';
import ContactViewPage from './pages/ContactViewPage';
import GroupPage from './pages/GroupPage';
import GroupDetailPage from './pages/GroupDetailPage';
import InvitationPage from './pages/InvitationPage';
import OnboardingPage from './pages/OnboardingPage';
import FeedPage from './pages/FeedPage';
@ -23,21 +25,30 @@ function App() {
<CssBaseline />
<OnboardingProvider>
<Router>
<DashboardLayout>
<Routes>
<Route path="/" element={<FeedPage />} />
<Route path="/feed" element={<FeedPage />} />
<Route path="/import" element={<ImportPage />} />
<Route path="/contacts" element={<ContactListPage />} />
<Route path="/contacts/:id" element={<ContactViewPage />} />
<Route path="/groups" element={<GroupPage />} />
<Route path="/posts" element={<PostsOffersPage />} />
<Route path="/messages" element={<MessagesPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/invite" element={<InvitationPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
</Routes>
</DashboardLayout>
<Routes>
{/* Social Contract page - outside DashboardLayout */}
<Route path="/onboarding" element={<SocialContractPage />} />
{/* Main app routes - inside DashboardLayout */}
<Route path="/*" element={
<DashboardLayout>
<Routes>
<Route path="/" element={<FeedPage />} />
<Route path="/feed" element={<FeedPage />} />
<Route path="/import" element={<ImportPage />} />
<Route path="/contacts" element={<ContactListPage />} />
<Route path="/contacts/:id" element={<ContactViewPage />} />
<Route path="/groups" element={<GroupPage />} />
<Route path="/groups/:groupId" element={<GroupDetailPage />} />
<Route path="/posts" element={<PostsOffersPage />} />
<Route path="/messages" element={<MessagesPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/invite" element={<InvitationPage />} />
<Route path="/onboarding/setup" element={<OnboardingPage />} />
</Routes>
</DashboardLayout>
} />
</Routes>
</Router>
</OnboardingProvider>
</ThemeProvider>

@ -0,0 +1,173 @@
import { useState } from 'react';
import {
Fab,
Dialog,
DialogTitle,
DialogContent,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
Box,
IconButton,
useTheme,
alpha
} from '@mui/material';
import {
Add,
PostAdd,
LocalOffer,
ShoppingCart,
Close
} from '@mui/icons-material';
interface PostCreateButtonProps {
groupId?: string;
onCreatePost?: (type: 'post' | 'offer' | 'want', groupId?: string) => void;
}
const PostCreateButton = ({ groupId, onCreatePost }: PostCreateButtonProps) => {
const [open, setOpen] = useState(false);
const theme = useTheme();
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleCreatePost = (type: 'post' | 'offer' | 'want') => {
if (onCreatePost) {
onCreatePost(type, groupId);
} else {
// Default behavior - navigate to posts page with type parameter
const searchParams = new URLSearchParams();
searchParams.append('type', type);
if (groupId) {
searchParams.append('groupId', groupId);
}
window.location.href = `/posts?${searchParams.toString()}`;
}
handleClose();
};
const postTypes = [
{
type: 'post' as const,
title: 'Post',
description: 'Share an update, thought, or announcement',
icon: <PostAdd />,
color: theme.palette.primary.main
},
{
type: 'offer' as const,
title: 'Offer',
description: 'Offer your services, expertise, or resources',
icon: <LocalOffer />,
color: theme.palette.success.main
},
{
type: 'want' as const,
title: 'Want',
description: 'Request help, services, or connections',
icon: <ShoppingCart />,
color: theme.palette.warning.main
}
];
return (
<>
<Fab
color="primary"
aria-label="create post"
onClick={handleOpen}
sx={{
position: 'fixed',
bottom: { xs: 90, md: 24 }, // Higher on mobile to avoid bottom nav
right: 24,
zIndex: 1000,
}}
>
<Add />
</Fab>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
p: 1
}
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pb: 1 }}>
<Typography variant="h6" component="div">
What would you like to create?
</Typography>
<IconButton onClick={handleClose} size="small">
<Close />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 2, pt: 0 }}>
<List sx={{ p: 0 }}>
{postTypes.map((postType, index) => (
<ListItem key={postType.type} disablePadding sx={{ mb: index < postTypes.length - 1 ? 1 : 0 }}>
<ListItemButton
onClick={() => handleCreatePost(postType.type)}
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 2,
'&:hover': {
borderColor: postType.color,
backgroundColor: alpha(postType.color, 0.04),
}
}}
>
<ListItemIcon sx={{ minWidth: 48 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 2,
backgroundColor: alpha(postType.color, 0.1),
color: postType.color
}}
>
{postType.icon}
</Box>
</ListItemIcon>
<ListItemText
primary={postType.title}
secondary={postType.description}
primaryTypographyProps={{
fontWeight: 600,
fontSize: '1rem'
}}
secondaryTypographyProps={{
fontSize: '0.875rem'
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</DialogContent>
</Dialog>
</>
);
};
export default PostCreateButton;

@ -65,7 +65,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
{ text: 'Feed', icon: <RssFeed />, path: '/feed' },
{ text: 'Network', icon: <Hub />, path: '/contacts' },
{ text: 'Groups', icon: <Groups />, path: '/groups' },
{ text: 'Post', icon: <PostAdd />, path: '/posts' },
{ text: 'Chat', icon: <Chat />, path: '/messages' },
];

@ -20,7 +20,6 @@ const BottomNavigation = () => {
{ label: 'Feed', icon: <RssFeed />, path: '/feed' },
{ label: 'Network', icon: <Hub />, path: '/contacts' },
{ label: 'Groups', icon: <Groups />, path: '/groups' },
{ label: 'Post', icon: <PostAdd />, path: '/posts' },
{ label: 'Chat', icon: <Chat />, path: '/messages' },
];

@ -1,17 +1,27 @@
import { Box, Typography, Container } from '@mui/material';
import PostCreateButton from '../components/PostCreateButton';
const FeedPage = () => {
const handleCreatePost = (type: 'post' | 'offer' | 'want') => {
console.log(`Creating ${type} in main feed`);
// TODO: Implement post creation logic
};
return (
<Container maxWidth="lg">
<Box sx={{ py: 4 }}>
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}>
Feed
</Typography>
<Typography variant="body1" color="text.secondary">
Your personalized network feed will appear here.
</Typography>
</Box>
</Container>
<>
<Container maxWidth="lg">
<Box sx={{ py: 4 }}>
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}>
Feed
</Typography>
<Typography variant="body1" color="text.secondary">
Your personalized network feed will appear here.
</Typography>
</Box>
</Container>
<PostCreateButton onCreatePost={handleCreatePost} />
</>
);
};

@ -0,0 +1,462 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Typography,
Box,
Tabs,
Tab,
Avatar,
Card,
CardContent,
IconButton,
Button,
Chip,
Divider,
Badge,
alpha,
useTheme
} from '@mui/material';
import {
ArrowBack,
RssFeed,
People,
Chat,
Folder,
Link,
ThumbUp,
Comment,
Share,
MoreVert,
FolderShared,
Description,
TableChart,
PersonAdd
} from '@mui/icons-material';
import { dataService } from '../services/dataService';
import type { Group, GroupPost, GroupLink } from '../types/group';
import PostCreateButton from '../components/PostCreateButton';
const GroupDetailPage = () => {
const { groupId } = useParams<{ groupId: string }>();
const navigate = useNavigate();
const theme = useTheme();
const [group, setGroup] = useState<Group | null>(null);
const [posts, setPosts] = useState<GroupPost[]>([]);
const [links, setLinks] = useState<GroupLink[]>([]);
const [tabValue, setTabValue] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadGroupData = async () => {
if (!groupId) return;
setIsLoading(true);
try {
const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null);
// Mock data for posts and links until we have real data
const mockPosts: GroupPost[] = [
{
id: '1',
groupId: groupId,
authorId: 'user1',
authorName: 'John Doe',
authorAvatar: undefined,
content: 'Welcome to our group! Looking forward to collaborating with everyone.',
createdAt: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
updatedAt: new Date(Date.now() - 1000 * 60 * 30),
likes: 5,
comments: 2,
},
{
id: '2',
groupId: groupId,
authorId: 'user2',
authorName: 'Jane Smith',
authorAvatar: undefined,
content: 'Just shared some useful resources in our Files section. Check them out!',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
likes: 3,
comments: 1,
}
];
const mockLinks: GroupLink[] = [
{
id: '1',
groupId: groupId,
title: 'Industry Best Practices Guide',
url: 'https://example.com/guide',
description: 'Comprehensive guide on industry best practices',
sharedBy: 'user1',
sharedByName: 'John Doe',
sharedAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
tags: ['guide', 'best-practices']
}
];
setPosts(mockPosts);
setLinks(mockLinks);
} catch (error) {
console.error('Failed to load group data:', error);
} finally {
setIsLoading(false);
}
};
loadGroupData();
}, [groupId]);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleBack = () => {
navigate('/groups');
};
const handleInviteToGroup = () => {
// TODO: Implement invite to group functionality
console.log('Inviting to group:', group?.name);
// This could navigate to an invite page or open a modal
navigate(`/invite?groupId=${groupId}`);
};
const handleCreatePost = (type: 'post' | 'offer' | 'want', groupId?: string) => {
console.log(`Creating ${type} in group ${groupId || 'unknown'}`);
// TODO: Implement group post creation logic
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).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>
);
const renderMembersTab = () => (
<Box sx={{ mt: 3 }}>
<Card>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Members ({group?.memberCount || 0})
</Typography>
<Typography variant="body2" color="text.secondary">
Member management coming soon...
</Typography>
</CardContent>
</Card>
</Box>
);
const renderChatTab = () => (
<Box sx={{ mt: 3 }}>
<Card>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Group Chat
</Typography>
<Typography variant="body2" color="text.secondary">
Real-time group chat functionality coming soon...
</Typography>
</CardContent>
</Card>
</Box>
);
const renderFilesTab = () => (
<Box sx={{ mt: 3 }}>
<Card>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Collaborative Files
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Share documents and spreadsheets with your group
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2, border: 1, borderColor: 'divider', borderRadius: 2 }}>
<Description sx={{ fontSize: 40, color: 'primary.main' }} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Collaborative Document Editor
</Typography>
<Typography variant="body2" color="text.secondary">
Real-time document editing with your group members
</Typography>
</Box>
<Chip label="Coming Soon" size="small" variant="outlined" />
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2, border: 1, borderColor: 'divider', borderRadius: 2 }}>
<TableChart sx={{ fontSize: 40, color: 'success.main' }} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Collaborative Spreadsheet Editor
</Typography>
<Typography variant="body2" color="text.secondary">
Work together on spreadsheets and data analysis
</Typography>
</Box>
<Chip label="Coming Soon" size="small" variant="outlined" />
</Box>
</Box>
</CardContent>
</Card>
</Box>
);
const renderLinksTab = () => (
<Box sx={{ mt: 3 }}>
{links.length === 0 ? (
<Card sx={{ textAlign: 'center', py: 8 }}>
<CardContent>
<Typography variant="h6" color="text.secondary" gutterBottom>
No links shared yet
</Typography>
<Typography variant="body2" color="text.secondary">
Share useful links with your group members
</Typography>
</CardContent>
</Card>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{links.map((link) => (
<Card key={link.id} sx={{ border: 1, borderColor: 'divider' }}>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Link sx={{ color: 'primary.main' }} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{link.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{link.description}
</Typography>
</Box>
</Box>
<Typography variant="body2" color="primary.main" sx={{ mb: 2 }}>
{link.url}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
{link.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',
}}
/>
))}
</Box>
<Typography variant="caption" color="text.secondary">
Shared by {link.sharedByName} {formatDate(link.sharedAt)}
</Typography>
</CardContent>
</Card>
))}
</Box>
)}
</Box>
);
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%' }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={handleBack} size="large">
<ArrowBack />
</IconButton>
<Avatar
src={group.image}
alt={group.name}
sx={{ width: 64, height: 64, bgcolor: 'primary.main' }}
>
{group.name.charAt(0)}
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" component="h1" sx={{ fontWeight: 700 }}>
{group.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
<Badge badgeContent={group.memberCount} color="primary">
<People sx={{ fontSize: 20, color: 'text.secondary' }} />
</Badge>
<Typography variant="body2" color="text.secondary">
{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 }}>
{group.description}
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInviteToGroup}
sx={{ borderRadius: 2 }}
>
Invite to Group
</Button>
</Box>
</Box>
{/* Navigation Tabs */}
<Card>
<Tabs
value={tabValue}
onChange={handleTabChange}
aria-label="group navigation tabs"
sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTab-root': {
minHeight: 56,
textTransform: 'none',
fontWeight: 500,
}
}}
>
<Tab icon={<RssFeed />} label="Feed" />
<Tab icon={<People />} label="Members" />
<Tab icon={<Chat />} label="Chat" />
<Tab icon={<Folder />} label="Files" />
<Tab icon={<Link />} label="Links" />
</Tabs>
{/* Tab Content */}
<Box sx={{ p: 3 }}>
{tabValue === 0 && renderFeedTab()}
{tabValue === 1 && renderMembersTab()}
{tabValue === 2 && renderChatTab()}
{tabValue === 3 && renderFilesTab()}
{tabValue === 4 && renderLinksTab()}
</Box>
</Card>
{/* Show Post Create Button only on Feed tab */}
{tabValue === 0 && (
<PostCreateButton
groupId={groupId}
onCreatePost={handleCreatePost}
/>
)}
</Box>
);
};
export default GroupDetailPage;

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Container,
Typography,
@ -14,7 +14,9 @@ import {
Snackbar,
Alert,
TextField,
InputAdornment
InputAdornment,
Avatar,
Chip
} from '@mui/material';
import {
Share,
@ -23,26 +25,47 @@ import {
Message,
WhatsApp,
GetApp,
Refresh
Refresh,
ArrowBack,
Groups
} from '@mui/icons-material';
import { QRCodeSVG } from 'qrcode.react';
import { dataService } from '../services/dataService';
import type { Group } from '../types/group';
const InvitationPage = () => {
const [invitationUrl, setInvitationUrl] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const [invitationId, setInvitationId] = useState('');
const [group, setGroup] = useState<Group | null>(null);
const [isGroupInvite, setIsGroupInvite] = useState(false);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => {
const generateInvitation = () => {
const loadGroupAndGenerateInvitation = async () => {
const groupId = searchParams.get('groupId');
if (groupId) {
setIsGroupInvite(true);
try {
const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null);
} catch (error) {
console.error('Failed to load group:', error);
}
}
const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id);
const url = `${window.location.origin}/onboarding?invite=${id}`;
const url = groupId
? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}`
: `${window.location.origin}/onboarding?invite=${id}`;
setInvitationUrl(url);
};
generateInvitation();
}, []);
loadGroupAndGenerateInvitation();
}, [searchParams]);
const handleCopyToClipboard = async () => {
try {
@ -56,9 +79,14 @@ const InvitationPage = () => {
const handleShare = async () => {
if (navigator.share) {
try {
const title = isGroupInvite ? `Join ${group?.name}` : 'Join My Network';
const text = isGroupInvite
? `Join our ${group?.name} group to collaborate and stay connected!`
: 'Join my personal network to stay connected!';
await navigator.share({
title: 'Join My Network',
text: 'Join my personal network to stay connected!',
title,
text,
url: invitationUrl,
});
} catch (err) {
@ -70,24 +98,26 @@ const InvitationPage = () => {
};
const handleEmailShare = () => {
const subject = encodeURIComponent('Join My Network');
const body = encodeURIComponent(
`I'd like to add you to my personal network. Please use this link to join: ${invitationUrl}`
);
const subject = isGroupInvite
? encodeURIComponent(`Join ${group?.name}`)
: encodeURIComponent('Join My Network');
const body = isGroupInvite
? encodeURIComponent(`I'd like to invite you to join our ${group?.name} group. Please use this link to join: ${invitationUrl}`)
: encodeURIComponent(`I'd like to add you to my personal network. Please use this link to join: ${invitationUrl}`);
window.open(`mailto:?subject=${subject}&body=${body}`);
};
const handleWhatsAppShare = () => {
const text = encodeURIComponent(
`Join my personal network: ${invitationUrl}`
);
const text = isGroupInvite
? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`)
: encodeURIComponent(`Join my personal network: ${invitationUrl}`);
window.open(`https://wa.me/?text=${text}`);
};
const handleSMSShare = () => {
const text = encodeURIComponent(
`Join my personal network: ${invitationUrl}`
);
const text = isGroupInvite
? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`)
: encodeURIComponent(`Join my personal network: ${invitationUrl}`);
window.open(`sms:?body=${text}`);
};
@ -116,9 +146,12 @@ const InvitationPage = () => {
};
const handleNewInvitation = () => {
const groupId = searchParams.get('groupId');
const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id);
const url = `${window.location.origin}/onboarding?invite=${id}`;
const url = groupId
? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}`
: `${window.location.origin}/onboarding?invite=${id}`;
setInvitationUrl(url);
};
@ -126,14 +159,59 @@ const InvitationPage = () => {
navigate('/contacts');
};
const handleBack = () => {
if (isGroupInvite && group) {
navigate(`/groups/${group.id}`);
} else {
navigate('/contacts');
}
};
return (
<Container maxWidth="md" sx={{ py: 4 }}>
<Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Invite to Your Network
{/* Back Button */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<IconButton onClick={handleBack} size="large">
<ArrowBack />
</IconButton>
<Typography variant="h6" sx={{ ml: 1 }}>
{isGroupInvite ? `Back to ${group?.name}` : 'Back to Contacts'}
</Typography>
</Box>
{/* Header */}
<Box sx={{ textAlign: 'center', mb: 4 }}>
{isGroupInvite && group && (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, mb: 2 }}>
<Avatar
src={group.image}
alt={group.name}
sx={{ width: 64, height: 64, bgcolor: 'primary.main' }}
>
<Groups />
</Avatar>
<Box>
<Typography variant="h4" component="h1" gutterBottom>
Invite to {group.name}
</Typography>
{group.isPrivate && (
<Chip label="Private Group" size="small" variant="outlined" />
)}
</Box>
</Box>
)}
{!isGroupInvite && (
<Typography variant="h3" component="h1" gutterBottom>
Invite to Your Network
</Typography>
)}
<Typography variant="h6" color="text.secondary">
Share your personal network invitation
{isGroupInvite
? `Share your ${group?.name} group invitation`
: 'Share your personal network invitation'
}
</Typography>
</Box>
@ -155,7 +233,10 @@ const InvitationPage = () => {
/>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Scan to join your network
{isGroupInvite
? `Scan to join ${group?.name}`
: 'Scan to join your network'
}
</Typography>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
<Button
@ -257,7 +338,10 @@ const InvitationPage = () => {
How it works
</Typography>
<Typography variant="body1" paragraph>
When someone scans your QR code or clicks your invitation link, they'll be guided through a simple onboarding process:
{isGroupInvite
? `When someone scans your QR code or clicks your invitation link, they'll be guided through a simple process to join ${group?.name}:`
: "When someone scans your QR code or clicks your invitation link, they'll be guided through a simple onboarding process:"
}
</Typography>
<Box component="ol" sx={{ pl: 2 }}>
<li>
@ -272,7 +356,10 @@ const InvitationPage = () => {
</li>
<li>
<Typography variant="body2">
They'll be added to your network automatically
{isGroupInvite
? `They'll be added to ${group?.name} and your network automatically`
: "They'll be added to your network automatically"
}
</Typography>
</li>
</Box>

@ -0,0 +1,312 @@
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Container,
Typography,
Box,
Paper,
Button,
Avatar,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
alpha,
useTheme
} from '@mui/material';
import {
Security,
Groups,
Favorite,
Psychology,
AccountTree,
TrendingUp,
InfoOutlined,
Close,
CheckCircle
} from '@mui/icons-material';
import { dataService } from '../services/dataService';
import type { Group } from '../types/group';
const SocialContractPage = () => {
const [group, setGroup] = useState<Group | null>(null);
const [isGroupInvite, setIsGroupInvite] = useState(false);
const [showMoreInfo, setShowMoreInfo] = useState(false);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const theme = useTheme();
useEffect(() => {
const loadGroupData = async () => {
const groupId = searchParams.get('groupId');
const inviteId = searchParams.get('invite');
if (groupId) {
setIsGroupInvite(true);
try {
const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null);
} catch (error) {
console.error('Failed to load group:', error);
}
}
// Store invite parameters for later use
if (inviteId) {
sessionStorage.setItem('inviteId', inviteId);
}
if (groupId) {
sessionStorage.setItem('groupId', groupId);
}
};
loadGroupData();
}, [searchParams]);
const handleAccept = () => {
// Store acceptance in session
sessionStorage.setItem('socialContractAccepted', 'true');
// Navigate directly to the appropriate page
if (isGroupInvite && group) {
navigate(`/groups/${group.id}`);
} else {
navigate('/contacts');
}
};
const handleDontLike = () => {
// Show the "Tell Me More" dialog instead of rejecting
setShowMoreInfo(true);
};
const handleTellMeMore = () => {
setShowMoreInfo(true);
};
const socialContractPrinciples = [
{
icon: <Psychology />,
title: 'Be Your Authentic Self',
description: 'Share your genuine thoughts, experiences, and perspectives. Authenticity builds trust and meaningful connections.'
},
{
icon: <Favorite />,
title: 'Act with Respect & Kindness',
description: 'Treat all members with dignity and respect. Disagreements are welcome, but personal attacks are not.'
},
{
icon: <Security />,
title: 'Maintain Confidentiality',
description: 'What is shared here, stays here. Respect the privacy of discussions and personal information shared by others.'
},
{
icon: <TrendingUp />,
title: 'Contribute Meaningfully',
description: 'Share valuable insights, ask thoughtful questions, and help others grow. Quality over quantity.'
},
{
icon: <AccountTree />,
title: 'Build Genuine Relationships',
description: 'Focus on creating real connections, not just expanding your network numbers. Relationships take time and effort.'
}
];
return (
<Container maxWidth="md" sx={{ py: 4, minHeight: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Paper
elevation={3}
sx={{
p: 4,
textAlign: 'center',
background: `linear-gradient(135deg, ${alpha(theme.palette.primary.main, 0.05)} 0%, ${alpha(theme.palette.secondary.main, 0.05)} 100%)`,
border: 1,
borderColor: 'divider'
}}
>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Welcome to NAO
</Typography>
<Typography variant="h5" color="primary" gutterBottom>
You're entering a high-trust environment
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: '600px', mx: 'auto' }}>
NAO is built on trust, authenticity, and meaningful connections. Before you join{isGroupInvite && group ? ` ${group.name}` : ''}, please read and agree to our social contract.
</Typography>
</Box>
{/* Core Principles */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<CheckCircle color="primary" />
Our Core Principles
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, mt: 3 }}>
{socialContractPrinciples.slice(0, 4).map((principle, index) => (
<Box
key={index}
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
p: 2,
borderRadius: 2,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
border: 1,
borderColor: alpha(theme.palette.primary.main, 0.12)
}}
>
<Box sx={{ color: 'primary.main', mt: 0.5 }}>
{principle.icon}
</Box>
<Box sx={{ textAlign: 'left' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
{principle.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{principle.description}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
{/* Call to Action */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom>
Ready to join our community?
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
By agreeing, you commit to upholding these principles and creating a positive environment for everyone.
</Typography>
</Box>
{/* Action Buttons */}
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
<Button
variant="contained"
size="large"
onClick={handleAccept}
sx={{
px: 4,
py: 1.5,
borderRadius: 2,
textTransform: 'none',
fontSize: '1.1rem'
}}
>
I Agree - Let's Connect!
</Button>
<Button
variant="outlined"
size="large"
startIcon={<InfoOutlined />}
onClick={handleTellMeMore}
sx={{
px: 3,
py: 1.5,
borderRadius: 2,
textTransform: 'none'
}}
>
Tell Me More
</Button>
<Button
variant="text"
size="large"
startIcon={<Close />}
onClick={handleDontLike}
sx={{
px: 3,
py: 1.5,
borderRadius: 2,
textTransform: 'none',
color: 'text.secondary'
}}
>
I Don't Like the Sound of That
</Button>
</Box>
</Paper>
{/* More Info Dialog */}
<Dialog
open={showMoreInfo}
onClose={() => setShowMoreInfo(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
<Typography variant="h5" component="div">
About Our High-Trust Environment
</Typography>
</DialogTitle>
<DialogContent>
<Typography variant="body1" paragraph>
NAO is more than just a networking platform - it's a community where professionals can be their authentic selves and build genuine relationships.
</Typography>
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
What makes us different:
</Typography>
<List>
{socialContractPrinciples.map((principle, index) => (
<ListItem key={index} sx={{ alignItems: 'flex-start' }}>
<ListItemIcon sx={{ color: 'primary.main', mt: 0.5 }}>
{principle.icon}
</ListItemIcon>
<ListItemText
primary={principle.title}
secondary={principle.description}
primaryTypographyProps={{ fontWeight: 600 }}
/>
</ListItem>
))}
</List>
<Divider sx={{ my: 3 }} />
<Typography variant="h6" gutterBottom>
Why this matters:
</Typography>
<Typography variant="body1" paragraph>
In a world of superficial connections and promotional content, we're creating something different.
A space where vulnerability is valued, where you can ask for help without judgment, and where
success is measured by the quality of relationships, not just the quantity of connections.
</Typography>
<Typography variant="body1" paragraph>
When you join, you're not just adding another network to your list - you're becoming part of
a community that will support your professional growth and personal development.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowMoreInfo(false)} variant="outlined">
Close
</Button>
<Button onClick={() => { setShowMoreInfo(false); handleAccept(); }} variant="contained">
I'm Ready to Join
</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default SocialContractPage;

@ -18,3 +18,29 @@ export interface GroupMember {
joinedAt: Date;
role: 'admin' | 'member' | 'moderator';
}
export interface GroupPost {
id: string;
groupId: string;
authorId: string;
authorName: string;
authorAvatar?: string;
content: string;
createdAt: Date;
updatedAt: Date;
likes: number;
comments: number;
attachments?: string[];
}
export interface GroupLink {
id: string;
groupId: string;
title: string;
url: string;
description?: string;
sharedBy: string;
sharedByName: string;
sharedAt: Date;
tags?: string[];
}
Loading…
Cancel
Save