feat: migrate and redesign dashboard to modern React
- Full rewrite of legacy vanilla JS UI into React SPA - Implement modern design system using Tailwind CSS and shadcn/ui primitives - Create typed API modules and hooks for voice, media, and moderation - Add new features: separated Music and Screen Share panels, Image Grid - Implement unified WebSocket hook for real-time state and PCM audio - Improve visualizer with smooth CSS transitions and live state sync - Add __dirname polyfill for ES module compatibility - Ensure responsive layout for mobile and desktop Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3c7d722973
commit
82025a19b2
@@ -0,0 +1,28 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { DashboardTab } from "../../types/ui";
|
||||
import type { VoiceStatus } from "../../types/voice";
|
||||
import type { WebSocketStatus } from "../../hooks/useDashboardSocket";
|
||||
import { Header } from "./Header";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
activeTab: DashboardTab;
|
||||
wsStatus: WebSocketStatus;
|
||||
voiceStatus: VoiceStatus;
|
||||
onTabChange: (tab: DashboardTab) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardLayout({ activeTab, wsStatus, voiceStatus, onTabChange, children }: DashboardLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar activeTab={activeTab} onTabChange={onTabChange} />
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<Header activeTab={activeTab} wsStatus={wsStatus} voiceStatus={voiceStatus} />
|
||||
<div className="flex-1 overflow-auto p-4 md:p-6 lg:p-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Wifi, WifiOff } from "lucide-react";
|
||||
import type { WebSocketStatus } from "../../hooks/useDashboardSocket";
|
||||
import type { DashboardTab } from "../../types/ui";
|
||||
import type { VoiceStatus } from "../../types/voice";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
const titles: Record<DashboardTab, string> = {
|
||||
voice: "Voice Control",
|
||||
media: "Media Player",
|
||||
messages: "Messages",
|
||||
review: "Moderation Review",
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
activeTab: DashboardTab;
|
||||
wsStatus: WebSocketStatus;
|
||||
voiceStatus: VoiceStatus;
|
||||
}
|
||||
|
||||
export function Header({ activeTab, wsStatus, voiceStatus }: HeaderProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-10 border-b border-border bg-background/80 px-4 py-4 backdrop-blur md:px-8">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{titles[activeTab]}</h1>
|
||||
<p className="text-sm text-muted-foreground">Voice, media, and moderation in one dashboard.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={wsStatus === "connected" ? "success" : wsStatus === "error" ? "destructive" : "warning"}>
|
||||
{wsStatus === "connected" ? <Wifi className="mr-1 h-3 w-3" /> : <WifiOff className="mr-1 h-3 w-3" />}
|
||||
WebSocket {wsStatus}
|
||||
</Badge>
|
||||
<Badge variant={voiceStatus.connected ? "success" : "secondary"}>
|
||||
Voice {voiceStatus.connected ? voiceStatus.activeChannelName || "connected" : "idle"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Bot, MessageSquare, Music2, ShieldAlert, Volume2 } from "lucide-react";
|
||||
import type { DashboardTab } from "../../types/ui";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Volume2 }> = [
|
||||
{ id: "voice", label: "Voice", icon: Volume2 },
|
||||
{ id: "media", label: "Media", icon: Music2 },
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "review", label: "Review", icon: ShieldAlert },
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
activeTab: DashboardTab;
|
||||
onTabChange: (tab: DashboardTab) => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ activeTab, onTabChange }: SidebarProps) {
|
||||
return (
|
||||
<aside className="hidden w-72 shrink-0 border-r border-border bg-card/60 p-5 backdrop-blur md:block">
|
||||
<div className="mb-8 flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-primary/15 text-primary">
|
||||
<Bot className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold tracking-tight">Bete Watcher</div>
|
||||
<div className="text-xs text-muted-foreground">Discord control center</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={activeTab === item.id ? "secondary" : "ghost"}
|
||||
className={cn("w-full justify-start", activeTab === item.id && "bg-primary/15 text-primary")}
|
||||
onClick={() => onTabChange(item.id)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user