Files
mice/app/components/PopularSongs.tsx
angel 579eb740c0 chore: update Tailwind CSS from 3.4.15, to 4.1.11
- Changed the PostCSS configuration to use '@tailwindcss/postcss' instead of 'tailwindcss'.
- Deleted the Tailwind configuration file as it is no longer needed.
2025-07-03 02:53:19 +00:00

152 lines
4.8 KiB
TypeScript

'use client';
import Image from 'next/image';
import { Song } from '@/lib/navidrome';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { Button } from '@/components/ui/button';
import { Play, Heart } from 'lucide-react';
import { useState } from 'react';
import { useNavidrome } from '@/app/components/NavidromeContext';
import { getNavidromeAPI } from '@/lib/navidrome';
interface PopularSongsProps {
songs: Song[];
artistName: string;
}
export function PopularSongs({ songs, artistName }: PopularSongsProps) {
const { playTrack } = useAudioPlayer();
const { starItem, unstarItem } = useNavidrome();
const [songStates, setSongStates] = useState<Record<string, boolean>>(() => {
const initial: Record<string, boolean> = {};
songs.forEach(song => {
initial[song.id] = !!song.starred;
});
return initial;
});
const api = getNavidromeAPI();
const songToTrack = (song: Song) => {
if (!api) {
throw new Error('Navidrome API not configured');
}
return {
id: song.id,
name: song.title,
url: api.getStreamUrl(song.id),
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
};
};
const handlePlaySong = async (song: Song) => {
try {
const track = songToTrack(song);
playTrack(track, true);
} catch (error) {
console.error('Failed to play song:', error);
}
};
const handleToggleStar = async (song: Song) => {
try {
const isStarred = songStates[song.id];
if (isStarred) {
await unstarItem(song.id, 'song');
setSongStates(prev => ({ ...prev, [song.id]: false }));
} else {
await starItem(song.id, 'song');
setSongStates(prev => ({ ...prev, [song.id]: true }));
}
} catch (error) {
console.error('Failed to star/unstar song:', error);
}
};
const formatDuration = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
if (songs.length === 0) {
return null;
}
return (
<div className="space-y-4">
<h2 className="text-2xl font-semibold tracking-tight">Popular Songs</h2>
<div className="space-y-2">
{songs.map((song, index) => (
<div
key={song.id}
className="flex items-center space-x-4 p-3 rounded-lg hover:bg-muted/50 group"
>
{/* Rank */}
<div className="w-8 text-sm text-muted-foreground text-center">
{index + 1}
</div>
{/* Album Art */}
<div className="relative w-12 h-12 bg-muted rounded-md overflow-hidden shrink-0">
{song.coverArt && api && (
<Image
src={api.getCoverArtUrl(song.coverArt, 96)}
alt={song.album}
width={48}
height={48}
className="w-full h-full object-cover"
/>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0 text-white hover:bg-white/20"
onClick={() => handlePlaySong(song)}
>
<Play className="h-4 w-4" />
</Button>
</div>
</div>
{/* Song Info */}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{song.title}</div>
<div className="text-xs text-muted-foreground truncate">{song.album}</div>
</div>
{/* Play Count */}
{song.playCount && song.playCount > 0 && (
<div className="text-xs text-muted-foreground">
{song.playCount.toLocaleString()} plays
</div>
)}
{/* Duration */}
<div className="text-xs text-muted-foreground w-12 text-right">
{formatDuration(song.duration)}
</div>
{/* Star Button */}
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => handleToggleStar(song)}
>
<Heart
className={`h-4 w-4 ${songStates[song.id] ? 'text-red-500 fill-red-500' : 'text-muted-foreground'}`}
/>
</Button>
</div>
))}
</div>
</div>
);
}