We use cookies to enhance your experience on the site
CodeWorlds

Servers and Channels - Community Structure

Welcome @name! Time to build Pirate's Cove - a community platform for crews and pirate guilds! 🎮

This will be like Discord - servers with text/voice channels, roles, permissions, and gaming-focused chat.

Discord vs Slack

Slack:

  • Workspace for companies/teams
  • Professional communication
  • Threading and focus on async chat
  • Integrations with business tools

Discord:

  • Servers for communities/gaming
  • Casual communication
  • Voice channels and screen share
  • Roles, permissions, emotes
  • Gaming integrations

Server Structure

1interface Server {
2  id: string;
3  name: string; // "Black Pearl Guild"
4  icon: string;
5  banner?: string;
6
7  owner: string; // user ID
8  members: Member[];
9
10  categories: Category[];
11  channels: Channel[];
12
13  roles: Role[];
14  emojis: Emoji[];
15
16  createdAt: Date;
17}
18
19interface Category {
20  id: string;
21  name: string; // "TEXT CHANNELS", "VOICE CHANNELS"
22  position: number;
23  channels: string[]; // channel IDs
24}
25
26interface Channel {
27  id: string;
28  serverId: string;
29  categoryId: string;
30
31  name: string;
32  type: 'text' | 'voice' | 'announcement';
33  topic?: string;
34
35  position: number;
36  isNSFW: boolean;
37
38  // Permissions
39  permissions: ChannelPermission[];
40
41  // Voice specific
42  userLimit?: number; // max users in voice channel
43  bitrate?: number;
44
45  createdAt: Date;
46}
47
48interface ChannelPermission {
49  roleId: string;
50  allow: Permission[];
51  deny: Permission[];
52}
53
54type Permission =
55  | 'VIEW_CHANNEL'
56  | 'SEND_MESSAGES'
57  | 'CONNECT' // voice
58  | 'SPEAK' // voice
59  | 'MANAGE_MESSAGES'
60  | 'MANAGE_CHANNELS';

Role System

1interface Role {
2  id: string;
3  serverId: string;
4
5  name: string;
6  color: string; // hex color for role badge
7  position: number; // hierarchy
8
9  permissions: Permission[];
10
11  isHoisted: boolean; // show separately in member list
12  isMentionable: boolean; // can be @mentioned
13
14  memberCount: number;
15  createdAt: Date;
16}
17
18interface Member {
19  userId: string;
20  user: {
21    id: string;
22    username: string;
23    discriminator: string; // "#1234"
24    avatar: string;
25    banner?: string;
26    status: 'online' | 'idle' | 'dnd' | 'offline';
27    customStatus?: string;
28  };
29
30  // Server-specific
31  nickname?: string; // override username
32  roles: string[]; // role IDs
33  joinedAt: Date;
34
35  // Voice state
36  voiceChannelId?: string;
37  isMuted: boolean;
38  isDeafened: boolean;
39}
40
41// Helper to get highest role
42function getHighestRole(member: Member, roles: Role[]): Role | null {
43  const memberRoles = roles
44    .filter(r => member.roles.includes(r.id))
45    .sort((a, b) => b.position - a.position);
46
47  return memberRoles[0] || null;
48}

Server Sidebar

1'use client';
2
3import { useState } from 'react';
4
5interface ServerSidebarProps {
6  server: Server;
7  currentUserId: string;
8  onChannelSelect: (channelId: string) => void;
9}
10
11export default function ServerSidebar({
12  server,
13  currentUserId,
14  onChannelSelect
15}: ServerSidebarProps) {
16  const [activeChannelId, setActiveChannelId] = useState<string | null>(null);
17  const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
18
19  const toggleCategory = (categoryId: string) => {
20    const newCollapsed = new Set(collapsedCategories);
21    if (newCollapsed.has(categoryId)) {
22      newCollapsed.delete(categoryId);
23    } else {
24      newCollapsed.add(categoryId);
25    }
26    setCollapsedCategories(newCollapsed);
27  };
28
29  const handleChannelClick = (channelId: string) => {
30    setActiveChannelId(channelId);
31    onChannelSelect(channelId);
32  };
33
34  return (
35    <div className="w-60 bg-gray-800 flex flex-col h-screen">
36      {/* Server Header */}
37      <div className="h-12 px-4 flex items-center justify-between border-b border-gray-900 shadow-md hover:bg-gray-750 cursor-pointer">
38        <h2 className="font-bold text-white">{server.name}</h2>
39        <button className="text-gray-400 hover:text-white"></button>
40      </div>
41
42      {/* Channels */}
43      <div className="flex-1 overflow-y-auto pt-4">
44        {server.categories
45          .sort((a, b) => a.position - b.position)
46          .map(category => {
47            const categoryChannels = server.channels
48              .filter(ch => ch.categoryId === category.id)
49              .sort((a, b) => a.position - b.position);
50
51            const isCollapsed = collapsedCategories.has(category.id);
52
53            return (
54              <div key={category.id} className="mb-4">
55                {/* Category Header */}
56                <button
57                  onClick={() => toggleCategory(category.id)}
58                  className="w-full px-2 py-1 flex items-center gap-1 text-xs font-semibold text-gray-400 uppercase hover:text-gray-300"
59                >
60                  <span className={`transform transition ${isCollapsed ? '' : 'rotate-90'}`}>
6162                  </span>
63                  {category.name}
64                </button>
65
66                {/* Channels */}
67                {!isCollapsed && (
68                  <div className="mt-1">
69                    {categoryChannels.map(channel => (
70                      <button
71                        key={channel.id}
72                        onClick={() => handleChannelClick(channel.id)}
73                        className={`w-full px-2 py-1 mx-2 rounded flex items-center gap-2 text-gray-400 hover:bg-gray-700 hover:text-gray-300 ${
74                          activeChannelId === channel.id
75                            ? 'bg-gray-700 text-white'
76                            : ''
77                        }`}
78                      >
79                        {channel.type === 'text' && <span className="text-gray-500">#</span>}
80                        {channel.type === 'voice' && <span className="text-gray-500">🔊</span>}
81                        {channel.type === 'announcement' && <span className="text-gray-500">📢</span>}
82                        <span className="text-sm flex-1 text-left truncate">
83                          {channel.name}
84                        </span>
85                        {channel.isNSFW && (
86                          <span className="text-xs bg-red-500 px-1 rounded">NSFW</span>
87                        )}
88                      </button>
89                    ))}
90                  </div>
91                )}
92              </div>
93            );
94          })}
95      </div>
96
97      {/* Voice Status */}
98      <div className="bg-gray-900 p-2">
99        {(() => {
100          const currentMember = server.members.find(m => m.userId === currentUserId);
101          if (!currentMember) return null;
102
103          return (
104            <div className="flex items-center gap-2 bg-gray-800 rounded p-2">
105              <div className="relative">
106                <img
107                  src={currentMember.user.avatar}
108                  alt={currentMember.user.username}
109                  className="w-8 h-8 rounded-full"
110                />
111                <div
112                  className={`absolute bottom-0 right-0 w-3 h-3 rounded-full border-2 border-gray-800 ${
113                    currentMember.user.status === 'online' ? 'bg-green-500' :
114                    currentMember.user.status === 'idle' ? 'bg-yellow-500' :
115                    currentMember.user.status === 'dnd' ? 'bg-red-500' : 'bg-gray-500'
116                  }`}
117                />
118              </div>
119              <div className="flex-1 min-w-0">
120                <div className="text-white text-sm font-semibold truncate">
121                  {currentMember.nickname || currentMember.user.username}
122                </div>
123                <div className="text-gray-400 text-xs truncate">
124                  {currentMember.user.customStatus || `#${currentMember.user.discriminator}`}
125                </div>
126              </div>
127              <div className="flex gap-1">
128                <button className="p-1.5 hover:bg-gray-700 rounded">🎤</button>
129                <button className="p-1.5 hover:bg-gray-700 rounded">🎧</button>
130                <button className="p-1.5 hover:bg-gray-700 rounded">⚙️</button>
131              </div>
132            </div>
133          );
134        })()}
135      </div>
136    </div>
137  );
138}

Server List (Left Sidebar)

1'use client';
2
3interface ServerListProps {
4  servers: Server[];
5  activeServerId: string;
6  onServerSelect: (serverId: string) => void;
7  onCreateServer: () => void;
8}
9
10export default function ServerList({
11  servers,
12  activeServerId,
13  onServerSelect,
14  onCreateServer
15}: ServerListProps) {
16  return (
17    <div className="w-18 bg-gray-900 flex flex-col items-center py-3 gap-2 h-screen overflow-y-auto">
18      {/* Home Button */}
19      <button className="w-12 h-12 bg-gray-700 hover:bg-blue-600 rounded-full flex items-center justify-center text-white transition-all hover:rounded-2xl">
20        🏠
21      </button>
22
23      <div className="w-8 h-0.5 bg-gray-700 rounded" />
24
25      {/* Servers */}
26      {servers.map(server => (
27        <button
28          key={server.id}
29          onClick={() => onServerSelect(server.id)}
30          className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all hover:rounded-2xl relative ${
31            activeServerId === server.id
32              ? 'rounded-2xl'
33              : ''
34          }`}
35          style={{
36            backgroundColor: server.icon ? 'transparent' : '#5865F2'
37          }}
38        >
39          {server.icon ? (
40            <img src={server.icon} alt={server.name} className="w-full h-full rounded-full hover:rounded-2xl" />
41          ) : (
42            <span className="text-white font-bold">
43              {server.name.charAt(0).toUpperCase()}
44            </span>
45          )}
46
47          {/* Active Indicator */}
48          {activeServerId === server.id && (
49            <div className="absolute left-0 w-1 h-10 bg-white rounded-r" />
50          )}
51        </button>
52      ))}
53
54      {/* Add Server */}
55      <button
56        onClick={onCreateServer}
57        className="w-12 h-12 bg-gray-700 hover:bg-green-600 rounded-full flex items-center justify-center text-white text-2xl transition-all hover:rounded-2xl"
58      >
59        +
60      </button>
61    </div>
62  );
63}

Member List (Right Sidebar)

1interface MemberListProps {
2  server: Server;
3  onMemberClick: (memberId: string) => void;
4}
5
6export default function MemberList({ server, onMemberClick }: MemberListProps) {
7  // Group members by highest role
8  const membersByRole = new Map<string, Member[]>();
9
10  // Add "Online" and "Offline" default groups
11  membersByRole.set('online', []);
12  membersByRole.set('offline', []);
13
14  server.members.forEach(member => {
15    const highestRole = getHighestRole(member, server.roles);
16
17    if (member.user.status === 'offline') {
18      membersByRole.get('offline')!.push(member);
19    } else if (highestRole && highestRole.isHoisted) {
20      if (!membersByRole.has(highestRole.id)) {
21        membersByRole.set(highestRole.id, []);
22      }
23      membersByRole.get(highestRole.id)!.push(member);
24    } else {
25      membersByRole.get('online')!.push(member);
26    }
27  });
28
29  return (
30    <div className="w-60 bg-gray-800 h-screen overflow-y-auto">
31      <div className="p-4">
32        {Array.from(membersByRole.entries()).map(([roleId, members]) => {
33          if (members.length === 0) return null;
34
35          const role = server.roles.find(r => r.id === roleId);
36          const groupName = roleId === 'online' ? 'ONLINE' :
37                           roleId === 'offline' ? 'OFFLINE' :
38                           role?.name.toUpperCase() || 'MEMBERS';
39
40          const groupColor = role?.color;
41
42          return (
43            <div key={roleId} className="mb-4">
44              <div
45                className="text-xs font-semibold mb-2 px-2"
46                style={{ color: groupColor || '#96989D' }}
47              >
48                {groupName}{members.length}
49              </div>
50
51              <div>
52                {members.map(member => {
53                  const displayName = member.nickname || member.user.username;
54                  const memberRole = getHighestRole(member, server.roles);
55
56                  return (
57                    <button
58                      key={member.userId}
59                      onClick={() => onMemberClick(member.userId)}
60                      className="w-full flex items-center gap-2 px-2 py-1 hover:bg-gray-700 rounded group"
61                    >
62                      <div className="relative">
63                        <img
64                          src={member.user.avatar}
65                          alt={displayName}
66                          className="w-8 h-8 rounded-full"
67                        />
68                        <div
69                          className={`absolute bottom-0 right-0 w-3 h-3 rounded-full border-2 border-gray-800 ${
70                            member.user.status === 'online' ? 'bg-green-500' :
71                            member.user.status === 'idle' ? 'bg-yellow-500' :
72                            member.user.status === 'dnd' ? 'bg-red-500' : 'bg-gray-500'
73                          }`}
74                        />
75                      </div>
76
77                      <div className="flex-1 min-w-0 text-left">
78                        <div
79                          className="text-sm font-medium truncate"
80                          style={{
81                            color: memberRole?.color || '#DCDDDE'
82                          }}
83                        >
84                          {displayName}
85                        </div>
86                        {member.user.customStatus && (
87                          <div className="text-xs text-gray-400 truncate">
88                            {member.user.customStatus}
89                          </div>
90                        )}
91                      </div>
92                    </button>
93                  );
94                })}
95              </div>
96            </div>
97          );
98        })}
99      </div>
100    </div>
101  );
102}

Summary 🎓

Server structure - categories, channels, roles, members ✅ Channel types - text, voice, announcement ✅ Role system - colors, permissions, hierarchy, hoisted ✅ Member management - nicknames, roles, voice state ✅ Server sidebar - collapsible categories, channel list ✅ Server list - left sidebar with server icons ✅ Member list - grouped by roles, status indicators ✅ Permissions - channel-specific and role-based

In the next exercise we'll add voice channels and rich presence!

See you next time! 🚀

Go to CodeWorlds