From 8884e29ce75c5694faef85cd373bdee735ce2d2f Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 21 May 2026 03:56:44 +0700 Subject: [PATCH] feat(recordings): add RecordingsPanel component for displaying voice recordings --- .gitignore | 2 +- .../components/recordings/RecordingsPanel.tsx | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/recordings/RecordingsPanel.tsx diff --git a/.gitignore b/.gitignore index 12dae15..26c2873 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -recordings +/recordings .env dist/ public/app/ diff --git a/frontend/src/components/recordings/RecordingsPanel.tsx b/frontend/src/components/recordings/RecordingsPanel.tsx new file mode 100644 index 0000000..932050f --- /dev/null +++ b/frontend/src/components/recordings/RecordingsPanel.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; + +interface VoiceRecording { + id: string; + user_id: string; + username: string; + avatar_url: string | null; + guild_id: string | null; + channel_id: string | null; + channel_name: string | null; + filename: string; + size_bytes: number; + download_url: string | null; + upload_status: "pending" | "uploaded" | "failed"; + upload_error: string | null; + created_at: number; + uploaded_at: number | null; +} + +function formatDate(value: number): string { + return new Date(value).toLocaleString(); +} + +function formatBytes(value: number): string { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / 1024 / 1024).toFixed(1)} MB`; +} + +export function RecordingsPanel() { + const [recordings, setRecordings] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + async function loadRecordings() { + try { + setLoading(true); + setError(null); + const response = await fetch("/api/recordings"); + if (!response.ok) { + throw new Error(`Failed to load recordings: ${response.status}`); + } + const data = (await response.json()) as VoiceRecording[]; + if (!cancelled) setRecordings(data); + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err.message : String(err)); + } + } finally { + if (!cancelled) setLoading(false); + } + } + + loadRecordings(); + window.addEventListener("voice_recording_uploaded", loadRecordings); + + return () => { + cancelled = true; + window.removeEventListener("voice_recording_uploaded", loadRecordings); + }; + }, []); + + return ( + + + Voice Recordings + + + {loading ? ( +
+ Loading recordings... +
+ ) : error ? ( +
+ {error} +
+ ) : recordings.length === 0 ? ( +
+ No recordings found. +
+ ) : ( +
+ {recordings.map((recording) => ( +
+
+
+
{recording.filename}
+
+ {recording.username} · {recording.channel_name ?? recording.channel_id ?? "unknown channel"} · {formatDate(recording.created_at)} +
+
+ {formatBytes(recording.size_bytes)} · {recording.upload_status} +
+ {recording.upload_error ? ( +
+ {recording.upload_error} +
+ ) : null} +
+ {recording.download_url ? ( + + Download + + ) : null} +
+
+ ))} +
+ )} +
+
+ ); +}