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,68 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/auth.ts';
|
||||
import { ServerBar } from './ServerBar.tsx';
|
||||
import { ChannelList } from './ChannelList.tsx';
|
||||
import { MemberList } from './MemberList.tsx';
|
||||
|
||||
export function Layout() {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const isLoading = useAuthStore((state) => state.isLoading);
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const fetchMe = useAuthStore((state) => state.fetchMe);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
fetchMe();
|
||||
}, [fetchMe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated && location.pathname !== '/login') {
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
}, [isLoading, isAuthenticated, location.pathname, navigate]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gb-bg text-gb-fg-f font-mono">
|
||||
[booting...]
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gb-bg text-gb-fg font-mono flex flex-col p-2">
|
||||
<div className="flex-1 terminal-border bg-gb-bg-h flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between px-3 py-1 border-b border-gb-bg-t bg-gb-bg-s">
|
||||
<span className="text-gb-orange font-bold">DUMPSTER</span>
|
||||
<div className="flex items-center gap-4 text-xs text-gb-fg-s">
|
||||
<span>
|
||||
USER: <span className="text-gb-aqua">{user?.username || 'unknown'}</span>
|
||||
</span>
|
||||
<button onClick={() => logout().then(() => navigate('/login'))} className="terminal-button text-xs">
|
||||
[LOGOUT]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex min-h-0">
|
||||
<ServerBar />
|
||||
<ChannelList />
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
<MemberList />
|
||||
</div>
|
||||
<div className="px-3 py-1 border-t border-gb-bg-t text-xs text-gb-fg-f flex justify-between bg-gb-bg-s">
|
||||
<span>TERM v1.0</span>
|
||||
<span>{new Date().toISOString().slice(0, 10)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user