Files
TermorServer/apps/web/src/pages/home-page.tsx

185 lines
7.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 { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
import { type Track, coverArtUrl, fetchHome, fetchRecentlyPlayed, fetchTracks } from '@/lib/api'
import { Link } from 'react-router-dom'
import { usePlayerStore } from '@/stores/player-store'
export function HomePage() {
const setQueue = usePlayerStore((state) => state.setQueue)
const homeQuery = useQuery({
queryKey: ['home'],
queryFn: fetchHome,
})
const tracksQuery = useQuery({
queryKey: ['tracks'],
queryFn: fetchTracks,
})
const recentTracksQuery = useQuery({
queryKey: ['recently-played'],
queryFn: fetchRecentlyPlayed,
})
const allTracks = tracksQuery.data?.items ?? []
const heroTrack = allTracks[0]
const recentAlbums = homeQuery.data?.recentAlbums ?? []
const recentTracks = recentTracksQuery.data?.items ?? homeQuery.data?.recentTracks ?? []
const popularAlbums = [...recentAlbums].reverse()
if (homeQuery.isLoading || tracksQuery.isLoading) {
return <LoadingPanel title="Загружаю домашнюю страницу" />
}
if (homeQuery.isError || tracksQuery.isError) {
return (
<ErrorPanel
onRetry={() => {
void homeQuery.refetch()
void tracksQuery.refetch()
void recentTracksQuery.refetch()
}}
title="Не получилось собрать главную"
/>
)
}
return (
<div className="space-y-10">
<section className="relative overflow-hidden rounded-[14px] bg-[#111b2e]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(168,135,87,0.35),transparent_34%),linear-gradient(180deg,rgba(20,30,47,0.4),rgba(14,20,35,0.92))]" />
<div className="relative flex min-h-[290px] items-end gap-4 px-8 py-6">
<div className="h-64 w-64 shrink-0 overflow-hidden rounded-[14px] bg-[#1a2437] shadow-2xl">
{heroTrack?.coverArtId ? (
<img alt={heroTrack.title} className="h-full w-full object-cover" src={coverArtUrl(heroTrack.id)} />
) : null}
</div>
<div className="pb-3">
<h2 className="text-6xl font-semibold tracking-tight text-white">
{heroTrack?.title ?? 'Dream on (Live in Paris, 2001)'}
</h2>
<div className="mt-2 text-3xl font-medium text-slate-300">{heroTrack?.artistName ?? 'Depeche Mode'}</div>
<div className="mt-4 flex gap-2">
<Tag>{new Date().getFullYear()}</Tag>
<Tag>{formatDuration(heroTrack?.durationSeconds ?? 339)}</Tag>
</div>
</div>
<div className="ml-auto flex gap-2 self-center">
<CarouselButton icon={<ChevronLeft size={18} />} />
<CarouselButton icon={<ChevronRight size={18} />} />
</div>
</div>
</section>
<TrackRow title="Недавно прослушанные" tracks={recentTracks} onPlayAll={() => setQueue(recentTracks)} />
<AlbumRow title="Недавно добавленные" albums={recentAlbums} onPlayAll={() => setQueue(allTracks)} />
<AlbumRow title="Наиболее прослушиваемые" albums={popularAlbums} onPlayAll={() => setQueue(allTracks)} />
</div>
)
}
function TrackRow({
title,
tracks,
onPlayAll,
}: {
title: string
tracks: Track[]
onPlayAll: () => void
}) {
const playTrack = usePlayerStore((state) => state.playTrack)
return (
<section>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[2rem] font-semibold tracking-tight text-white">{title}</h3>
<div className="flex items-center gap-3">
<button className="text-base text-slate-400 transition hover:text-white" onClick={onPlayAll} type="button">
Еще
</button>
<CarouselButton icon={<ChevronLeft size={18} />} />
<CarouselButton icon={<ChevronRight size={18} />} />
</div>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-7">
{tracks.map((track) => (
<button
key={track.id}
className="text-left"
onClick={() => playTrack(track, tracks)}
type="button"
>
<div className="aspect-square overflow-hidden rounded-[8px] bg-[#232d42]">
{track.coverArtId ? (
<img alt={track.title} className="h-full w-full object-cover" src={coverArtUrl(track.id)} />
) : null}
</div>
<div className="mt-3 line-clamp-1 block text-[1.08rem] font-semibold text-white">{track.title}</div>
<div className="line-clamp-1 text-base text-slate-400">{track.artistName}</div>
<div className="line-clamp-1 text-sm text-slate-500">{track.albumTitle}</div>
</button>
))}
</div>
</section>
)
}
function AlbumRow({
title,
albums,
onPlayAll,
}: {
title: string
albums: Array<{
id: string
title: string
artistName: string
coverArtId: string
}>
onPlayAll: () => void
}) {
return (
<section>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[2rem] font-semibold tracking-tight text-white">{title}</h3>
<div className="flex items-center gap-3">
<button className="text-base text-slate-400 transition hover:text-white" onClick={onPlayAll} type="button">
Еще
</button>
<CarouselButton icon={<ChevronLeft size={18} />} />
<CarouselButton icon={<ChevronRight size={18} />} />
</div>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-7">
{albums.map((album) => (
<article key={album.id}>
<Link className="block aspect-square overflow-hidden rounded-[8px] bg-[#232d42]" to={`/albums/${album.id}`}>
{album.coverArtId ? (
<img alt={album.title} className="h-full w-full object-cover" src={coverArtUrl(album.id)} />
) : null}
</Link>
<Link className="mt-3 line-clamp-1 block text-[1.08rem] font-semibold text-white hover:underline" to={`/albums/${album.id}`}>
{album.title}
</Link>
<div className="text-base text-slate-400">{album.artistName}</div>
</article>
))}
</div>
</section>
)
}
function CarouselButton({ icon }: { icon: React.ReactNode }) {
return <button className="grid h-8 w-8 place-items-center rounded-[8px] border border-[#24314f] text-slate-400 hover:bg-[#18233a] hover:text-white" type="button">{icon}</button>
}
function Tag({ children }: { children: React.ReactNode }) {
return <div className="rounded-full bg-[#f1f4f8] px-4 py-1 text-sm font-semibold text-[#152035]">{children}</div>
}
function formatDuration(durationSeconds: number) {
const minutes = Math.floor(durationSeconds / 60)
const seconds = durationSeconds % 60
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}