Collaborative Workspace Assignment
Real-Time Team Collaboration
Build a collaborative workspace for FastOpp
Assignment Overview
What You'll Build
A collaborative workspace application that enables real-time team collaboration on FastOpp conversations, featuring: - Real-time collaboration - Live updates when team members make changes - User presence - See who's online and what they're working on - Shared workspaces - Team-based conversation organization - Comment system - Discuss conversations with team members - Permission management - Control who can access what
Problem Statement
Team Collaboration Challenges
The current FastOpp system, while powerful, lacks team collaboration features: - No real-time updates - Changes aren't reflected immediately - No user presence - Can't see who's online or working on what - No shared workspaces - Conversations are isolated per user - No commenting system - Can't discuss conversations with team - No permission control - All users have the same access level
Your Solution
Collaborative Workspace
Create a collaborative workspace that addresses these limitations:
- Real-time Updates - WebSocket-based live collaboration
- User Presence - Show online status and current activity
- Shared Workspaces - Team-based conversation organization
- Comment System - Threaded discussions on conversations
- Permission Management - Role-based access control
Technical Requirements
Tech Stack
- Frontend Framework - React, Vue, or Angular
- Real-time Communication - Socket.io or WebSocket
- State Management - Redux, Pinia, or NgRx
- Authentication - JWT with role-based permissions
- Database - PostgreSQL with real-time subscriptions
- Backend - FastAPI with WebSocket support
Project Structure
Recommended Architecture
src/
├── components/
│ ├── collaboration/
│ │ ├── UserPresence.tsx
│ │ ├── CommentThread.tsx
│ │ ├── SharedWorkspace.tsx
│ │ └── PermissionManager.tsx
│ ├── realtime/
│ │ ├── RealtimeProvider.tsx
│ │ ├── PresenceIndicator.tsx
│ │ └── ActivityFeed.tsx
│ └── shared/
├── hooks/
│ ├── useWebSocket.ts
│ ├── usePresence.ts
│ ├── useComments.ts
│ └── usePermissions.ts
├── services/
│ ├── websocket.ts
│ ├── collaboration.ts
│ └── permissions.ts
└── stores/
├── collaboration.ts
├── presence.ts
└── comments.ts
Core Components
1. User Presence Component
// components/collaboration/UserPresence.tsx
import React from 'react'
import { usePresence } from '@/hooks/usePresence'
interface UserPresenceProps {
conversationId: string
}
const UserPresence: React.FC<UserPresenceProps> = ({ conversationId }) => {
const { onlineUsers, currentUser } = usePresence(conversationId)
return (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">Online:</span>
<div className="flex -space-x-2">
{onlineUsers.map((user) => (
<div
key={user.id}
className="relative"
title={`${user.name} - ${user.activity}`}
>
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-medium">
{user.name[0].toUpperCase()}
</div>
<div className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white" />
</div>
))}
</div>
{onlineUsers.length > 3 && (
<span className="text-sm text-gray-500">
+{onlineUsers.length - 3} more
</span>
)}
</div>
)
}
Core Components
2. Comment Thread Component
// components/collaboration/CommentThread.tsx
import React, { useState } from 'react'
import { useComments } from '@/hooks/useComments'
import { useAuth } from '@/hooks/useAuth'
interface CommentThreadProps {
conversationId: string
parentId?: string
}
const CommentThread: React.FC<CommentThreadProps> = ({
conversationId,
parentId
}) => {
const { comments, addComment, updateComment, deleteComment } = useComments(conversationId, parentId)
const { user } = useAuth()
const [newComment, setNewComment] = useState('')
const [editingComment, setEditingComment] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!newComment.trim()) return
try {
await addComment({
content: newComment,
conversationId,
parentId
})
setNewComment('')
} catch (error) {
console.error('Failed to add comment:', error)
}
}
const handleEdit = async (commentId: string, content: string) => {
try {
await updateComment(commentId, { content })
setEditingComment(null)
} catch (error) {
console.error('Failed to update comment:', error)
}
}
const handleDelete = async (commentId: string) => {
if (confirm('Are you sure you want to delete this comment?')) {
try {
await deleteComment(commentId)
} catch (error) {
console.error('Failed to delete comment:', error)
}
}
}
return (
<div className="space-y-4">
{/* Comment Form */}
<form onSubmit={handleSubmit} className="flex space-x-2">
<input
type="text"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={!newComment.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
Comment
</button>
</form>
{/* Comments List */}
<div className="space-y-3">
{comments.map((comment) => (
<div key={comment.id} className="bg-gray-50 rounded-lg p-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<span className="font-medium text-sm">{comment.author.name}</span>
<span className="text-xs text-gray-500">
{formatDate(comment.createdAt)}
</span>
{comment.updatedAt !== comment.createdAt && (
<span className="text-xs text-gray-400">(edited)</span>
)}
</div>
{editingComment === comment.id ? (
<EditCommentForm
comment={comment}
onSave={(content) => handleEdit(comment.id, content)}
onCancel={() => setEditingComment(null)}
/>
) : (
<p className="text-sm text-gray-700 mt-1">{comment.content}</p>
)}
</div>
{comment.author.id === user?.id && (
<div className="flex space-x-1">
<button
onClick={() => setEditingComment(comment.id)}
className="text-xs text-blue-600 hover:text-blue-800"
>
Edit
</button>
<button
onClick={() => handleDelete(comment.id)}
className="text-xs text-red-600 hover:text-red-800"
>
Delete
</button>
</div>
)}
</div>
</div>
))}
</div>
</div>
)
}
Real-time Communication
WebSocket Hook
// hooks/useWebSocket.ts
import { useEffect, useRef, useState } from 'react'
import { io, Socket } from 'socket.io-client'
interface WebSocketHook {
socket: Socket | null
isConnected: boolean
emit: (event: string, data: any) => void
on: (event: string, callback: (data: any) => void) => void
off: (event: string, callback: (data: any) => void) => void
}
export const useWebSocket = (url: string): WebSocketHook => {
const [socket, setSocket] = useState<Socket | null>(null)
const [isConnected, setIsConnected] = useState(false)
const socketRef = useRef<Socket | null>(null)
useEffect(() => {
const newSocket = io(url, {
auth: {
token: localStorage.getItem('auth_token')
}
})
newSocket.on('connect', () => {
setIsConnected(true)
console.log('Connected to WebSocket')
})
newSocket.on('disconnect', () => {
setIsConnected(false)
console.log('Disconnected from WebSocket')
})
newSocket.on('error', (error) => {
console.error('WebSocket error:', error)
})
setSocket(newSocket)
socketRef.current = newSocket
return () => {
newSocket.close()
}
}, [url])
const emit = (event: string, data: any) => {
if (socketRef.current) {
socketRef.current.emit(event, data)
}
}
const on = (event: string, callback: (data: any) => void) => {
if (socketRef.current) {
socketRef.current.on(event, callback)
}
}
const off = (event: string, callback: (data: any) => void) => {
if (socketRef.current) {
socketRef.current.off(event, callback)
}
}
return {
socket,
isConnected,
emit,
on,
off
}
}
Presence Management
Presence Hook
// hooks/usePresence.ts
import { useState, useEffect } from 'react'
import { useWebSocket } from './useWebSocket'
import { useAuth } from './useAuth'
interface User {
id: string
name: string
email: string
avatar?: string
activity: string
lastSeen: Date
}
export const usePresence = (conversationId: string) => {
const [onlineUsers, setOnlineUsers] = useState<User[]>([])
const [currentUser, setCurrentUser] = useState<User | null>(null)
const { socket, isConnected, emit, on, off } = useWebSocket(process.env.REACT_APP_WS_URL || 'ws://localhost:8000')
const { user } = useAuth()
useEffect(() => {
if (!socket || !isConnected) return
// Join conversation room
emit('join_conversation', { conversationId })
// Listen for presence updates
const handlePresenceUpdate = (data: { users: User[] }) => {
setOnlineUsers(data.users)
}
const handleUserJoined = (data: { user: User }) => {
setOnlineUsers(prev => [...prev.filter(u => u.id !== data.user.id), data.user])
}
const handleUserLeft = (data: { userId: string }) => {
setOnlineUsers(prev => prev.filter(u => u.id !== data.userId))
}
const handleActivityUpdate = (data: { userId: string, activity: string }) => {
setOnlineUsers(prev =>
prev.map(u => u.id === data.userId ? { ...u, activity: data.activity } : u)
)
}
on('presence_update', handlePresenceUpdate)
on('user_joined', handleUserJoined)
on('user_left', handleUserLeft)
on('activity_update', handleActivityUpdate)
// Set current user
if (user) {
setCurrentUser({
id: user.id,
name: user.name,
email: user.email,
activity: 'viewing conversation',
lastSeen: new Date()
})
}
return () => {
emit('leave_conversation', { conversationId })
off('presence_update', handlePresenceUpdate)
off('user_joined', handleUserJoined)
off('user_left', handleUserLeft)
off('activity_update', handleActivityUpdate)
}
}, [socket, isConnected, conversationId, user, emit, on, off])
const updateActivity = (activity: string) => {
if (socket && isConnected) {
emit('update_activity', { conversationId, activity })
}
}
return {
onlineUsers,
currentUser,
updateActivity
}
}
Permission Management
Permission Hook
// hooks/usePermissions.ts
import { useState, useEffect } from 'react'
import { useAuth } from './useAuth'
export type Permission =
| 'read_conversation'
| 'write_conversation'
| 'delete_conversation'
| 'add_comment'
| 'edit_comment'
| 'delete_comment'
| 'manage_permissions'
| 'invite_users'
export type Role = 'owner' | 'admin' | 'editor' | 'viewer'
interface PermissionMap {
[key: string]: Permission[]
}
const ROLE_PERMISSIONS: PermissionMap = {
owner: [
'read_conversation',
'write_conversation',
'delete_conversation',
'add_comment',
'edit_comment',
'delete_comment',
'manage_permissions',
'invite_users'
],
admin: [
'read_conversation',
'write_conversation',
'delete_conversation',
'add_comment',
'edit_comment',
'delete_comment',
'invite_users'
],
editor: [
'read_conversation',
'write_conversation',
'add_comment',
'edit_comment'
],
viewer: [
'read_conversation',
'add_comment'
]
}
export const usePermissions = (conversationId: string) => {
const [userRole, setUserRole] = useState<Role | null>(null)
const [permissions, setPermissions] = useState<Permission[]>([])
const { user } = useAuth()
useEffect(() => {
if (!user || !conversationId) return
// Fetch user role for this conversation
fetchUserRole(conversationId)
.then(role => {
setUserRole(role)
setPermissions(ROLE_PERMISSIONS[role] || [])
})
.catch(error => {
console.error('Failed to fetch user role:', error)
setUserRole('viewer')
setPermissions(ROLE_PERMISSIONS.viewer)
})
}, [user, conversationId])
const hasPermission = (permission: Permission): boolean => {
return permissions.includes(permission)
}
const canRead = () => hasPermission('read_conversation')
const canWrite = () => hasPermission('write_conversation')
const canDelete = () => hasPermission('delete_conversation')
const canComment = () => hasPermission('add_comment')
const canManagePermissions = () => hasPermission('manage_permissions')
return {
userRole,
permissions,
hasPermission,
canRead,
canWrite,
canDelete,
canComment,
canManagePermissions
}
}
Shared Workspace
Workspace Component
// components/collaboration/SharedWorkspace.tsx
import React, { useState } from 'react'
import { usePermissions } from '@/hooks/usePermissions'
import { usePresence } from '@/hooks/usePresence'
import { CommentThread } from './CommentThread'
interface SharedWorkspaceProps {
conversationId: string
conversation: Conversation
}
const SharedWorkspace: React.FC<SharedWorkspaceProps> = ({
conversationId,
conversation
}) => {
const { canRead, canWrite, canComment } = usePermissions(conversationId)
const { onlineUsers, updateActivity } = usePresence(conversationId)
const [activeTab, setActiveTab] = useState<'conversation' | 'comments'>('conversation')
useEffect(() => {
updateActivity('viewing conversation')
}, [updateActivity])
if (!canRead()) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<h3 className="text-lg font-medium text-gray-900">Access Denied</h3>
<p className="text-gray-500">You don't have permission to view this conversation.</p>
</div>
</div>
)
}
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="bg-white border-b px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-gray-900">
{conversation.title}
</h1>
<UserPresence conversationId={conversationId} />
</div>
<div className="flex items-center space-x-4">
<div className="flex space-x-1">
<button
onClick={() => setActiveTab('conversation')}
className={`px-3 py-2 text-sm font-medium rounded-md ${
activeTab === 'conversation'
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Conversation
</button>
<button
onClick={() => setActiveTab('comments')}
className={`px-3 py-2 text-sm font-medium rounded-md ${
activeTab === 'comments'
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Comments ({conversation.commentCount || 0})
</button>
</div>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden">
{activeTab === 'conversation' ? (
<ConversationView
conversation={conversation}
canWrite={canWrite()}
canDelete={canDelete()}
/>
) : (
<div className="h-full overflow-y-auto p-6">
<CommentThread conversationId={conversationId} />
</div>
)}
</div>
</div>
)
}
Activity Feed
Activity Component
// components/realtime/ActivityFeed.tsx
import React from 'react'
import { useActivity } from '@/hooks/useActivity'
interface Activity {
id: string
type: 'conversation_created' | 'conversation_updated' | 'comment_added' | 'user_joined'
user: {
id: string
name: string
avatar?: string
}
data: any
timestamp: Date
}
const ActivityFeed: React.FC = () => {
const { activities, isLoading } = useActivity()
if (isLoading) {
return (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-3 bg-gray-200 rounded w-1/2" />
</div>
))}
</div>
)
}
return (
<div className="space-y-4">
{activities.map((activity) => (
<div key={activity.id} className="flex items-start space-x-3">
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-medium">
{activity.user.name[0].toUpperCase()}
</div>
<div className="flex-1">
<p className="text-sm text-gray-900">
<span className="font-medium">{activity.user.name}</span>{' '}
{getActivityText(activity)}
</p>
<p className="text-xs text-gray-500">
{formatRelativeTime(activity.timestamp)}
</p>
</div>
</div>
))}
</div>
)
}
const getActivityText = (activity: Activity): string => {
switch (activity.type) {
case 'conversation_created':
return 'created a new conversation'
case 'conversation_updated':
return 'updated the conversation'
case 'comment_added':
return 'added a comment'
case 'user_joined':
return 'joined the workspace'
default:
return 'performed an action'
}
}
Testing
Collaboration Tests
// tests/collaboration/UserPresence.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { UserPresence } from '@/components/collaboration/UserPresence'
import { WebSocketProvider } from '@/contexts/WebSocketContext'
const mockWebSocket = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
connected: true
}
describe('UserPresence', () => {
it('displays online users', async () => {
const mockUsers = [
{ id: '1', name: 'John Doe', activity: 'viewing conversation' },
{ id: '2', name: 'Jane Smith', activity: 'editing conversation' }
]
render(
<WebSocketProvider value={mockWebSocket}>
<UserPresence conversationId="test-conversation" />
</WebSocketProvider>
)
// Simulate presence update
const presenceHandler = mockWebSocket.on.mock.calls.find(
call => call[0] === 'presence_update'
)?.[1]
if (presenceHandler) {
presenceHandler({ users: mockUsers })
}
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
})
})
})
Success Criteria
Must-Have Features
- Real-time Updates - Live collaboration on conversations
- User Presence - Show online users and their activity
- Comment System - Threaded discussions on conversations
- Permission Management - Role-based access control
- Shared Workspaces - Team-based conversation organization
- Activity Feed - Real-time activity updates
- WebSocket Integration - Stable real-time communication
- Error Handling - Graceful handling of connection issues
Bonus Challenges
Advanced Features
- Video Calls - Integrated video conferencing
- Screen Sharing - Share screens during collaboration
- File Sharing - Upload and share files in conversations
- Notifications - Push notifications for important events
- Conflict Resolution - Handle simultaneous edits
- Version History - Track changes over time
- Advanced Permissions - Granular permission control
- Team Analytics - Usage and collaboration metrics
Getting Started
Setup Instructions
- Set up WebSocket server - Add WebSocket support to FastAPI
- Choose your frontend framework - React, Vue, or Angular
- Implement real-time communication - Socket.io or native WebSocket
- Add presence management - Track user online status
- Build comment system - Threaded discussions
- Implement permissions - Role-based access control
- Add shared workspaces - Team-based organization
- Test collaboration - Multi-user testing
Resources
Helpful Links
- Socket.io - https://socket.io/
- WebSocket API - https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
- Real-time Collaboration - https://web.dev/real-time-collaboration/
- Presence Systems - https://pusher.com/guides/presence-channels
- Permission Management - https://auth0.com/blog/role-based-access-control-rbac/
Let's Build!
Ready to Start?
This assignment will teach you: - Real-time web application development - WebSocket communication - User presence management - Collaborative editing - Permission systems - Team workspace design
Start with basic WebSocket integration and build up from there!
Next Steps
After Completing This Assignment
- Test with multiple users - Ensure real-time features work correctly
- Deploy your app - Use a platform that supports WebSockets
- Share your code - Create a GitHub repository
- Document your approach - Write a comprehensive README
- Move to the next track - Try advanced UI patterns or mobile development next!
Happy coding! 🚀