feat: realtime status, mentions, push notifications

This commit is contained in:
2026-06-29 15:52:34 -04:00
parent 282a436995
commit 6c8defb6fd
10 changed files with 387 additions and 34 deletions
+61 -5
View File
@@ -1,22 +1,43 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth.ts';
import { useAuthStore, type UserStatus } from '../stores/auth.ts';
import { useWebSocketStore } from '../stores/ws.ts';
import { ServerBar } from './ServerBar.tsx';
import { ChannelList } from './ChannelList.tsx';
import { MemberList } from './MemberList.tsx';
import { VoicePanel } from './VoicePanel.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 navigate = useNavigate();
const location = useLocation();
const [showStatusMenu, setShowStatusMenu] = useState(false);
useEffect(() => {
fetchMe();
@@ -30,6 +51,17 @@ export function Layout() {
}
}, [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">
@@ -48,9 +80,33 @@ export function Layout() {
<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>
<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>
<button onClick={() => logout().then(() => navigate('/login'))} className="terminal-button text-xs">
[LOGOUT]