diff --git a/public/groups.json b/public/groups.json index 10cac92..51b69a9 100644 --- a/public/groups.json +++ b/public/groups.json @@ -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" } ] \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 77db9fb..18c65d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + {/* Social Contract page - outside DashboardLayout */} + } /> + + {/* Main app routes - inside DashboardLayout */} + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + diff --git a/src/components/PostCreateButton.tsx b/src/components/PostCreateButton.tsx new file mode 100644 index 0000000..e6bad76 --- /dev/null +++ b/src/components/PostCreateButton.tsx @@ -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: , + color: theme.palette.primary.main + }, + { + type: 'offer' as const, + title: 'Offer', + description: 'Offer your services, expertise, or resources', + icon: , + color: theme.palette.success.main + }, + { + type: 'want' as const, + title: 'Want', + description: 'Request help, services, or connections', + icon: , + color: theme.palette.warning.main + } + ]; + + return ( + <> + + + + + + + + What would you like to create? + + + + + + + + + {postTypes.map((postType, index) => ( + + handleCreatePost(postType.type)} + sx={{ + borderRadius: 2, + border: 1, + borderColor: 'divider', + p: 2, + '&:hover': { + borderColor: postType.color, + backgroundColor: alpha(postType.color, 0.04), + } + }} + > + + + {postType.icon} + + + + + + ))} + + + + + ); +}; + +export default PostCreateButton; \ No newline at end of file diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index 0483f24..74bf625 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -65,7 +65,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { { text: 'Feed', icon: , path: '/feed' }, { text: 'Network', icon: , path: '/contacts' }, { text: 'Groups', icon: , path: '/groups' }, - { text: 'Post', icon: , path: '/posts' }, { text: 'Chat', icon: , path: '/messages' }, ]; diff --git a/src/components/navigation/BottomNavigation.tsx b/src/components/navigation/BottomNavigation.tsx index 4593c6b..1e18313 100644 --- a/src/components/navigation/BottomNavigation.tsx +++ b/src/components/navigation/BottomNavigation.tsx @@ -20,7 +20,6 @@ const BottomNavigation = () => { { label: 'Feed', icon: , path: '/feed' }, { label: 'Network', icon: , path: '/contacts' }, { label: 'Groups', icon: , path: '/groups' }, - { label: 'Post', icon: , path: '/posts' }, { label: 'Chat', icon: , path: '/messages' }, ]; diff --git a/src/pages/FeedPage.tsx b/src/pages/FeedPage.tsx index 67e51bb..ed6fd3c 100644 --- a/src/pages/FeedPage.tsx +++ b/src/pages/FeedPage.tsx @@ -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 ( - - - - Feed - - - Your personalized network feed will appear here. - - - + <> + + + + Feed + + + Your personalized network feed will appear here. + + + + + + ); }; diff --git a/src/pages/GroupDetailPage.tsx b/src/pages/GroupDetailPage.tsx new file mode 100644 index 0000000..b38d888 --- /dev/null +++ b/src/pages/GroupDetailPage.tsx @@ -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(null); + const [posts, setPosts] = useState([]); + const [links, setLinks] = useState([]); + 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 = () => ( + + {posts.length === 0 ? ( + + + + No posts yet + + + Be the first to share something with the group! + + + + ) : ( + + {posts.map((post) => ( + + + + + {post.authorName.charAt(0)} + + + + {post.authorName} + + + {formatDate(post.createdAt)} + + + + + + + + + {post.content} + + + + + + + + + + ))} + + )} + + ); + + const renderMembersTab = () => ( + + + + + Members ({group?.memberCount || 0}) + + + Member management coming soon... + + + + + ); + + const renderChatTab = () => ( + + + + + Group Chat + + + Real-time group chat functionality coming soon... + + + + + ); + + const renderFilesTab = () => ( + + + + + Collaborative Files + + + Share documents and spreadsheets with your group + + + + + + + + Collaborative Document Editor + + + Real-time document editing with your group members + + + + + + + + + + Collaborative Spreadsheet Editor + + + Work together on spreadsheets and data analysis + + + + + + + + + ); + + const renderLinksTab = () => ( + + {links.length === 0 ? ( + + + + No links shared yet + + + Share useful links with your group members + + + + ) : ( + + {links.map((link) => ( + + + + + + + {link.title} + + + {link.description} + + + + + + {link.url} + + + + {link.tags?.map((tag) => ( + + ))} + + + + Shared by {link.sharedByName} • {formatDate(link.sharedAt)} + + + + ))} + + )} + + ); + + if (isLoading) { + return ( + + + Loading group... + + + ); + } + + if (!group) { + return ( + + + Group not found + + + ); + } + + return ( + + {/* Header */} + + + + + + {group.name.charAt(0)} + + + + {group.name} + + + + + + + {group.memberCount} members + + {group.isPrivate && ( + + )} + + {group.description && ( + + {group.description} + + )} + + + + + + + {/* Navigation Tabs */} + + + } label="Feed" /> + } label="Members" /> + } label="Chat" /> + } label="Files" /> + } label="Links" /> + + + {/* Tab Content */} + + {tabValue === 0 && renderFeedTab()} + {tabValue === 1 && renderMembersTab()} + {tabValue === 2 && renderChatTab()} + {tabValue === 3 && renderFilesTab()} + {tabValue === 4 && renderLinksTab()} + + + + {/* Show Post Create Button only on Feed tab */} + {tabValue === 0 && ( + + )} + + ); +}; + +export default GroupDetailPage; \ No newline at end of file diff --git a/src/pages/InvitationPage.tsx b/src/pages/InvitationPage.tsx index 82a3480..4743568 100644 --- a/src/pages/InvitationPage.tsx +++ b/src/pages/InvitationPage.tsx @@ -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(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 ( - - - Invite to Your Network + {/* Back Button */} + + + + + + {isGroupInvite ? `Back to ${group?.name}` : 'Back to Contacts'} + + + {/* Header */} + + {isGroupInvite && group && ( + + + + + + + Invite to {group.name} + + {group.isPrivate && ( + + )} + + + )} + + {!isGroupInvite && ( + + Invite to Your Network + + )} + - Share your personal network invitation + {isGroupInvite + ? `Share your ${group?.name} group invitation` + : 'Share your personal network invitation' + } @@ -155,7 +233,10 @@ const InvitationPage = () => { /> - Scan to join your network + {isGroupInvite + ? `Scan to join ${group?.name}` + : 'Scan to join your network' + } + + + + + + + + {/* More Info Dialog */} + setShowMoreInfo(false)} + maxWidth="md" + fullWidth + > + + + About Our High-Trust Environment + + + + + NAO is more than just a networking platform - it's a community where professionals can be their authentic selves and build genuine relationships. + + + + What makes us different: + + + + {socialContractPrinciples.map((principle, index) => ( + + + {principle.icon} + + + + ))} + + + + + + Why this matters: + + + + 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. + + + + 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. + + + + + + + + + ); +}; + +export default SocialContractPage; \ No newline at end of file diff --git a/src/types/group.ts b/src/types/group.ts index e078af0..2eac60f 100644 --- a/src/types/group.ts +++ b/src/types/group.ts @@ -17,4 +17,30 @@ export interface GroupMember { groupId: string; 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[]; } \ No newline at end of file