diff --git a/webui/src/routes/stats/-route.test.tsx b/webui/src/routes/stats/-route.test.tsx index ec251ae9..20867072 100644 --- a/webui/src/routes/stats/-route.test.tsx +++ b/webui/src/routes/stats/-route.test.tsx @@ -117,14 +117,29 @@ describe('stats route', () => { await waitFor(() => expect(history.location.search).toContain('range=30d')); }); - it('hands artist detail navigation directly to the shell bridge', async () => { - renderStatsRoute(); + it('links artist names to the artist-detail route', async () => { + const { history } = renderStatsRoute(); + + const bubbleLink = await screen.findByRole('link', { + name: 'Open artist detail for Artist A', + }); + expect(bubbleLink).toHaveAttribute('href', '/artist-detail/library/7'); - fireEvent.click(await screen.findByRole('button', { name: 'Artist A' })); + const rankedLink = screen.getByRole('link', { name: 'Artist A' }); + expect(rankedLink).toHaveAttribute('href', '/artist-detail/library/7'); - expect(window.SoulSyncWebShellBridge?.navigateToArtistDetail).toHaveBeenCalledWith( - 7, - 'Artist A', + fireEvent.click(bubbleLink); + + await waitFor(() => expect(history.location.pathname).toBe('/artist-detail/library/7')); + await waitFor(() => + expect(window.SoulSyncWebShellBridge?.navigateToArtistDetail).toHaveBeenCalledWith( + '7', + '', + null, + { + skipRouteChange: true, + }, + ), ); }); diff --git a/webui/src/routes/stats/-ui/stats-page.module.css b/webui/src/routes/stats/-ui/stats-page.module.css index 17be6814..a6260111 100644 --- a/webui/src/routes/stats/-ui/stats-page.module.css +++ b/webui/src/routes/stats/-ui/stats-page.module.css @@ -364,10 +364,13 @@ border: none; padding: 0; cursor: pointer; + color: inherit; + text-decoration: none; + font: inherit; transition: transform 0.2s ease; } -.statsArtistBubble:disabled { +.statsArtistBubbleDisabled { cursor: default; } @@ -375,6 +378,10 @@ transform: translateY(-3px); } +.statsArtistBubbleDisabled:hover { + transform: none; +} + .statsBubbleImage { border-radius: 50%; background-size: cover; diff --git a/webui/src/routes/stats/-ui/stats-page.tsx b/webui/src/routes/stats/-ui/stats-page.tsx index 63259a7d..1c93b461 100644 --- a/webui/src/routes/stats/-ui/stats-page.tsx +++ b/webui/src/routes/stats/-ui/stats-page.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from '@tanstack/react-router'; -import { type ReactNode, useEffect, useRef, useState } from 'react'; +import { Link, useNavigate } from '@tanstack/react-router'; +import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useRef, useState } from 'react'; import { Bar, BarChart, @@ -72,6 +72,8 @@ const STATS_CHART_CURSOR = { fill: 'rgba(var(--accent-rgb), 0.12)', } as const; +const ARTIST_DETAIL_SOURCE = 'library' as const; + export function StatsPage() { const bridge = useReactPageShell('stats'); @@ -136,10 +138,6 @@ export function StatsPage() { }); }; - const openArtistDetail = (artistId: string | number, artistName: string) => { - bridge.navigateToArtistDetail(artistId, artistName); - }; - return (
@@ -229,25 +227,15 @@ export function StatsPage() {
- openArtistDetail(artistId, artistName)} - /> - openArtistDetail(artistId, artistName)} - /> + + - openArtistDetail(artistId, artistName)} - /> + openArtistDetail(artistId, artistName)} onPlay={(track) => playStatsTrack(bridge, track)} /> @@ -405,13 +393,7 @@ function StatsGenreLegend({ ); } -function TopArtistsVisual({ - artists, - onArtistSelect, -}: { - artists: StatsArtistRow[]; - onArtistSelect: (artistId: string | number, artistName: string) => void; -}) { +function TopArtistsVisual({ artists }: { artists: StatsArtistRow[] }) { const topArtists = getTopArtistBubbles(artists); if (topArtists.length === 0) return null; @@ -420,18 +402,8 @@ function TopArtistsVisual({
{topArtists.map(({ artist, percent, size }) => { const isClickable = artist.id !== null && artist.id !== undefined; - return ( - + + ); + return isClickable ? ( + + {bubbleContent} + + ) : ( +
+ {bubbleContent} +
); })}
@@ -459,13 +449,30 @@ function TopArtistsVisual({ ); } -function StatsRankedArtists({ - artists, - onArtistSelect, +function ArtistDetailLink({ + artistId, + children, + ...linkProps }: { - artists: StatsArtistRow[]; - onArtistSelect: (artistId: string | number, artistName: string) => void; -}) { + artistId: string | number | null | undefined; + children: ReactNode; +} & Omit, 'children' | 'href'>) { + if (artistId == null) { + return <>{children}; + } + + return ( + + {children} + + ); +} + +function StatsRankedArtists({ artists }: { artists: StatsArtistRow[] }) { return (
{artists.length === 0 ? : null} @@ -479,17 +486,9 @@ function StatsRankedArtists({ )}
- {artist.id ? ( - - ) : ( - artist.name - )} + + {artist.name} + {artist.soul_id && !String(artist.soul_id).startsWith('soul_unnamed_') ? ( SoulID ) : null} @@ -509,13 +508,7 @@ function StatsRankedArtists({ ); } -function StatsRankedAlbums({ - albums, - onArtistSelect, -}: { - albums: StatsAlbumRow[]; - onArtistSelect: (artistId: string | number, artistName: string) => void; -}) { +function StatsRankedAlbums({ albums }: { albums: StatsAlbumRow[] }) { return (
{albums.length === 0 ? : null} @@ -530,19 +523,9 @@ function StatsRankedAlbums({
{album.name}
- {album.artist_id ? ( - - ) : ( - album.artist || '' - )} + + {album.artist || ''} +
@@ -556,11 +539,9 @@ function StatsRankedAlbums({ function StatsRankedTracks({ tracks, - onArtistSelect, onPlay, }: { tracks: StatsTrackRow[]; - onArtistSelect: (artistId: string | number, artistName: string) => void; onPlay: (track: { title: string; artist: string; album: string }) => Promise; }) { return ( @@ -577,19 +558,9 @@ function StatsRankedTracks({
{track.name}
- {track.artist_id ? ( - - ) : ( - track.artist || '' - )} + + {track.artist || ''} + {track.album ? ` ยท ${track.album}` : ''}