Phase 1 MVP: gateway, CRUD handlers, frontend components
Backend: - WebSocket gateway (hub, client, events) with fanout broadcast - Server CRUD handlers (create, list, get, update, delete) - Channel CRUD handlers (create, list, get, update, delete) - Message CRUD handlers (list with cursor pagination, create, update, delete) - cmd/migrate standalone migration CLI (up/down) - cmd/server wired to all handlers + WebSocket + static file serving Frontend: - Zustand stores: auth, server, channel, message, websocket - API client with fetch wrapper - Terminal-styled components: Layout, LoginForm, ChatArea, ChannelList, ServerBar, MemberList - React Router with login and main routes - Gruvbox dark palette throughout Ops: - Docker Compose with app service (multi-stage build) - Caddyfile with WebSocket upgrade support - Makefile for common tasks
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { User, UserStatus } from '../stores/auth.ts';
|
||||
|
||||
interface MemberListProps {
|
||||
members?: User[];
|
||||
}
|
||||
|
||||
function statusIcon(status: UserStatus): string {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return '\u25cf';
|
||||
case 'idle':
|
||||
return '\u25d0';
|
||||
case 'dnd':
|
||||
return '\u26d5';
|
||||
default:
|
||||
return '\u25cb';
|
||||
}
|
||||
}
|
||||
|
||||
function statusColor(status: UserStatus): string {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'text-gb-green';
|
||||
case 'idle':
|
||||
return 'text-gb-yellow';
|
||||
case 'dnd':
|
||||
return 'text-gb-red';
|
||||
default:
|
||||
return 'text-gb-gray';
|
||||
}
|
||||
}
|
||||
|
||||
function usernameColor(status: UserStatus): string {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'text-gb-aqua';
|
||||
case 'idle':
|
||||
return 'text-gb-yellow';
|
||||
case 'dnd':
|
||||
return 'text-gb-red';
|
||||
default:
|
||||
return 'text-gb-fg-f';
|
||||
}
|
||||
}
|
||||
|
||||
export function MemberList({ members = [] }: MemberListProps) {
|
||||
const online = members.filter((m) => m.status !== 'offline');
|
||||
const offline = members.filter((m) => m.status === 'offline');
|
||||
|
||||
return (
|
||||
<div className="h-full w-52 bg-gb-bg-s border-l border-gb-bg-t flex flex-col">
|
||||
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s">
|
||||
[MEMBERS]
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2 font-mono text-sm">
|
||||
{members.length === 0 && (
|
||||
<p className="text-gb-fg-f">[no members]</p>
|
||||
)}
|
||||
{online.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="text-gb-fg-t text-xs uppercase mb-1">ONLINE</div>
|
||||
<div className="text-gb-fg-f text-xs mb-1">---</div>
|
||||
{online.map((member) => (
|
||||
<div key={member.id} className="flex items-center gap-2 px-2 py-1">
|
||||
<span className={statusColor(member.status)}>{statusIcon(member.status)}</span>
|
||||
<span className={`truncate ${usernameColor(member.status)}`}>
|
||||
{member.username}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{offline.length > 0 && (
|
||||
<div>
|
||||
<div className="text-gb-fg-t text-xs uppercase mb-1">OFFLINE</div>
|
||||
<div className="text-gb-fg-f text-xs mb-1">---</div>
|
||||
{offline.map((member) => (
|
||||
<div key={member.id} className="flex items-center gap-2 px-2 py-1">
|
||||
<span className={statusColor(member.status)}>{statusIcon(member.status)}</span>
|
||||
<span className={`truncate ${usernameColor(member.status)}`}>
|
||||
{member.username}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user