Files
dumpsterChat/web/src/components/ChannelList.tsx
T
hobokenchicken 3f33859f4a feat: UI for server/channel creation, settings link, role management backend
Frontend:
- Settings link in header next to logout
- [+] button to create server (name + optional icon)
- [#] button to join server via invite code
- [+] button to create channel (name, type, category)
- Server name shown instead of UUID in channel list header
- /invites/:code route wired for JoinServer component

Backend:
- Wire role CRUD + member-role assignment routes (MANAGE_SERVER gated)
- Seed @everyone default role on server creation
2026-06-29 12:50:46 -04:00

96 lines
3.6 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useServerStore } from '../stores/server.ts';
import { useChannelStore } from '../stores/channel.ts';
import { VoiceChannel } from './VoiceChannel.tsx';
import { CreateChannelModal } from './CreateChannelModal.tsx';
export function ChannelList() {
const activeServerId = useServerStore((state) => state.activeServerId);
const servers = useServerStore((state) => state.servers);
const channelsByServer = useChannelStore((state) => state.channelsByServer);
const fetchChannels = useChannelStore((state) => state.fetchChannels);
const activeChannelId = useChannelStore((state) => state.activeChannelId);
const setActiveChannel = useChannelStore((state) => state.setActiveChannel);
const [showCreate, setShowCreate] = useState(false);
useEffect(() => {
if (activeServerId) {
fetchChannels(activeServerId);
}
}, [activeServerId, fetchChannels]);
const channels = useMemo(() => {
return activeServerId ? channelsByServer[activeServerId] || [] : [];
}, [activeServerId, channelsByServer]);
const activeServer = useMemo(() => {
if (!activeServerId) return null;
return servers.find((s) => s.id === activeServerId) || null;
}, [servers, activeServerId]);
const categories = useMemo(() => {
const map = new Map<string, typeof channels>();
for (const channel of channels) {
const category = channel.category || 'TEXT CHANNELS';
const list = map.get(category) || [];
list.push(channel);
map.set(category, list);
}
for (const list of map.values()) {
list.sort((a, b) => a.position - b.position);
}
return Array.from(map.entries());
}, [channels]);
return (
<div className="h-full w-56 bg-gb-bg-s border-r border-gb-bg-t flex flex-col">
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between">
<span>{activeServerId ? `[SERVER ${activeServer?.name ?? activeServerId}]` : '[NO SERVER]'}</span>
{activeServerId && (
<button
onClick={() => setShowCreate(true)}
className="terminal-button text-xs"
title="Create channel"
>
[+]
</button>
)}
</div>
<div className="flex-1 overflow-y-auto p-2 font-mono text-sm">
{categories.length === 0 && (
<p className="text-gb-fg-f">[no channels]</p>
)}
{categories.map(([category, list]) => (
<div key={category} className="mb-3">
<div className="text-gb-fg-t text-xs uppercase mb-1">{category}</div>
<div className="text-gb-fg-f text-xs mb-1">---</div>
{list.map((channel) =>
channel.type === 'voice' ? (
<VoiceChannel
key={channel.id}
channelId={channel.id}
channelName={channel.name}
/>
) : (
<button
key={channel.id}
onClick={() => setActiveChannel(channel.id)}
className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${
channel.id === activeChannelId ? 'terminal-active' : 'hover:bg-gb-bg-t text-gb-fg-s'
}`}
>
<span className="text-gb-fg-f">#</span>
<span className="truncate">{channel.name}</span>
</button>
)
)}
</div>
))}
</div>
{showCreate && activeServerId && (
<CreateChannelModal serverId={activeServerId} onClose={() => setShowCreate(false)} />
)}
</div>
);
}