spotify-clone/frontend-vite/src/pages/Album.tsx

211 lines
12 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { libraryService } from '../services/library';
import { usePlayer } from '../context/PlayerContext';
import { Play, Shuffle, Heart, Clock, ListPlus, Download } from 'lucide-react';
import { Track } from '../types';
export default function Album() {
const { id } = useParams();
const { playTrack, toggleLike, likedTracks, setIsFullScreenOpen, currentTrack } = usePlayer();
const [tracks, setTracks] = useState<Track[]>([]);
const [albumInfo, setAlbumInfo] = useState<{ title: string, artist: string, cover?: string, year?: string } | null>(null);
const [moreByArtist, setMoreByArtist] = useState<Track[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) return;
setLoading(true);
const fetchAlbum = async () => {
const queryId = decodeURIComponent(id);
try {
const album = await libraryService.getAlbum(queryId);
if (album) {
setTracks(album.tracks);
setAlbumInfo({
title: album.title,
artist: album.creator || "Unknown Artist",
cover: album.cover_url,
year: '2024'
});
// Fetch suggestions
try {
const artistQuery = album.creator || "Unknown Artist";
const suggestions = await libraryService.search(artistQuery);
const currentIds = new Set(album.tracks.map(t => t.id));
setMoreByArtist(suggestions.filter(t => !currentIds.has(t.id)).slice(0, 10));
} catch (e) { }
} else {
// Fallback to searching the query if ID not found anywhere
const cleanTitle = queryId.replace(/^search-|^album-/, '');
const results = await libraryService.search(queryId);
setTracks(results);
setAlbumInfo({
title: cleanTitle,
artist: results.length > 0 ? results[0].artist : "Unknown Artist",
cover: results.length > 0 ? results[0].cover_url : undefined,
year: '2024'
});
}
} catch (e) {
console.error(e);
}
setLoading(false);
};
fetchAlbum();
}, [id]);
if (loading) return <div className="flex items-center justify-center h-full"><div className="animate-spin rounded-full h-12 w-12 border-t-2 border-white"></div></div>;
if (!albumInfo) return <div>Album not found</div>;
const totalDuration = tracks.reduce((acc, t) => acc + (t.duration || 0), 0);
const formattedDuration = `${Math.floor(totalDuration / 60)} minutes`;
return (
<div className="flex-1 overflow-y-auto bg-[#121212] no-scrollbar pb-32 relative">
{/* Banner Background */}
{albumInfo.cover && (
<div
className="absolute top-0 left-0 w-full h-[50vh] min-h-[400px] opacity-30 pointer-events-none"
style={{
backgroundImage: `url(${albumInfo.cover})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
maskImage: 'linear-gradient(to bottom, black 0%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, transparent 100%)'
}}
/>
)}
<div className="relative z-10 flex flex-col md:flex-row gap-4 md:gap-8 p-4 md:p-12 items-center md:items-end pt-16 md:pt-16">
{/* Cover */}
<div
className="w-48 h-48 md:w-64 md:h-64 shadow-[0_20px_50px_rgba(0,0,0,0.5)] rounded-lg overflow-hidden shrink-0 mt-8 md:mt-0 cursor-pointer group/cover relative"
onClick={() => {
if (tracks.length > 0) {
playTrack(tracks[0], tracks);
setIsFullScreenOpen(true);
}
}}
>
<img src={albumInfo.cover} alt={albumInfo.title} className="w-full h-full object-cover transition-transform duration-700 group-hover/cover:scale-110" />
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover/cover:opacity-100 transition flex items-center justify-center">
<Play fill="white" size={48} className="text-white drop-shadow-2xl" />
</div>
</div>
{/* Info */}
<div className="flex flex-col items-center md:items-start text-center md:text-left gap-2 md:gap-4 flex-1">
<span className="text-xs md:text-sm font-bold tracking-widest uppercase text-white/70">Album</span>
<h1 className="text-2xl md:text-6xl font-black text-white leading-tight line-clamp-3 text-ellipsis overflow-hidden">{albumInfo.title}</h1>
<div className="flex flex-wrap justify-center md:justify-start items-center gap-2 text-white/80 font-medium text-sm md:text-base">
<img src={albumInfo.cover} className="w-6 h-6 rounded-full" />
<span className="hover:underline cursor-pointer">{albumInfo.artist}</span>
<span></span>
<span>{albumInfo.year}</span>
<span></span>
<span>{tracks.length} songs, {formattedDuration}</span>
</div>
</div>
</div>
{/* Toolbar */}
<div className="px-4 py-3 flex items-center justify-center gap-6 bg-black/20 backdrop-blur-sm sticky top-0 z-10 md:px-8">
<button
onClick={() => tracks.length > 0 && playTrack(tracks[0])} // Should play all
className="bg-white text-black px-8 py-2 rounded-full font-bold text-sm hover:scale-105 transition flex items-center gap-2 shadow-lg hover:shadow-xl hover:bg-neutral-200"
>
<Play fill="currentColor" size={18} />
Play
</button>
<div className="flex items-center gap-4">
<button className="p-2 text-neutral-400 hover:text-white transition border border-white/10 rounded-full hover:bg-white/10 hover:border-white">
<Shuffle size={20} />
</button>
<button className="p-2 text-neutral-400 hover:text-white transition border border-white/10 rounded-full hover:bg-white/10 hover:border-white">
<ListPlus size={20} />
</button>
<button className="p-2 text-neutral-400 hover:text-white transition border border-white/10 rounded-full hover:bg-white/10 hover:border-white">
<Download size={20} />
</button>
</div>
</div>
{/* Tracklist */}
<div className="p-4 md:p-8">
{/* Header Row */}
<div className="flex items-center text-sm text-neutral-400 border-b border-white/10 pb-2 mb-4 px-4 sticky top-20 bg-[#121212] z-10">
<span className="w-10 text-center">#</span>
<span className="flex-1">Title</span>
<span className="hidden md:block w-12 text-right"><Clock size={16} /></span>
</div>
<div className="flex flex-col">
{tracks.map((track, i) => (
<div
key={track.id}
className="group flex items-center p-3 rounded-md hover:bg-white/10 transition cursor-pointer"
onClick={() => playTrack(track, tracks)}
>
<span className="w-10 text-center text-neutral-500 font-medium group-hover:hidden">{i + 1}</span>
<Play size={16} className="w-10 hidden group-hover:block fill-white pl-2" />
<div className="flex-1 min-w-0 pr-4">
<div className="font-medium text-white truncate text-base">{track.title}</div>
<div className="text-sm text-neutral-400 truncate group-hover:text-white/70">{track.artist}</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); toggleLike(track); }}
className={`mr-6 ${likedTracks.has(track.id) ? 'text-green-500 opacity-100' : 'text-neutral-400 opacity-0 group-hover:opacity-100'} hover:scale-110 transition`}
>
<Heart size={18} fill={likedTracks.has(track.id) ? "currentColor" : "none"} />
</button>
<span className="text-neutral-500 text-sm hidden md:block w-12 text-right font-mono">
{Math.floor((track.duration || 0) / 60)}:{((track.duration || 0) % 60).toString().padStart(2, '0')}
</span>
</div>
))}
</div>
</div>
{/* Suggestions / More By Artist */}
{moreByArtist.length > 0 && (
<div className="p-4 md:p-8 mt-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold hover:underline cursor-pointer">More by {albumInfo.artist}</h2>
<Link to={`/artist/${encodeURIComponent(albumInfo.artist)}`}>
<span className="text-xs font-bold text-[#b3b3b3] uppercase tracking-wider hover:text-white cursor-pointer">Show discography</span>
</Link>
</div>
<div className="grid grid-cols-2 fold:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 md:gap-4">
{moreByArtist.map((track) => (
<div
className="bg-[#181818] p-3 md:p-4 rounded-xl hover:bg-[#282828] transition duration-300 group cursor-pointer relative flex flex-col"
key={track.id}
onClick={() => {
playTrack(track, moreByArtist);
}}
>
<div className="relative mb-3 md:mb-4">
<img src={track.cover_url} className="w-full aspect-square rounded-md shadow-lg object-cover" />
<div className="absolute bottom-1 right-1 md:bottom-2 md:right-2 translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition duration-300 shadow-xl">
<div className="w-10 h-10 md:w-12 md:h-12 bg-[#1DB954] rounded-full flex items-center justify-center hover:scale-105">
<Play className="fill-black text-black ml-0.5 w-4 h-4 md:w-6 md:h-6" />
</div>
</div>
</div>
<h3 className="font-bold text-sm md:text-base mb-1 truncate">{track.title}</h3>
<p className="text-xs md:text-sm text-[#a7a7a7] truncate">{track.artist}</p>
</div>
))}
</div>
</div>
)}
</div>
);
}