Initial commit

This commit is contained in:
2026-10-05 20:38:28 +02:00
commit 60a68b420e
107 changed files with 21716 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
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>
);
}