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,34 @@
|
||||
import type { MediaState } from "../../types/media";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { MediaQueue } from "./MediaQueue";
|
||||
import { MusicPlayer } from "./MusicPlayer";
|
||||
import { ScreenShare } from "./ScreenShare";
|
||||
|
||||
interface MediaPanelProps {
|
||||
state: MediaState;
|
||||
loading: boolean;
|
||||
onQueueMusic: (source: string) => void;
|
||||
onStartScreen: (source: string) => void;
|
||||
onSkip: () => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
export function MediaPanel({ state, loading, onQueueMusic, onStartScreen, onSkip, onStop }: MediaPanelProps) {
|
||||
return (
|
||||
<div className="grid gap-6 xl:grid-cols-[1fr_380px]">
|
||||
<Tabs defaultValue="music" className="min-w-0">
|
||||
<TabsList>
|
||||
<TabsTrigger value="music">Music</TabsTrigger>
|
||||
<TabsTrigger value="screen">Screen Share</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="music">
|
||||
<MusicPlayer loading={loading} onQueue={onQueueMusic} onSkip={onSkip} onStop={onStop} />
|
||||
</TabsContent>
|
||||
<TabsContent value="screen">
|
||||
<ScreenShare loading={loading} onStart={onStartScreen} onSkip={onSkip} onStop={onStop} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<MediaQueue state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { MediaState } from "../../types/media";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||
|
||||
interface MediaQueueProps {
|
||||
state: MediaState;
|
||||
}
|
||||
|
||||
export function MediaQueue({ state }: MediaQueueProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Now Playing</CardTitle>
|
||||
<CardDescription>Current item and queue state.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{state.current ? (
|
||||
<div className="rounded-xl border border-primary/30 bg-primary/10 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{state.current.title}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{state.current.source}</div>
|
||||
</div>
|
||||
<Badge variant={state.current.mode === "screen" ? "warning" : "success"}>{state.current.mode || "music"}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">No media playing.</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Queue</div>
|
||||
{state.queue.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Queue is empty.</div>
|
||||
) : (
|
||||
state.queue.map((item, index) => (
|
||||
<div key={`${item.source}-${index}`} className="rounded-lg border border-border bg-background/60 p-3 text-sm">
|
||||
<div className="font-medium">{item.title}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{item.source}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Music2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
interface MusicPlayerProps {
|
||||
loading: boolean;
|
||||
onQueue: (source: string) => void;
|
||||
onSkip: () => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
export function MusicPlayer({ loading, onQueue, onSkip, onStop }: MusicPlayerProps) {
|
||||
const [source, setSource] = useState("");
|
||||
|
||||
const submit = () => {
|
||||
const trimmed = source.trim();
|
||||
if (!trimmed) return;
|
||||
onQueue(trimmed);
|
||||
setSource("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><Music2 className="h-5 w-5" /> Music Player</CardTitle>
|
||||
<CardDescription>Play YouTube, Spotify tracks, search terms, or local files as audio.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Input
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
onKeyDown={(event) => event.key === "Enter" && submit()}
|
||||
placeholder="YouTube URL, Spotify track, or search terms"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button disabled={loading || !source.trim()} onClick={submit}>Queue / Play</Button>
|
||||
<Button variant="secondary" disabled={loading} onClick={onSkip}>Skip</Button>
|
||||
<Button variant="destructive" disabled={loading} onClick={onStop}>Stop</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MonitorUp } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
interface ScreenShareProps {
|
||||
loading: boolean;
|
||||
onStart: (source: string) => void;
|
||||
onSkip: () => void;
|
||||
onStop: () => void;
|
||||
}
|
||||
|
||||
export function ScreenShare({ loading, onStart, onSkip, onStop }: ScreenShareProps) {
|
||||
const [source, setSource] = useState("");
|
||||
|
||||
const submit = () => {
|
||||
const trimmed = source.trim();
|
||||
if (!trimmed) return;
|
||||
onStart(trimmed);
|
||||
setSource("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><MonitorUp className="h-5 w-5" /> Screen Share</CardTitle>
|
||||
<CardDescription>Start screen-share playback from a URL or local file path.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Input
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
onKeyDown={(event) => event.key === "Enter" && submit()}
|
||||
placeholder="Screen share URL or local file path"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button disabled={loading || !source.trim()} onClick={submit}>Start Screen Share</Button>
|
||||
<Button variant="secondary" disabled={loading} onClick={onSkip}>Skip</Button>
|
||||
<Button variant="destructive" disabled={loading} onClick={onStop}>Stop</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user