Welcome @name! Time to build Ship's Comms - a team communication system for pirate ship crews! 💬
This will be like Slack - organized into channels, direct messages, threads, and real-time chat for team collaboration.
Twitter/Instagram:
LinkedIn:
Slack:
1interface Workspace {
2 id: string;
3 name: string; // "Black Pearl Crew"
4 icon: string;
5 url: string; // "blackpearl.slack.com"
6
7 members: Member[];
8 channels: Channel[];
9 directMessages: DirectMessage[];
10
11 createdBy: string;
12 createdAt: Date;
13}
14
15interface Member {
16 id: string;
17 name: string;
18 avatar: string;
19 role: 'owner' | 'admin' | 'member' | 'guest';
20 status: 'online' | 'away' | 'offline';
21 statusText?: string; // "In a meeting"
22 timezone: string;
23}
24
25interface Channel {
26 id: string;
27 name: string; // "general", "treasure-hunts"
28 description: string;
29 isPrivate: boolean;
30
31 members: string[]; // member IDs
32 messages: Message[];
33
34 createdBy: string;
35 createdAt: Date;
36}
37
38interface DirectMessage {
39 id: string;
40 participants: string[]; // 2 member IDs
41 messages: Message[];
42 lastReadAt: { [userId: string]: Date };
43}1interface Message {
2 id: string;
3 channelId: string;
4 authorId: string;
5
6 content: string;
7 mentions: string[]; // @user IDs
8
9 attachments?: Attachment[];
10 reactions: Reaction[];
11
12 // Threading
13 threadId?: string; // parent message ID
14 replies: Message[];
15 replyCount: number;
16
17 isEdited: boolean;
18 isPinned: boolean;
19
20 timestamp: Date;
21}
22
23interface Attachment {
24 id: string;
25 type: 'image' | 'video' | 'file' | 'link';
26 url: string;
27 name: string;
28 size: number;
29 thumbnail?: string;
30}
31
32interface Reaction {
33 emoji: string;
34 users: string[]; // user IDs who reacted
35 count: number;
36}1'use client';
2
3import { useState } from 'react';
4
5interface SidebarProps {
6 workspace: Workspace;
7 currentUserId: string;
8 onChannelSelect: (channelId: string) => void;
9 onDMSelect: (dmId: string) => void;
10}
11
12export default function Sidebar({
13 workspace,
14 currentUserId,
15 onChannelSelect,
16 onDMSelect
17}: SidebarProps) {
18 const [showAllChannels, setShowAllChannels] = useState(false);
19
20 const unreadDMs = workspace.directMessages.filter(dm => {
21 const lastRead = dm.lastReadAt[currentUserId];
22 const lastMessage = dm.messages[dm.messages.length - 1];
23 return lastMessage && (!lastRead || lastMessage.timestamp > lastRead);
24 });
25
26 return (
27 <div className="w-64 bg-purple-900 text-white flex flex-col h-screen">
28 {/* Workspace Header */}
29 <div className="p-4 border-b border-purple-800">
30 <div className="flex items-center gap-2 mb-2">
31 <span className="text-2xl">{workspace.icon}</span>
32 <h2 className="font-bold text-lg">{workspace.name}</h2>
33 </div>
34 <div className="flex items-center gap-2 text-sm opacity-80">
35 <div className="w-2 h-2 bg-green-400 rounded-full"></div>
36 <span>{workspace.members.filter(m => m.status === 'online').length} online</span>
37 </div>
38 </div>
39
40 {/* Navigation */}
41 <div className="flex-1 overflow-y-auto p-2">
42 {/* Quick Links */}
43 <div className="mb-4">
44 <button className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded flex items-center gap-2">
45 <span>💬</span>
46 <span>Threads</span>
47 </button>
48 <button className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded flex items-center gap-2">
49 <span>💌</span>
50 <span>All DMs</span>
51 {unreadDMs.length > 0 && (
52 <span className="ml-auto bg-red-500 text-xs px-2 py-0.5 rounded-full">
53 {unreadDMs.length}
54 </span>
55 )}
56 </button>
57 <button className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded flex items-center gap-2">
58 <span>🔖</span>
59 <span>Saved items</span>
60 </button>
61 </div>
62
63 {/* Channels */}
64 <div className="mb-4">
65 <button
66 onClick={() => setShowAllChannels(!showAllChannels)}
67 className="w-full text-left px-3 py-1 text-sm font-semibold opacity-80 hover:opacity-100 flex items-center gap-1"
68 >
69 <span className={`transform transition ${showAllChannels ? 'rotate-90' : ''}`}>▶</span>
70 Channels
71 </button>
72 {showAllChannels && (
73 <div className="mt-1">
74 {workspace.channels.map(channel => (
75 <button
76 key={channel.id}
77 onClick={() => onChannelSelect(channel.id)}
78 className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded flex items-center gap-2"
79 >
80 <span>{channel.isPrivate ? '🔒' : '#'}</span>
81 <span>{channel.name}</span>
82 </button>
83 ))}
84 <button className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded text-sm opacity-80">
85 + Add channels
86 </button>
87 </div>
88 )}
89 </div>
90
91 {/* Direct Messages */}
92 <div>
93 <div className="px-3 py-1 text-sm font-semibold opacity-80">
94 Direct Messages
95 </div>
96 <div className="mt-1">
97 {workspace.directMessages.slice(0, 5).map(dm => {
98 const otherUser = workspace.members.find(
99 m => dm.participants.includes(m.id) && m.id !== currentUserId
100 );
101 if (!otherUser) return null;
102
103 return (
104 <button
105 key={dm.id}
106 onClick={() => onDMSelect(dm.id)}
107 className="w-full text-left px-3 py-1.5 hover:bg-purple-800 rounded flex items-center gap-2"
108 >
109 <div className="relative">
110 <img
111 src={otherUser.avatar}
112 alt={otherUser.name}
113 className="w-6 h-6 rounded"
114 />
115 <div
116 className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border border-purple-900 ${
117 otherUser.status === 'online' ? 'bg-green-400' :
118 otherUser.status === 'away' ? 'bg-yellow-400' : 'bg-gray-400'
119 }`}
120 />
121 </div>
122 <span>{otherUser.name}</span>
123 </button>
124 );
125 })}
126 </div>
127 </div>
128 </div>
129
130 {/* User Profile */}
131 <div className="p-3 border-t border-purple-800">
132 {(() => {
133 const currentUser = workspace.members.find(m => m.id === currentUserId);
134 if (!currentUser) return null;
135
136 return (
137 <div className="flex items-center gap-2">
138 <div className="relative">
139 <img
140 src={currentUser.avatar}
141 alt={currentUser.name}
142 className="w-8 h-8 rounded"
143 />
144 <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-purple-900"></div>
145 </div>
146 <div className="flex-1 min-w-0">
147 <div className="font-semibold text-sm truncate">{currentUser.name}</div>
148 <div className="text-xs opacity-80 truncate">
149 {currentUser.statusText || 'Active'}
150 </div>
151 </div>
152 <button className="hover:bg-purple-800 p-1 rounded">⚙️</button>
153 </div>
154 );
155 })()}
156 </div>
157 </div>
158 );
159}1interface ChannelHeaderProps {
2 channel: Channel;
3 memberCount: number;
4 onAddPeople: () => void;
5 onShowDetails: () => void;
6}
7
8export default function ChannelHeader({
9 channel,
10 memberCount,
11 onAddPeople,
12 onShowDetails
13}: ChannelHeaderProps) {
14 return (
15 <div className="border-b bg-white px-6 py-3">
16 <div className="flex items-center justify-between">
17 <div className="flex-1">
18 <div className="flex items-center gap-2 mb-1">
19 <h2 className="text-xl font-bold">
20 {channel.isPrivate ? '🔒' : '#'} {channel.name}
21 </h2>
22 <button className="hover:bg-gray-100 p-1 rounded">⭐</button>
23 </div>
24 <p className="text-sm text-gray-600">
25 {channel.description || 'No description'} • {memberCount} members
26 </p>
27 </div>
28
29 <div className="flex items-center gap-2">
30 <button
31 onClick={onAddPeople}
32 className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-50 text-sm font-semibold"
33 >
34 + Add people
35 </button>
36 <button
37 onClick={onShowDetails}
38 className="p-2 hover:bg-gray-100 rounded"
39 >
40 ℹ️
41 </button>
42 </div>
43 </div>
44 </div>
45 );
46}✅ Workspace structure - teams, members, channels, DMs ✅ Member roles - owner, admin, member, guest ✅ Channel types - public (#) and private (🔒) ✅ Sidebar navigation - channels, DMs, quick links ✅ Online status - green/yellow/gray indicators ✅ Unread counts - notification badges ✅ User profile - current user in the sidebar ✅ Channel header - description, member count, actions
In the next exercise we'll add messages, threads, and reactions!
See you next time! 🚀