Implement pagination for candidates and episodes in ShowDetail component: add Pager component for both lists, update state management for page navigation, and ensure safe page handling during filtering and updates.
build / backend (push) Successful in 1m52s
build / frontend (push) Successful in 53s
tests / backend-tests (push) Successful in 2m10s

This commit is contained in:
Leonid Pershin
2026-07-25 20:33:44 +03:00
parent 7694ff3388
commit 8484587313
@@ -8,6 +8,7 @@ import type { MediaAssetDto } from '@/shared/api/types'
import { Badge } from '@/shared/ui/badge' import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button' import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input' import { Input } from '@/shared/ui/input'
import { Pager } from '@/shared/ui/pager'
import { toast } from '@/shared/ui/toast-store' import { toast } from '@/shared/ui/toast-store'
import { listMedia } from '@/features/admin/media/api' import { listMedia } from '@/features/admin/media/api'
import { import {
@@ -23,12 +24,16 @@ import { addEpisode, getShow, removeEpisode } from './api'
type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode } type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
const PAGE_SIZE = 20
export function ShowDetail({ showId }: { showId: string }) { export function ShowDetail({ showId }: { showId: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [filter, setFilter] = useState('') const [filter, setFilter] = useState('')
const [deselected, setDeselected] = useState<Set<string>>(new Set()) const [deselected, setDeselected] = useState<Set<string>>(new Set())
const [adding, setAdding] = useState<{ current: number; total: number } | null>(null) const [adding, setAdding] = useState<{ current: number; total: number } | null>(null)
const [candPage, setCandPage] = useState(1)
const [epPage, setEpPage] = useState(1)
const { data: show, isLoading } = useQuery({ const { data: show, isLoading } = useQuery({
queryKey: ['admin', 'shows', showId], queryKey: ['admin', 'shows', showId],
@@ -84,6 +89,16 @@ export function ShowDetail({ showId }: { showId: string }) {
const isSingle = show.kind === 'Single' const isSingle = show.kind === 'Single'
const canAdd = !isSingle || show.episodes.length === 0 const canAdd = !isSingle || show.episodes.length === 0
// Постраничный вывод длинных списков (кандидаты на добавление и сами серии). Страницу зажимаем в
// допустимый диапазон — чтобы после удаления/фильтрации не застрять на пустой странице.
const candTotalPages = Math.max(1, Math.ceil(candidates.length / PAGE_SIZE))
const candPageSafe = Math.min(candPage, candTotalPages)
const candItems = candidates.slice((candPageSafe - 1) * PAGE_SIZE, candPageSafe * PAGE_SIZE)
const epTotalPages = Math.max(1, Math.ceil(show.episodes.length / PAGE_SIZE))
const epPageSafe = Math.min(epPage, epTotalPages)
const epOffset = (epPageSafe - 1) * PAGE_SIZE
const epItems = show.episodes.slice(epOffset, epOffset + PAGE_SIZE)
const toggle = (id: string) => const toggle = (id: string) =>
setDeselected((prev) => { setDeselected((prev) => {
const next = new Set(prev) const next = new Set(prev)
@@ -151,7 +166,10 @@ export function ShowDetail({ showId }: { showId: string }) {
className="max-w-md" className="max-w-md"
placeholder={t('admin.shows.filterAssets')} placeholder={t('admin.shows.filterAssets')}
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => {
setCandPage(1)
setFilter(e.target.value)
}}
/> />
<Button size="sm" variant="outline" onClick={() => setDeselected(new Set())}> <Button size="sm" variant="outline" onClick={() => setDeselected(new Set())}>
{t('admin.shows.selectAll')} {t('admin.shows.selectAll')}
@@ -175,7 +193,7 @@ export function ShowDetail({ showId }: { showId: string }) {
<p className="px-4 py-3 text-sm text-muted-foreground">{t('admin.shows.noMatches')}</p> <p className="px-4 py-3 text-sm text-muted-foreground">{t('admin.shows.noMatches')}</p>
) : ( ) : (
<ul className="divide-y divide-border text-sm"> <ul className="divide-y divide-border text-sm">
{candidates.map(({ asset, parsed }) => { {candItems.map(({ asset, parsed }) => {
const label = formatSeasonEpisode(parsed) const label = formatSeasonEpisode(parsed)
return ( return (
<li key={asset.id}> <li key={asset.id}>
@@ -194,6 +212,7 @@ export function ShowDetail({ showId }: { showId: string }) {
</ul> </ul>
)} )}
</div> </div>
<Pager page={candPageSafe} totalPages={candTotalPages} onChange={setCandPage} />
</div> </div>
)} )}
@@ -209,7 +228,7 @@ export function ShowDetail({ showId }: { showId: string }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{show.episodes.map((episode, index) => { {epItems.map((episode, index) => {
const parsed = const parsed =
episode.season != null && episode.episode != null episode.season != null && episode.episode != null
? { season: episode.season, episode: episode.episode } ? { season: episode.season, episode: episode.episode }
@@ -217,7 +236,7 @@ export function ShowDetail({ showId }: { showId: string }) {
const label = formatSeasonEpisode(parsed) const label = formatSeasonEpisode(parsed)
return ( return (
<tr key={episode.id} className="border-b border-border last:border-0"> <tr key={episode.id} className="border-b border-border last:border-0">
<td className="px-4 py-2 text-muted-foreground">{index + 1}</td> <td className="px-4 py-2 text-muted-foreground">{epOffset + index + 1}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{episode.stillImageId && ( {episode.stillImageId && (
@@ -270,6 +289,7 @@ export function ShowDetail({ showId }: { showId: string }) {
</tbody> </tbody>
</table> </table>
</div> </div>
<Pager page={epPageSafe} totalPages={epTotalPages} onChange={setEpPage} />
</div> </div>
) )
} }