We use cookies to enhance your experience on the site
CodeWorlds

Workspace and Channels - Team Communication

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.

Slack vs Other Platforms

Twitter/Instagram:

  • Public social media
  • Broadcasting to followers
  • Like, share, comment

LinkedIn:

  • Professional profiles
  • Job posts and networking
  • Formal posts

Slack:

  • Team communication (private workspaces)
  • Channels for projects/topics
  • Real-time chat and threads
  • File sharing and integrations
  • @mentions and notifications

Workspace Structure

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}

Message Structure

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}

Sidebar - Workspace Navigation

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}

Channel Header

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}

Summary 🎓

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! 🚀

Go to CodeWorlds