171 lines
6.9 KiB
TypeScript
171 lines
6.9 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAuthStore, type UserStatus } from '../stores/auth.ts';
|
|
import { useWebSocketStore } from '../stores/ws.ts';
|
|
import { useServerStore } from '../stores/server.ts';
|
|
import { ServerBar } from './ServerBar.tsx';
|
|
import { ChannelList } from './ChannelList.tsx';
|
|
import { ConversationList } from './ConversationList.tsx';
|
|
import { MemberList } from './MemberList.tsx';
|
|
import { VoicePanel } from './VoicePanel.tsx';
|
|
import { ServerSettingsModal } from './ServerSettingsModal.tsx';
|
|
const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline'];
|
|
function statusColor(status: UserStatus): string {
|
|
switch (status) {
|
|
case 'online':
|
|
return 'bg-gb-green';
|
|
case 'idle':
|
|
return 'bg-gb-yellow';
|
|
case 'dnd':
|
|
return 'bg-gb-red';
|
|
default:
|
|
return 'bg-gb-gray';
|
|
}
|
|
}
|
|
function statusLabel(status: UserStatus): string {
|
|
return status.toUpperCase();
|
|
}
|
|
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 updateProfile = useAuthStore((state) => state.updateProfile);
|
|
const wsConnect = useWebSocketStore((s) => s.connect);
|
|
const wsDisconnect = useWebSocketStore((s) => s.disconnect);
|
|
const setActiveServer = useServerStore((s) => s.setActiveServer);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [showStatusMenu, setShowStatusMenu] = useState(false);
|
|
const [dmMode, setDmMode] = useState(false);
|
|
const [showServerSettings, setShowServerSettings] = useState(false);
|
|
const activeServerId = useServerStore((s) => s.activeServerId);
|
|
useEffect(() => {
|
|
fetchMe();
|
|
wsConnect();
|
|
return () => { wsDisconnect(); };
|
|
}, [fetchMe, wsConnect, wsDisconnect]);
|
|
useEffect(() => {
|
|
if (!isLoading && !isAuthenticated && location.pathname !== '/login') {
|
|
navigate('/login', { replace: true });
|
|
}
|
|
}, [isLoading, isAuthenticated, location.pathname, navigate]);
|
|
const handleStatusChange = async (status: UserStatus) => {
|
|
setShowStatusMenu(false);
|
|
try {
|
|
await updateProfile({ status });
|
|
} catch (err) {
|
|
// Error is surfaced via auth store; menu closes optimistically.
|
|
}
|
|
};
|
|
const currentStatus = user?.status ?? 'offline';
|
|
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">
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowStatusMenu((prev) => !prev)}
|
|
className="flex items-center gap-2 hover:text-gb-fg transition-colors"
|
|
title="Change status"
|
|
>
|
|
<span className={`w-2.5 h-2.5 rounded-full ${statusColor(currentStatus)}`} />
|
|
<span className="text-gb-aqua">{user?.username || 'unknown'}</span>
|
|
<span className="text-gb-fg-f">[{statusLabel(currentStatus)}]</span>
|
|
</button>
|
|
{showStatusMenu && (
|
|
<div className="absolute right-0 top-full mt-1 z-50 w-32 bg-gb-bg-s border border-gb-bg-t shadow-lg">
|
|
{STATUS_CYCLE.map((s) => (
|
|
<button
|
|
key={s}
|
|
type="button"
|
|
onClick={() => handleStatusChange(s)}
|
|
className="w-full px-2 py-1 text-left text-xs font-mono flex items-center gap-2 hover:bg-gb-orange hover:text-gb-bg transition-colors"
|
|
>
|
|
<span className={`w-2 h-2 rounded-full ${statusColor(s)}`} />
|
|
<span>{statusLabel(s)}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Link to="/settings" className="terminal-button text-xs">[SETTINGS]</Link>
|
|
{activeServerId && (
|
|
<button
|
|
onClick={() => setShowServerSettings(true)}
|
|
className="terminal-button text-xs"
|
|
>
|
|
[SERVER SETTINGS]
|
|
</button>
|
|
)}
|
|
<button onClick={() => logout().then(() => navigate('/login'))} className="terminal-button text-xs">
|
|
[LOGOUT]
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 flex min-h-0">
|
|
<div className="flex flex-col items-center w-16 bg-gb-bg-h border-r border-gb-bg-t py-2 gap-2 shrink-0">
|
|
<button
|
|
onClick={() => setDmMode(false)}
|
|
className={`w-11 h-11 flex items-center justify-center font-mono text-xs border rounded ${
|
|
!dmMode
|
|
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
|
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t'
|
|
}`}
|
|
title="Servers"
|
|
>
|
|
[S]
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setDmMode(true);
|
|
setActiveServer(null);
|
|
}}
|
|
className={`w-11 h-11 flex items-center justify-center font-mono text-xs border rounded ${
|
|
dmMode
|
|
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
|
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t'
|
|
}`}
|
|
title="Direct Messages"
|
|
>
|
|
[@]
|
|
</button>
|
|
</div>
|
|
<ServerBar />
|
|
{dmMode ? <ConversationList /> : <ChannelList />}
|
|
<div className="flex-1 min-w-0 flex flex-col">
|
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
<VoicePanel />
|
|
<div className="flex-1 min-h-0">
|
|
<Outlet />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!dmMode && <MemberList />}
|
|
</div>
|
|
{showServerSettings && activeServerId && (
|
|
<ServerSettingsModal serverId={activeServerId} onClose={() => setShowServerSettings(false)} />
|
|
)}
|
|
<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>
|
|
);
|
|
}
|