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:
@@ -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>
|
||||
|
||||
@@ -167,6 +167,38 @@
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Equalizer bars — bouncing heights for the "now playing" waveform */
|
||||
@keyframes eq-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleY(0.25);
|
||||
}
|
||||
30% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
60% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
}
|
||||
.animate-eq {
|
||||
animation: eq-bounce 0.9s ease-in-out infinite;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
/* Soft pulsing glow for the active/loading card */
|
||||
@keyframes card-glow {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 oklch(0.62 0.17 215 / 0);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 22px 0 oklch(0.62 0.17 215 / 0.25);
|
||||
}
|
||||
}
|
||||
.animate-card-glow {
|
||||
animation: card-glow 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ── Utility classes ─────────────────────── */
|
||||
|
||||
/* Gradient text */
|
||||
|
||||
Reference in New Issue
Block a user