diff --git a/src/features/artist-detail/ArtistDetailPage.tsx b/src/features/artist-detail/ArtistDetailPage.tsx index 5b4e4c4..3bc9cf0 100644 --- a/src/features/artist-detail/ArtistDetailPage.tsx +++ b/src/features/artist-detail/ArtistDetailPage.tsx @@ -1,8 +1,270 @@ +import { useParams, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; -import { Placeholder } from '../../components/common/Placeholder'; +import { ScrollArea, IconButton, Button, Card } from '@olly/modern-sk'; +import { + useGetArtistQuery, + useGetArtistAlbumsQuery, + useGetArtistTracksQuery, +} from '../../api/endpoints/library'; +import { TrackRow } from '../../components/track/TrackRow'; +import { ArtTile } from '../../components/common/ArtTile'; +import { LoadingSkeleton } from '../../components/common/LoadingSkeleton'; +import { ErrorState } from '../../components/common/ErrorState'; +import { EmptyState } from '../../components/common/EmptyState'; +import { useAppDispatch } from '../../hooks/useAppDispatch'; +import { setQueue } from '../../store/slices/queue'; +import { formatDuration } from '../../lib/format'; +import { getCoverUrl } from '../../api/endpoints/streaming'; +import type { Album } from '../../api/types'; -/** `/artists/:artistId` — A3 artist detail (discography + similar). Scaffold only. */ export function ArtistDetailPage() { const { t } = useTranslation(); - return ; + const { artistId } = useParams<{ artistId: string }>(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const artistQuery = useGetArtistQuery(artistId ?? '', { skip: !artistId }); + const albumsQuery = useGetArtistAlbumsQuery(artistId ?? '', { + skip: !artistId, + }); + const tracksQuery = useGetArtistTracksQuery(artistId ?? '', { + skip: !artistId, + }); + + if (artistQuery.isLoading) { + return ( +
+ +
+ ); + } + + if (artistQuery.isError || !artistQuery.data) { + return ( + artistQuery.refetch()} + /> + ); + } + + const artist = artistQuery.data; + const albums = albumsQuery.data ?? []; + const tracks = tracksQuery.data ?? []; + + const handlePlayAll = () => { + if (!tracks.length) return; + dispatch( + setQueue({ + entries: tracks.map((tr) => ({ + trackId: tr.id, + title: tr.title, + artistName: tr.artistName, + albumTitle: tr.albumTitle, + durationMs: tr.durationMs, + albumArtUrl: tr.albumArtUrl, + })), + source: 'artist', + sourceId: artist.id, + sourceName: artist.name, + }), + ); + }; + + return ( +
+
+ navigate(-1)} + aria-label={t('common.back')} + > + ← + +
+ +
+

+ {t('artist.type')} +

+

+ {artist.name} +

+

+ {t('artist.meta', { + albumCount: artist.albumCount, + trackCount: artist.trackCount, + })} +

+
+
+ +
+ + + {/* Discography */} +
+

+ {t('artist.albums')} +

+ {albumsQuery.isLoading && } + {albumsQuery.isError && ( + albumsQuery.refetch()} /> + )} + {!albumsQuery.isLoading && + !albumsQuery.isError && + albums.length === 0 && ( +

+ {t('artist.noAlbums')} +

+ )} + {albums.length > 0 && ( +
+ {albums.map((album) => ( + void navigate(`/albums/${album.id}`)} + /> + ))} +
+ )} +
+ + {/* All tracks */} +
+

+ {t('artist.tracks')} +

+ {tracksQuery.isLoading && } + {tracksQuery.isError && ( + tracksQuery.refetch()} /> + )} + {!tracksQuery.isLoading && + !tracksQuery.isError && + tracks.length === 0 && ( + + )} + {tracks.map((track, i) => ( + + ))} +
+
+
+ ); +} + +function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) { + const { t } = useTranslation(); + const artUrl = getCoverUrl(album.artUrl); + return ( + + {artUrl ? ( + {album.title} + ) : ( +
+ +
+ )} +
+
+ {album.title} +
+
+ {album.year ? `${album.year} · ` : ''} + {t('library.albumCard.tracksDuration', { + count: album.trackCount, + duration: formatDuration(album.totalDurationMs), + })} +
+
+
+ ); } diff --git a/src/features/library/LibraryPage.tsx b/src/features/library/LibraryPage.tsx index 1fcaa62..b8c9e38 100644 --- a/src/features/library/LibraryPage.tsx +++ b/src/features/library/LibraryPage.tsx @@ -216,7 +216,11 @@ export function LibraryPage() { {artistsQuery.data && (
{artistsQuery.data.items.map((artist) => ( - + void navigate(`/artists/${artist.id}`)} + /> ))}
)} @@ -302,15 +306,23 @@ function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) { ); } -function ArtistRow({ artist }: { artist: Artist }) { +function ArtistRow({ + artist, + onClick, +}: { + artist: Artist; + onClick: () => void; +}) { const { t } = useTranslation(); return (