Files
diyaa.de/components/public/audio-player.tsx
T

107 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { trackClientEvent } from "@/lib/analytics-client";
type AudioPlayerProps = {
src: string;
title: string;
locale: "ar" | "en";
downloadable?: boolean;
analytics?: { entityId: string };
className?: string;
};
function formatTime(value: number) {
if (!Number.isFinite(value) || value < 0) return "0:00";
const minutes = Math.floor(value / 60);
const seconds = Math.floor(value % 60).toString().padStart(2, "0");
return `${minutes}:${seconds}`;
}
export default function AudioPlayer({ src, title, locale, downloadable = false, analytics, className }: AudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const hasTrackedPlay = useRef(false);
const labels = locale === "ar"
? { play: "تشغيل", pause: "إيقاف مؤقت", back: "تأخير 10 ثوانٍ", forward: "تقديم 10 ثوانٍ", seek: "موضع التشغيل", volume: "مستوى الصوت", download: "تحميل الصوت" }
: { play: "Play", pause: "Pause", back: "Back 10 seconds", forward: "Forward 10 seconds", seek: "Playback position", volume: "Volume", download: "Download audio" };
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const onLoadedMetadata = () => setDuration(audio.duration);
const onTimeUpdate = () => setCurrentTime(audio.currentTime);
const onEnded = () => setIsPlaying(false);
audio.addEventListener("loadedmetadata", onLoadedMetadata);
audio.addEventListener("timeupdate", onTimeUpdate);
audio.addEventListener("ended", onEnded);
audio.volume = volume;
return () => {
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
audio.removeEventListener("timeupdate", onTimeUpdate);
audio.removeEventListener("ended", onEnded);
};
}, [volume]);
const togglePlayback = async () => {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
await audio.play();
setIsPlaying(true);
if (analytics && !hasTrackedPlay.current) {
hasTrackedPlay.current = true;
trackClientEvent({ type: "MELODY_PLAY", entityId: analytics.entityId });
}
} else {
audio.pause();
setIsPlaying(false);
}
};
const seekBy = (seconds: number) => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = Math.max(0, Math.min(audio.duration || 0, audio.currentTime + seconds));
setCurrentTime(audio.currentTime);
};
const updateTime = (value: number) => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = value;
setCurrentTime(value);
};
const updateVolume = (value: number) => {
setVolume(value);
if (audioRef.current) audioRef.current.volume = value;
};
return (
<div className={cn("rounded-xl border border-border bg-card p-4", className)}>
<audio ref={audioRef} src={src} preload="metadata" />
<div className="flex items-center gap-2">
<button type="button" onClick={togglePlayback} aria-label={isPlaying ? labels.pause : labels.play} className="inline-flex h-10 min-w-10 items-center justify-center rounded-full bg-primary px-3 text-sm font-medium text-primary-foreground hover:opacity-90">
{isPlaying ? "Ⅱ" : "▶"}
</button>
<button type="button" onClick={() => seekBy(-10)} aria-label={labels.back} className="rounded-md border border-border px-2 py-2 text-xs hover:bg-accent">10</button>
<button type="button" onClick={() => seekBy(10)} aria-label={labels.forward} className="rounded-md border border-border px-2 py-2 text-xs hover:bg-accent">+10</button>
<span className="ms-auto text-xs tabular-nums text-muted-foreground">{formatTime(currentTime)} / {formatTime(duration)}</span>
</div>
<input type="range" min={0} max={duration || 0} step="any" value={Math.min(currentTime, duration || 0)} onChange={(event) => updateTime(event.target.valueAsNumber)} aria-label={labels.seek} className="mt-4 w-full accent-primary" />
<div className="mt-3 flex items-center gap-3 text-xs text-muted-foreground">
<span aria-hidden="true">🔊</span>
<input type="range" min={0} max={1} step="0.05" value={volume} onChange={(event) => updateVolume(event.target.valueAsNumber)} aria-label={labels.volume} className="w-28 accent-primary" />
{downloadable ? <a href={src} download className="ms-auto font-medium text-brand-2 hover:underline">{labels.download}</a> : null}
</div>
<p className="sr-only">{title}</p>
</div>
);
}