Major changes: - Remove Post+ button from desktop left menu and mobile bottom navigation - Add floating action button to Feed page and Group feed tabs - Create PostCreateButton component with popup for post type selection - Add Social Contract page for invite flow with high-trust messaging - Update Group Detail page with expandable feed and better UX - Enhance invitation flow with group-specific context - Update group mock data with appropriate professional logos Features: - Post type selector popup (Post, Offer, Want) with color-coded icons - Context-aware posting (main feed vs group-specific) - Responsive FAB positioning (avoids mobile bottom nav) - Social contract for authentic professional networking - Group-specific invitation experience - Improved navigation structure Technical: - New PostCreateButton component with Material-UI dialog - Enhanced GroupDetailPage with tabs and post functionality - Updated routing for social contract in onboarding flow - Better TypeScript interfaces for group posts and links - Responsive design considerations for mobile and desktop 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
parent
5dc7a35fff
commit
50f78fc729
@ -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; |
@ -1,17 +1,27 @@ |
|||||||
import { Box, Typography, Container } from '@mui/material'; |
import { Box, Typography, Container } from '@mui/material'; |
||||||
|
import PostCreateButton from '../components/PostCreateButton'; |
||||||
|
|
||||||
const FeedPage = () => { |
const FeedPage = () => { |
||||||
|
const handleCreatePost = (type: 'post' | 'offer' | 'want') => { |
||||||
|
console.log(`Creating ${type} in main feed`); |
||||||
|
// TODO: Implement post creation logic
|
||||||
|
}; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<Container maxWidth="lg"> |
<> |
||||||
<Box sx={{ py: 4 }}> |
<Container maxWidth="lg"> |
||||||
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}> |
<Box sx={{ py: 4 }}> |
||||||
Feed |
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}> |
||||||
</Typography> |
Feed |
||||||
<Typography variant="body1" color="text.secondary"> |
</Typography> |
||||||
Your personalized network feed will appear here. |
<Typography variant="body1" color="text.secondary"> |
||||||
</Typography> |
Your personalized network feed will appear here. |
||||||
</Box> |
</Typography> |
||||||
</Container> |
</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; |
@ -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; |
Loading…
Reference in new issue