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:
MythEclipse
2026-05-16 19:17:34 +07:00
co-authored by Claude Opus 4.7
parent 3c7d722973
commit 82025a19b2
51 changed files with 2865 additions and 1204 deletions
@@ -0,0 +1,35 @@
import type { ActiveSpeaker } from "../../types/voice";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
interface ActiveSpeakersProps {
speakers: ActiveSpeaker[];
}
export function ActiveSpeakers({ speakers }: ActiveSpeakersProps) {
return (
<Card>
<CardHeader>
<CardTitle>Active Speakers</CardTitle>
</CardHeader>
<CardContent>
{speakers.length === 0 ? (
<div className="rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
No active speakers.
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{speakers.map((speaker, index) => (
<div key={speaker.userId || speaker.id || index} className="flex items-center gap-3 rounded-xl border border-border bg-background/60 p-3">
<img src={speaker.avatar} alt="" className="h-10 w-10 rounded-full object-cover" />
<div className="min-w-0">
<div className="truncate text-sm font-medium">{speaker.username}</div>
<div className="text-xs text-emerald-300">Speaking</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,18 @@
interface AudioVisualizerProps {
levels: number[];
}
export function AudioVisualizer({ levels }: AudioVisualizerProps) {
const bars = levels.length ? levels : Array.from({ length: 32 }, () => 0.04);
return (
<div className="flex h-40 items-end gap-1 rounded-2xl border border-border bg-background/60 p-4">
{bars.map((level, index) => (
<div
key={`${index}-${level}`}
className="flex-1 rounded-full bg-gradient-to-t from-primary/50 to-cyan-300 transition-all duration-150"
style={{ height: `${Math.max(6, Math.min(100, level * 100))}%` }}
/>
))}
</div>
);
}
@@ -0,0 +1,76 @@
import type { Channel, Guild, VoiceStatus } from "../../types/voice";
import { Button } from "../ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select } from "../ui/select";
interface VoiceControlProps {
guilds: Guild[];
channels: Channel[];
selectedGuild: string;
selectedChannel: string;
status: VoiceStatus;
loading: boolean;
onGuildChange: (guildId: string) => void;
onChannelChange: (channelId: string) => void;
onJoin: () => void;
onDisconnect: () => void;
onListenToggle: () => void;
isListening: boolean;
}
export function VoiceControl({
guilds,
channels,
selectedGuild,
selectedChannel,
status,
loading,
onGuildChange,
onChannelChange,
onJoin,
onDisconnect,
onListenToggle,
isListening,
}: VoiceControlProps) {
return (
<Card>
<CardHeader>
<CardTitle>Voice Bridge</CardTitle>
<CardDescription>Join a Discord voice channel and monitor audio in real time.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Guild</label>
<Select
value={selectedGuild}
onChange={(event) => onGuildChange(event.target.value)}
placeholder="Select guild"
options={guilds.map((guild) => ({ value: guild.id, label: guild.name }))}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Voice Channel</label>
<Select
value={selectedChannel}
onChange={(event) => onChannelChange(event.target.value)}
placeholder="Select voice channel"
options={channels.map((channel) => ({ value: channel.id, label: channel.name }))}
/>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button disabled={!selectedGuild || !selectedChannel || loading} onClick={onJoin}>
{status.connected ? "Reconnect" : "Join Voice"}
</Button>
<Button variant="destructive" disabled={!status.connected || loading} onClick={onDisconnect}>
Disconnect
</Button>
<Button variant={isListening ? "secondary" : "outline"} onClick={onListenToggle}>
{isListening ? "Stop Listening" : "Listen Live"}
</Button>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,41 @@
import type { ActiveSpeaker, Channel, Guild, VoiceStatus } from "../../types/voice";
import { AudioVisualizer } from "./AudioVisualizer";
import { ActiveSpeakers } from "./ActiveSpeakers";
import { VoiceControl } from "./VoiceControl";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
interface VoicePanelProps {
guilds: Guild[];
channels: Channel[];
selectedGuild: string;
selectedChannel: string;
status: VoiceStatus;
loading: boolean;
activeSpeakers: ActiveSpeaker[];
levels: number[];
isListening: boolean;
onGuildChange: (guildId: string) => void;
onChannelChange: (channelId: string) => void;
onJoin: () => void;
onDisconnect: () => void;
onListenToggle: () => void;
}
export function VoicePanel(props: VoicePanelProps) {
return (
<div className="grid gap-6">
<VoiceControl {...props} />
<div className="grid gap-6 xl:grid-cols-[1fr_360px]">
<Card>
<CardHeader>
<CardTitle>Live Audio Visualizer</CardTitle>
</CardHeader>
<CardContent>
<AudioVisualizer levels={props.levels} />
</CardContent>
</Card>
<ActiveSpeakers speakers={props.activeSpeakers} />
</div>
</div>
);
}