Refactor SongRecommendations and LibraryPage components for improved mobile responsiveness and code clarity

This commit is contained in:
2025-07-25 04:52:46 +00:00
committed by GitHub
parent 940ed94579
commit fe40c0264c
2 changed files with 103 additions and 86 deletions

View File

@@ -196,7 +196,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
{isMobile ? 'Here are some albums you might enjoy' : 'Here are some songs you might enjoy'} {isMobile ? 'Here are some albums you might enjoy' : 'Here are some songs you might enjoy'}
</p> </p>
</div> </div>
{(isMobile ? recommendedAlbums.length > 0 : recommendedSongs.length > 0) && ( {(isMobile ? recommendedAlbums.length > 0 : recommendedSongs.length > 0) && !isMobile && (
<Button onClick={handleShuffleAll} variant="outline" size="sm"> <Button onClick={handleShuffleAll} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" /> <Shuffle className="w-4 h-4 mr-2" />
Shuffle All Shuffle All
@@ -209,47 +209,45 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
recommendedAlbums.length > 0 ? ( recommendedAlbums.length > 0 ? (
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{recommendedAlbums.map((album) => ( {recommendedAlbums.map((album) => (
<Link <div key={album.id} className="space-y-2">
key={album.id} <Link
href={`/album/${album.id}`} href={`/album/${album.id}`}
className="group cursor-pointer" className="group cursor-pointer block"
> >
<div className="space-y-2">
<div className="relative aspect-square rounded-lg overflow-hidden bg-muted"> <div className="relative aspect-square rounded-lg overflow-hidden bg-muted">
{album.coverArt && api ? ( {album.coverArt && api ? (
<> <Image
<Image src={api.getCoverArtUrl(album.coverArt, 200)}
src={api.getCoverArtUrl(album.coverArt, 200)} alt={album.name}
alt={album.name} fill
fill className="object-cover"
className="object-cover" sizes="(max-width: 768px) 33vw, 200px"
sizes="(max-width: 768px) 33vw, 200px" onLoad={handleImageLoad}
onLoad={handleImageLoad} onError={handleImageError}
onError={handleImageError} loading="lazy"
loading="lazy" />
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Play
className="w-8 h-8 text-white"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handlePlayAlbum(album);
}}
/>
</div>
</>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<Music className="w-8 h-8 text-muted-foreground" /> <Music className="w-8 h-8 text-muted-foreground" />
</div> </div>
)} )}
</div> </div>
<div className="space-y-1"> </Link>
<p className="font-medium text-sm truncate">{album.name}</p> <div className="space-y-1">
</div> <Link
href={`/album/${album.id}`}
className="font-medium text-sm truncate hover:underline block"
>
{album.name}
</Link>
<Link
href={`/artist/${album.artistId || album.artist}`}
className="text-xs text-muted-foreground truncate hover:underline block"
>
{album.artist}
</Link>
</div> </div>
</Link> </div>
))} ))}
</div> </div>
) : ( ) : (

View File

@@ -6,12 +6,15 @@ import Image from 'next/image';
import { Music, Users, Disc, ListMusic, Heart, Play } from 'lucide-react'; import { Music, Users, Disc, ListMusic, Heart, Play } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { getNavidromeAPI } from '@/lib/navidrome'; import { getNavidromeAPI } from '@/lib/navidrome';
import NavidromeAPI from '@/lib/navidrome';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { useIsMobile } from '@/hooks/use-mobile';
interface Album { interface Album {
id: string; id: string;
name: string; name: string;
artist: string; artist: string;
artistId?: string;
coverArt?: string; coverArt?: string;
year?: number; year?: number;
songCount: number; songCount: number;
@@ -28,26 +31,29 @@ export default function LibraryPage() {
const [recentAlbums, setRecentAlbums] = useState<Album[]>([]); const [recentAlbums, setRecentAlbums] = useState<Album[]>([]);
const [stats, setStats] = useState<LibraryStats>({ albums: 0, artists: 0, songs: 0, playlists: 0 }); const [stats, setStats] = useState<LibraryStats>({ albums: 0, artists: 0, songs: 0, playlists: 0 });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [api, setApi] = useState<NavidromeAPI | null>(null);
const { playAlbum } = useAudioPlayer(); const { playAlbum } = useAudioPlayer();
const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
const loadLibraryData = async () => { const loadLibraryData = async () => {
try { try {
const api = getNavidromeAPI(); const navidromeApi = getNavidromeAPI();
if (!api) { if (!navidromeApi) {
console.error('Navidrome API not available'); console.error('Navidrome API not available');
return; return;
} }
setApi(navidromeApi);
// Load recent albums // Load recent albums
const albumsData = await api.getAlbums('newest', 4, 0); const albumsData = await navidromeApi.getAlbums('newest', 4, 0);
setRecentAlbums(albumsData || []); setRecentAlbums(albumsData || []);
// Load library stats // Load library stats
const [allAlbums, allArtists, allPlaylists] = await Promise.all([ const [allAlbums, allArtists, allPlaylists] = await Promise.all([
api.getAlbums('alphabeticalByName', 1, 0), // Just to get count navidromeApi.getAlbums('alphabeticalByName', 1, 0), // Just to get count
api.getArtists(), navidromeApi.getArtists(),
api.getPlaylists() navidromeApi.getPlaylists()
]); ]);
setStats({ setStats({
@@ -118,6 +124,18 @@ export default function LibraryPage() {
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-2xl font-bold">Your Library</h1> <h1 className="text-2xl font-bold">Your Library</h1>
{/* Loading skeleton for library links */}
<div>
<h2 className="text-lg font-semibold mb-3">Browse</h2>
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-muted rounded-lg h-16"></div>
</div>
))}
</div>
</div>
{/* Loading skeleton for recent albums */} {/* Loading skeleton for recent albums */}
<div> <div>
<h2 className="text-lg font-semibold mb-3">Recently Added</h2> <h2 className="text-lg font-semibold mb-3">Recently Added</h2>
@@ -131,18 +149,6 @@ export default function LibraryPage() {
))} ))}
</div> </div>
</div> </div>
{/* Loading skeleton for library links */}
<div>
<h2 className="text-lg font-semibold mb-3">Browse</h2>
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-muted rounded-lg h-16"></div>
</div>
))}
</div>
</div>
</div> </div>
</div> </div>
); );
@@ -153,41 +159,7 @@ export default function LibraryPage() {
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-2xl font-bold">Your Library</h1> <h1 className="text-2xl font-bold">Your Library</h1>
{/* Recently Added Albums */} {/* Library Navigation - Always at top */}
<div>
<h2 className="text-lg font-semibold mb-3">Recently Added</h2>
<div className="grid grid-cols-2 gap-4">
{recentAlbums.map((album) => (
<Card key={album.id} className="group cursor-pointer hover:bg-muted/50 transition-colors">
<CardContent className="p-3">
<div className="relative aspect-square mb-2">
<Image
src={album.coverArt || '/default-user.jpg'}
alt={album.name}
fill
className="object-cover rounded-lg"
/>
<button
onClick={() => handlePlayAlbum(album)}
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
>
<Play className="w-8 h-8 text-white fill-white" />
</button>
</div>
<Link href={`/album/${album.id}`}>
<h3 className="font-medium text-sm truncate hover:underline">{album.name}</h3>
<p className="text-xs text-muted-foreground truncate">{album.artist}</p>
{album.year && (
<p className="text-xs text-muted-foreground">{album.year}</p>
)}
</Link>
</CardContent>
</Card>
))}
</div>
</div>
{/* Library Navigation */}
<div> <div>
<h2 className="text-lg font-semibold mb-3">Browse</h2> <h2 className="text-lg font-semibold mb-3">Browse</h2>
<div className="space-y-3"> <div className="space-y-3">
@@ -218,6 +190,53 @@ export default function LibraryPage() {
})} })}
</div> </div>
</div> </div>
{/* Recently Added Albums - At bottom on mobile, after Browse on desktop */}
<div>
<h2 className="text-lg font-semibold mb-3">Recently Added</h2>
<div className="grid grid-cols-2 gap-4">
{recentAlbums.map((album) => (
<Card key={album.id} className="group cursor-pointer hover:bg-muted/50 transition-colors">
<CardContent className="p-3">
<Link href={`/album/${album.id}`}>
<div className="relative aspect-square mb-2">
<Image
src={album.coverArt && api ? api.getCoverArtUrl(album.coverArt, 200) : '/default-user.jpg'}
alt={album.name}
fill
className="object-cover rounded-lg"
sizes="(max-width: 768px) 50vw, 200px"
/>
{!isMobile && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handlePlayAlbum(album);
}}
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
>
<Play className="w-8 h-8 text-white fill-white" />
</button>
)}
</div>
<h3 className="font-medium text-sm truncate hover:underline">{album.name}</h3>
<Link
href={`/artist/${album.artistId || album.artist}`}
className="text-xs text-muted-foreground truncate hover:underline block"
onClick={(e) => e.stopPropagation()}
>
{album.artist}
</Link>
{album.year && (
<p className="text-xs text-muted-foreground">{album.year}</p>
)}
</Link>
</CardContent>
</Card>
))}
</div>
</div>
</div> </div>
</div> </div>
); );