trptk/components/player/Player.tsx
2026-02-24 17:14:07 +01:00

139 lines
5 KiB
TypeScript

"use client";
import { useRef, useCallback } from "react";
import Image from "next/image";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import {
IoPlayOutline,
IoPauseOutline,
IoPlaySkipForwardOutline,
IoPlaySkipBackOutline,
IoCloseOutline,
} from "react-icons/io5";
import { IconButton } from "@/components/IconButton";
import { usePlayer } from "./PlayerContext";
import { urlFor } from "@/lib/sanityImage";
function formatTime(seconds: number): string {
if (!seconds || !isFinite(seconds)) return "0:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
export function Player() {
const {
playlist,
currentIndex,
isPlaying,
currentTime,
audioDuration,
togglePlayback,
next,
previous,
seekTo,
stop,
} = usePlayer();
const progressRef = useRef<HTMLDivElement>(null);
const track = currentIndex !== null ? playlist[currentIndex] : null;
const hasPrev = currentIndex !== null && currentIndex > 0;
const hasNext = currentIndex !== null && currentIndex < playlist.length - 1;
const displayDuration = track?.trackDuration || audioDuration;
const progress = audioDuration > 0 ? currentTime / audioDuration : 0;
const handleProgressClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const bar = progressRef.current;
if (!bar) return;
const rect = bar.getBoundingClientRect();
const fraction = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
seekTo(fraction);
},
[seekTo],
);
const coverUrl = track?.albumCover ? urlFor(track.albumCover).width(96).height(96).url() : null;
return (
<AnimatePresence>
{track && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ type: "tween", ease: "easeInOut", duration: 0.3 }}
className="fixed right-0 bottom-0 left-0 z-50 border-t border-lighttext/10 bg-lightbg/90 backdrop-blur-xl dark:border-darktext/10 dark:bg-darkbg/90"
>
{/* Progress bar at top edge of player */}
<div
ref={progressRef}
onClick={handleProgressClick}
className="group relative h-1 w-full cursor-pointer bg-lighttext/10 transition-[height] hover:h-2 dark:bg-darktext/10"
>
<div
className="absolute top-0 left-0 h-full bg-trptkblue dark:bg-white"
style={{ width: `${progress * 100}%` }}
/>
</div>
<div className="flex max-w-full items-center gap-4 p-6 md:p-8">
{/* Album cover */}
{track.releaseSlug ? (
<Link href={`/release/${track.releaseSlug}`} className="relative hidden h-16 w-16 shrink-0 overflow-hidden rounded-lg transition-opacity hover:opacity-80 sm:block">
{coverUrl ? (
<Image src={coverUrl} alt="" fill className="object-cover" sizes="48px" />
) : (
<div className="h-full w-full bg-lighttext/10 dark:bg-darktext/10" />
)}
</Link>
) : (
<div className="relative hidden h-16 w-16 shrink-0 overflow-hidden rounded-lg sm:block">
{coverUrl ? (
<Image src={coverUrl} alt="" fill className="object-cover" sizes="48px" />
) : (
<div className="h-full w-full bg-lighttext/10 dark:bg-darktext/10" />
)}
</div>
)}
{/* Track info */}
<div className="min-w-0 flex-1">
<p className="truncate">
<span className="font-silkasb">{track.title}</span>
{track.movement && <span>: {track.movement}</span>}
</p>
<p className="truncate text-lightsec dark:text-darksec">{track.subtitle}</p>
</div>
{/* Time */}
<span className="hidden shrink-0 text-lightsec tabular-nums sm:block dark:text-darksec">
{formatTime(currentTime)} / {formatTime(displayDuration)}
</span>
{/* Controls */}
<div className="flex shrink-0 items-center gap-3">
<IconButton onClick={previous} disabled={!hasPrev} aria-label="Previous track">
<IoPlaySkipBackOutline size={18} />
</IconButton>
<IconButton onClick={togglePlayback} aria-label={isPlaying ? "Pause" : "Play"} className="hidden sm:block">
{isPlaying ? <IoPauseOutline size={18} /> : <IoPlayOutline size={18} />}
</IconButton>
<IconButton onClick={next} disabled={!hasNext} aria-label="Next track">
<IoPlaySkipForwardOutline size={18} />
</IconButton>
<IconButton onClick={stop} aria-label="Close player">
<IoCloseOutline size={18} />
</IconButton>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}