feat: add song recommendations component with loading state and shuffle functionality

This commit is contained in:
2025-07-10 19:55:02 +00:00
committed by GitHub
parent 52e465d2cf
commit 3c13c13143
5 changed files with 314 additions and 59 deletions

View File

@@ -46,6 +46,8 @@ export function AlbumArtwork({
const router = useRouter();
const { addAlbumToQueue, playTrack, addToQueue } = useAudioPlayer();
const { playlists, starItem, unstarItem } = useNavidrome();
const [imageLoading, setImageLoading] = useState(true);
const [imageError, setImageError] = useState(false);
const handleClick = () => {
router.push(`/album/${album.id}`);
@@ -115,28 +117,54 @@ export function AlbumArtwork({
<Card key={album.id} className="overflow-hidden cursor-pointer px-0 py-0 gap-0" onClick={() => handleClick()}>
<div className="aspect-square relative group">
{album.coverArt && api ? (
<Image
src={api.getCoverArtUrl(album.coverArt)}
alt={album.name}
fill
className="w-full h-full object-cover"
sizes="(max-width: 768px) 100vw, 300px"
/>
<>
{imageLoading && (
<div className="absolute inset-0 bg-muted animate-pulse rounded flex items-center justify-center">
<Disc className="w-12 h-12 text-muted-foreground animate-spin" />
</div>
)}
<Image
src={api.getCoverArtUrl(album.coverArt)}
alt={album.name}
fill
className={`w-full h-full object-cover transition-opacity duration-300 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
sizes="(max-width: 768px) 100vw, 300px"
onLoad={() => setImageLoading(false)}
onError={() => {
setImageLoading(false);
setImageError(true);
}}
/>
</>
) : (
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
<Disc className="w-12 h-12 text-muted-foreground" />
</div>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Play className="w-6 h-6 mx-auto hidden group-hover:block" onClick={() => handlePlayAlbum(album)}/>
</div>
{!imageLoading && (
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Play className="w-6 h-6 mx-auto hidden group-hover:block" onClick={() => handlePlayAlbum(album)}/>
</div>
)}
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{album.name}</h3>
<p className="text-sm text-muted-foreground truncate " onClick={() => router.push(album.artistId)}>{album.artist}</p>
<p className="text-xs text-muted-foreground mt-1">
{album.songCount} songs {Math.floor(album.duration / 60)} min
</p>
{imageLoading ? (
<>
<div className="h-5 w-3/4 bg-muted animate-pulse rounded mb-2" />
<div className="h-4 w-1/2 bg-muted animate-pulse rounded mb-1" />
<div className="h-3 w-2/3 bg-muted animate-pulse rounded" />
</>
) : (
<>
<h3 className="font-semibold truncate">{album.name}</h3>
<p className="text-sm text-muted-foreground truncate " onClick={() => router.push(album.artistId)}>{album.artist}</p>
<p className="text-xs text-muted-foreground mt-1">
{album.songCount} songs {Math.floor(album.duration / 60)} min
</p>
</>
)}
</CardContent>
</Card>
{/* <div onClick={handleClick} className="overflow-hidden rounded-md">