Files
retivo/apps/web/src/pages/ArtistPage.tsx
2026-10-05 20:38:28 +02:00

96 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Play, UserCheck, UserPlus } from "lucide-react";
import { Link, useParams } from "react-router-dom";
import { artistsApi } from "../lib/api";
import { usePlayer } from "../store/player";
import { Cover } from "../components/Cover";
import { ShareButton } from "../components/ShareButton";
import { TrackRow } from "../components/TrackRow";
export function ArtistPage() {
const { id = "" } = useParams();
const qc = useQueryClient();
const playQueue = usePlayer((s) => s.playQueue);
const { data, isLoading } = useQuery({
queryKey: ["artist", id],
queryFn: () => artistsApi.get(id),
enabled: Boolean(id),
});
const { data: tracks = [] } = useQuery({
queryKey: ["artist-tracks", id],
queryFn: () => artistsApi.tracks(id),
enabled: Boolean(id),
});
const follow = useMutation({
mutationFn: () => (data?.following ? artistsApi.unfollow(id) : artistsApi.follow(id)),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ["artist", id] });
void qc.invalidateQueries({ queryKey: ["library"] });
},
});
if (isLoading) return <p className="text-sm text-zinc-500">Загрузка...</p>;
if (!data) return <p className="text-sm text-zinc-500">Артист не найден.</p>;
const { artist } = data;
return (
<div className="mx-auto max-w-5xl animate-fade-in">
<div className="card mb-6 flex flex-col items-center gap-5 p-6 text-center sm:flex-row sm:items-end sm:text-left">
<Cover src={artist.imageUrl} name={artist.name} rounded="rounded-full" className="h-36 w-36 shrink-0" />
<div className="min-w-0 flex-1">
<span className="chip">Артист</span>
<h1 className="mt-2 truncate text-3xl font-bold tracking-tight text-white">{artist.name}</h1>
<p className="mt-1 text-sm text-zinc-500">
{artist.followersCount} подписчиков
{artist.trackCount !== undefined && ` · ${artist.trackCount} треков`}
{artist.country && ` · ${artist.country}`}
</p>
{artist.description && (
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-zinc-400">{artist.description}</p>
)}
<div className="mt-4 flex flex-wrap justify-center gap-2 sm:justify-start">
<button className="btn-primary" onClick={() => playQueue(tracks, 0)} disabled={!tracks.length}>
<Play size={15} /> Слушать
</button>
<button className="btn-ghost" onClick={() => follow.mutate()}>
{data.following ? <UserCheck size={15} /> : <UserPlus size={15} />}
{data.following ? "Вы подписаны" : "Подписаться"}
</button>
<ShareButton path={`/artist/${id}`} />
</div>
</div>
</div>
{data.albums.length > 0 && (
<section className="mb-8">
<h2 className="section-title mb-3">Альбомы</h2>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{data.albums.map((al) => (
<Link key={al.id} to={`/album/${al.id}`} className="card p-3 hover:bg-white/[0.07]">
<Cover src={al.coverUrl} name={al.title} className="aspect-square w-full" />
<div className="mt-2 truncate text-sm text-white">{al.title}</div>
<div className="truncate text-xs text-zinc-500">{al.releaseDate?.slice(0, 4)}</div>
</Link>
))}
</div>
</section>
)}
<section>
<h2 className="section-title mb-3">Треки</h2>
{tracks.length ? (
<div className="card p-2">
{tracks.map((t, i) => (
<TrackRow key={t.id} track={t} list={tracks} index={i} showCover={false} showUploader />
))}
</div>
) : (
<p className="text-sm text-zinc-500">Треков пока нет.</p>
)}
</section>
</div>
);
}