import { unwrap } from '@pipu/core'; import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from '@tanstack/react-router'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession } from '../app/session'; import { TrackList, type Track } from '../entities/catalog'; import { useCollectionCounts } from '../entities/collection'; import { usePlayer } from '../player/context'; import { useErrorText } from '../shared/lib/errors'; import { dislikeFillIcon, historyFillIcon, likeFillIcon, playIcon, shuffleIcon } from '../shared/ui/icons'; import { Button, Dialog, FormError, Icon } from '../shared/ui/ui'; import c from './catalog.module.css'; import s from './page.module.css'; export type CollectionTab = 'history' | 'likes' | 'dislikes'; const TABS: CollectionTab[] = ['history', 'likes', 'dislikes']; const HERO: Record = { history: { icon: historyFillIcon, tile: 'linear-gradient(140deg, #35D3FF, #2F80FF 60%, #1B3FA8)' }, likes: { icon: likeFillIcon, tile: 'linear-gradient(140deg, #5E9BFF, #2F80FF 50%, #7C5CFF)' }, dislikes: { icon: dislikeFillIcon, tile: 'linear-gradient(140deg, #4A5572, #1A2236)' }, }; export function CollectionPage({ tab }: { tab: CollectionTab }) { const { t } = useTranslation(); const counts = useCollectionCounts(); const hero = HERO[tab]; const sub = tab === 'likes' ? t('collection.sub_likes', { count: counts.likes ?? 0 }) : t(tab === 'history' ? 'collection.sub_history' : 'collection.sub_dislikes'); return (
{t('collection.title')}

{t(`collection.${tab}`)}

{sub}
{TABS.map((k) => ( {t(`collection.${k}`)} ))}
{tab === 'history' ? : }
); } /** Ключ дня в локальном времени: история группируется на клиенте. */ function dayKey(iso: string): string { const d = new Date(iso); return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; } function dayTitle(iso: string, locale: string, t: (k: 'collection.today' | 'collection.yesterday') => string): string { const d = new Date(iso); const today = new Date(); const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); if (dayKey(iso) === dayKey(today.toISOString())) return t('collection.today'); if (dayKey(iso) === dayKey(yesterday.toISOString())) return t('collection.yesterday'); return new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'long', year: 'numeric' }).format(d); } function History() { const { t, i18n } = useTranslation(); const session = useSession(); const queryClient = useQueryClient(); const errorText = useErrorText(); const [confirm, setConfirm] = useState(false); const history = useInfiniteQuery({ queryKey: ['history'], initialPageParam: undefined as string | undefined, queryFn: async ({ pageParam }) => unwrap(await session.client.GET('/api/v1/me/history', { params: { query: { cursor: pageParam, limit: 100 } } })), getNextPageParam: (p) => p.next_cursor ?? undefined, }); const clear = useMutation({ mutationFn: async () => unwrap(await session.client.DELETE('/api/v1/me/history')), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['history'] }), }); const items = history.data?.pages.flatMap((p) => p.items) ?? []; const groups: { key: string; title: string; items: typeof items }[] = []; for (const it of items) { const key = dayKey(it.played_at); const last = groups.at(-1); if (last?.key === key) last.items.push(it); else groups.push({ key, title: dayTitle(it.played_at, i18n.language, t), items: [it] }); } const time = new Intl.DateTimeFormat(i18n.language, { hour: '2-digit', minute: '2-digit' }); return ( <> {items.length > 0 && (
)} {history.error || clear.error ? errorText(history.error ?? clear.error) : null} {history.isSuccess && items.length === 0 &&
{t('collection.empty_history')}
} {groups.map((g) => (

{g.title}

i.track)} showAlbum context={{ type: 'history' }} lead={(_, i) => time.format(new Date(g.items[i]!.played_at))} />
))} {history.hasNextPage && ( )} {confirm && ( setConfirm(false)} footer={ <> } >

{t('collection.clear_confirm')}

)} ); } function Reacted({ reaction }: { reaction: 'like' | 'dislike' }) { const { t } = useTranslation(); const session = useSession(); const player = usePlayer(); const errorText = useErrorText(); const path = reaction === 'like' ? '/api/v1/me/likes' : '/api/v1/me/dislikes'; const list = useInfiniteQuery({ queryKey: [reaction === 'like' ? 'likes' : 'dislikes'], initialPageParam: undefined as string | undefined, queryFn: async ({ pageParam }) => unwrap(await session.client.GET(path, { params: { query: { cursor: pageParam, limit: 100 } } })), getNextPageParam: (p) => p.next_cursor ?? undefined, }); const tracks: Track[] = list.data?.pages.flatMap((p) => p.items.map((i) => i.track)) ?? []; const total = list.data?.pages[0]?.total ?? 0; const context = { type: 'collection' as const, id: reaction === 'like' ? 'likes' : 'dislikes' }; return ( <> {reaction === 'like' && tracks.length > 0 && (
{t('catalog.track_count', { count: total })}
)} {list.error ? errorText(list.error) : null} {list.isSuccess && tracks.length === 0 && (
{t(reaction === 'like' ? 'collection.empty_likes' : 'collection.empty_dislikes')}
)} {tracks.length > 0 && ( String(i + 1)} disliked={reaction === 'dislike'} /> )} {list.hasNextPage && ( )} ); }