feat(fe): recordings — visible playing/loading/paused states

- recording-card: kartu aktif di-highlight (ring primary + glow pulse),
  badge 'Now Playing'/'Loading'/'Paused', tombol play berubah jadi Pause
  saat playing dan spinner saat loading, waveform equalizer beranimasi
  (animate-eq, delay per bar) saat playing / pulse saat loading.
- recording-player: jadi now-playing panel — tombol play/pause + spinner
  loading, progress bar + waktu (current/duration), status 'loading…',
  audio element pindah ke sini + event onPlay/onPause/onWaiting/onCanPlay/
  onPlaying/onError naik ke page.
- recordings/page: state isPlaying/isLoadingAudio + audioRef, togglePlay
  (klik card lain = ganti track, klik card sama = pause/resume).
- globals.css: keyframes eq-bounce + card-glow.

Verified: FE tsc0, next build 10/10 static pages.
This commit is contained in:
asepharyana
2026-08-01 16:54:58 +07:00
parent 0daee56213
commit 98064d1dd9
4 changed files with 241 additions and 46 deletions
@@ -1,7 +1,7 @@
"use client";
import { Clock, Database, Mic, Users } from "lucide-react";
import { useMemo, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { StatCard } from "@/components/dashboard/stat-card";
import { SubNav } from "@/components/layout/sub-nav";
import { RecordingCard } from "@/components/recordings/recording-card";
@@ -22,8 +22,11 @@ export default function RecordingsPage() {
mutate: refetch,
} = useRecordings();
const [playingId, setPlayingId] = useState<string | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isLoadingAudio, setIsLoadingAudio] = useState(false);
const [tab, setTab] = useState<RecordingsTab>("library");
const ws = useWebSocket();
const audioRef = useRef<HTMLAudioElement | null>(null);
// Live-update the library when the gateway publishes voice_recording_uploaded
useRecordingsWsSync(ws);
@@ -33,6 +36,17 @@ export default function RecordingsPage() {
? recordings.find((r: VoiceRecording) => r.id === playingId)
: null;
const togglePlay = (id: string) => {
if (playingId !== id) {
setPlayingId(id); // RecordingPlayer picks up the new url + autoplays
} else {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) audio.play().catch(() => {});
else audio.pause();
}
};
const stats = useMemo(() => {
const list = recordings ?? [];
const totalSize = list.reduce((sum, r) => sum + (r.size_bytes ?? 0), 0);
@@ -80,7 +94,10 @@ export default function RecordingsPage() {
<RecordingCard
key={rec.id}
recording={rec}
onPlay={(id) => setPlayingId(id === playingId ? null : id)}
active={playingId === rec.id}
playing={playingId === rec.id && isPlaying}
loading={playingId === rec.id && isLoadingAudio}
onTogglePlay={togglePlay}
/>
))}
{(recordings ?? []).length === 0 && (
@@ -154,6 +171,14 @@ export default function RecordingsPage() {
<RecordingPlayer
url={currentTrack?.download_url ?? undefined}
filename={currentTrack?.filename ?? undefined}
playing={isPlaying}
loading={isLoadingAudio}
audioRef={audioRef}
onToggle={() => togglePlay(playingId!)}
onStateChange={(s) => {
setIsPlaying(s.playing);
setIsLoadingAudio(s.loading);
}}
onClose={() => setPlayingId(null)}
/>
</div>