68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import { Link } from '@tanstack/react-router';
|
|
import type { TrackArtist } from '../types';
|
|
|
|
interface ArtistLinksProps {
|
|
/** Ordered artists (first = main, rest = featured). */
|
|
artists?: TrackArtist[] | null;
|
|
/** Shown when there are no structured artists (plain text, not a link). */
|
|
fallback?: string;
|
|
className?: string;
|
|
/** Stop row/card click handlers from firing when an artist link is clicked. */
|
|
stopPropagation?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Deduplicates artists by ID, preferring `main` over `featured` when the
|
|
* same artist has both roles (can happen because track_artists has a composite
|
|
* PK of track_id + artist_id + role).
|
|
*/
|
|
function deduplicateArtists(artists: TrackArtist[]): TrackArtist[] {
|
|
const map = new Map<string, TrackArtist>();
|
|
for (const a of artists) {
|
|
const existing = map.get(a.id);
|
|
if (!existing || (existing.role === 'featured' && a.role === 'main')) {
|
|
map.set(a.id, a);
|
|
}
|
|
}
|
|
// Preserve original order, skipping duplicates.
|
|
const seen = new Set<string>();
|
|
return artists.filter((a) => {
|
|
if (seen.has(a.id)) return false;
|
|
seen.add(a.id);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders a track/album's artists as clickable links — main artist(s) then
|
|
* "feat." guests. Single source of truth used by TrackRow, the playback bar,
|
|
* the now-playing panel and album pages so artist navigation looks and behaves
|
|
* the same everywhere.
|
|
*
|
|
* Artists are deduplicated by id — if the same artist appears as both main
|
|
* and featured, only the main entry is shown.
|
|
*/
|
|
export function ArtistLinks({ artists, fallback, className = '', stopPropagation }: ArtistLinksProps) {
|
|
if (!artists || artists.length === 0) {
|
|
return <span className={className}>{fallback || 'Unknown artist'}</span>;
|
|
}
|
|
const unique = deduplicateArtists(artists);
|
|
return (
|
|
<span className={className}>
|
|
{unique.map((a, i) => (
|
|
<span key={a.id}>
|
|
{i > 0 && <span className="opacity-60">{a.role === 'featured' && unique[i - 1].role !== 'featured' ? ' feat. ' : ', '}</span>}
|
|
<Link
|
|
to="/artists/$artistId"
|
|
params={{ artistId: a.id }}
|
|
onClick={stopPropagation ? (e) => e.stopPropagation() : undefined}
|
|
className={`hover:text-text hover:underline ${a.role === 'featured' ? 'opacity-75' : ''}`}
|
|
>
|
|
{a.name}
|
|
</Link>
|
|
</span>
|
|
))}
|
|
</span>
|
|
);
|
|
}
|