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

109 lines
4.4 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 { Heart, Play, Plus } from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import { tracksApi } from "../lib/api";
import { formatDate, formatDuration } from "../lib/format";
import { usePlayer } from "../store/player";
import { AddToPlaylistModal } from "../components/AddToPlaylistModal";
import { ArtistLinks } from "../components/ArtistLinks";
import { Comments } from "../components/Comments";
import { Cover } from "../components/Cover";
import { LyricsView } from "../components/LyricsView";
import { ShareButton } from "../components/ShareButton";
export function TrackPage() {
const { id = "" } = useParams();
const playNow = usePlayer((s) => s.playNow);
const addToQueue = usePlayer((s) => s.addToQueue);
const qc = useQueryClient();
const [addOpen, setAddOpen] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ["track", id],
queryFn: () => tracksApi.get(id),
enabled: Boolean(id),
});
const like = useMutation({
mutationFn: () => (data?.liked ? tracksApi.unlike(id) : tracksApi.like(id)),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ["track", 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 { track } = data;
return (
<div className="mx-auto max-w-5xl animate-fade-in">
<div className="card mb-6 flex flex-col gap-5 p-5 sm:flex-row sm:items-end">
<Cover src={track.coverUrl} name={track.title} className="h-40 w-40 shrink-0" rounded="rounded-2xl" />
<div className="min-w-0 flex-1">
<span className="chip">Трек</span>
<h1 className="mt-2 truncate text-3xl font-bold tracking-tight text-white">{track.title}</h1>
<p className="mt-1 text-sm text-zinc-400">
<ArtistLinks artists={track.artists} fallback={track.artist?.name} linkClassName="link" />
{track.album && (
<>
{" · "}
<Link to={`/album/${track.album.id}`} className="link">
{track.album.title}
</Link>
</>
)}
</p>
<p className="mt-1 text-sm text-zinc-500">
{formatDuration(track.durationMs)}
{track.releaseDate && ` · ${formatDate(track.releaseDate)}`}
{` · ${track.playCount} прослушиваний`}
</p>
<p className="mt-2 text-xs text-zinc-500">
Добавил в библиотеку:{" "}
{track.uploadedBy ? (
<Link to={`/u/${track.uploadedBy.username}`} className="link">
{track.uploadedBy.displayName || track.uploadedBy.username}
</Link>
) : (
"неизвестно"
)}
</p>
<div className="mt-4 flex flex-wrap gap-2">
<button className="btn-primary" onClick={() => playNow(track, [track])}>
<Play size={15} /> Играть
</button>
<button className="btn-ghost" onClick={() => addToQueue(track)}>
<Plus size={15} /> В очередь
</button>
<button className={data.liked ? "btn-danger" : "btn-ghost"} onClick={() => like.mutate()}>
<Heart size={15} fill={data.liked ? "currentColor" : "none"} />
{data.liked ? "В избранном" : "В избранное"}
</button>
<button className="btn-ghost" onClick={() => setAddOpen(true)}>
В плейлист
</button>
<a className="btn-ghost" href={`/api/tracks/${id}/stream?download=1`}>
Скачать
</a>
<ShareButton path={`/track/${id}`} />
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<section className="card max-h-[60vh] overflow-hidden p-2">
<h2 className="section-title px-3 pb-2 pt-3">Текст песни</h2>
<LyricsView trackId={id} />
</section>
<Comments trackId={id} />
</div>
<AddToPlaylistModal track={track} open={addOpen} onClose={() => setAddOpen(false)} />
</div>
);
}