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.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 (