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
This commit is contained in:
2026-06-29 12:50:46 -04:00
parent 3072cda051
commit 3f33859f4a
11 changed files with 778 additions and 27 deletions
+26 -4
View File
@@ -1,14 +1,17 @@
import { useEffect, useMemo } from 'react';
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) {
@@ -16,7 +19,14 @@ export function ChannelList() {
}
}, [activeServerId, fetchChannels]);
const channels = activeServerId ? channelsByServer[activeServerId] || [] : [];
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>();
@@ -34,8 +44,17 @@ export function ChannelList() {
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">
{activeServerId ? `[SERVER ${activeServerId}]` : '[NO SERVER]'}
<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 && (
@@ -68,6 +87,9 @@ export function ChannelList() {
</div>
))}
</div>
{showCreate && activeServerId && (
<CreateChannelModal serverId={activeServerId} onClose={() => setShowCreate(false)} />
)}
</div>
);
}