Files
Aurora-SpotiRip/components/SpotifyPlayerBar.tsx
T

156 lines
4.5 KiB
TypeScript
Raw Normal View History

2026-09-25 23:20:57 -05:00
import { Captions, ExternalLink, Pause, Play, Radio, SkipBack, SkipForward, Volume2 } from "lucide-react";
import { Button } from "./Button";
import { Slider } from "./Slider";
import styles from "./SpotifyPlayerBar.module.css";
type SpotifyPlayerBarProps = {
className?: string;
connected?: boolean;
ready?: boolean;
trackTitle?: string;
artist?: string;
album?: string;
imageUrl?: string;
externalUrl?: string;
isPlaying?: boolean;
positionMs?: number;
durationMs?: number;
volume?: number;
error?: string;
connectHref?: string;
onTogglePlay?: () => void;
onPrevious?: () => void;
onNext?: () => void;
onSeek?: (positionMs: number) => void;
onVolume?: (volume: number) => void;
lyricsOpen?: boolean;
onToggleLyrics?: () => void;
};
function formatTime(ms: number) {
if (!Number.isFinite(ms) || ms <= 0) return "0:00";
const total = Math.floor(ms / 1000);
const minutes = Math.floor(total / 60);
return `${minutes}:${String(total % 60).padStart(2, "0")}`;
}
export function SpotifyPlayerBar({
className,
connected = false,
ready = false,
trackTitle,
artist,
album = "Aurora SpotiRip",
imageUrl,
externalUrl,
isPlaying = false,
positionMs = 0,
durationMs = 0,
volume = 0.7,
error,
connectHref = "/_api/spotify-auth",
onTogglePlay,
onPrevious,
onNext,
onSeek,
onVolume,
lyricsOpen = false,
onToggleLyrics,
}: SpotifyPlayerBarProps) {
const usable = connected && ready;
const title = trackTitle || (connected ? "Nothing playing" : "Spotify playback");
const subtitle =
error ||
artist ||
(!connected
? "Connect your account to activate playback"
: ready
? "Press play on any song"
: "Starting Spotify playback…");
return (
<section className={`${styles.player} ${className || ""}`} aria-label="Spotify player">
<div className={styles.nowPlaying}>
<div className={styles.cover}>
{imageUrl ? <img src={imageUrl} alt="" /> : <Radio size={20} />}
</div>
<div className={styles.trackText}>
<strong>{title}</strong>
<span>{subtitle}</span>
</div>
</div>
<div className={styles.transport}>
<div className={styles.controls}>
<Button variant="ghost" size="icon-sm" disabled={!usable} onClick={onPrevious} aria-label="Previous track">
<SkipBack size={17} />
</Button>
<Button
size="icon"
disabled={!usable}
onClick={onTogglePlay}
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? <Pause size={18} /> : <Play size={18} fill="currentColor" />}
</Button>
<Button variant="ghost" size="icon-sm" disabled={!usable} onClick={onNext} aria-label="Next track">
<SkipForward size={17} />
</Button>
</div>
<div className={styles.timeline}>
<span>{formatTime(positionMs)}</span>
<Slider
className={styles.timelineSlider}
min={0}
max={Math.max(durationMs, 1)}
step={1000}
value={[Math.min(positionMs, Math.max(durationMs, 1))]}
disabled={!usable || durationMs <= 0}
onValueCommit={(value) => onSeek?.(value[0] || 0)}
/>
<span>{formatTime(durationMs)}</span>
</div>
</div>
<div className={styles.playerMeta}>
<div className={styles.albumText}>{album}</div>
{connected && externalUrl ? (
<a
className={styles.spotifyLink}
href={externalUrl}
target="_blank"
rel="noreferrer"
>
Spotify <ExternalLink size={12} />
</a>
) : null}
{connected ? (
<>
<Button
variant={lyricsOpen ? "outline" : "ghost"}
size="icon-sm"
onClick={onToggleLyrics}
aria-label="Toggle lyrics"
>
<Captions size={16} />
</Button>
<Volume2 size={17} />
<Slider
className={styles.volumeSlider}
min={0}
max={1}
step={0.05}
value={[volume]}
onValueChange={(value) => onVolume?.(value[0] ?? volume)}
/>
</>
) : (
<Button size="sm" asChild>
<a href={connectHref}>
Connect Spotify <ExternalLink size={14} />
</a>
</Button>
)}
</div>
</section>
);
}