Files
TeleWave/frontend/src/features/admin/shows/ShowGenresField.tsx
T

117 lines
4.4 KiB
TypeScript

import { useMutation, useQuery } from '@tanstack/react-query'
import { Tag } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { listGenres } from '@/features/admin/genres/api'
import { qk } from '@/shared/api/query-keys'
import type { ShowDto } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
import { setShowGenres } from './api'
/**
* Жанры шоу: бейджи в шапке карточки + диалог правки. Основной жанр отмечается отдельно —
* он показывается в списке шоу и участвует в отборе контента наравне с остальными.
*/
export function ShowGenresField({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<string[]>([])
const [primary, setPrimary] = useState<string | null>(null)
const { data: genres } = useQuery({
queryKey: qk.genres.all,
queryFn: listGenres,
enabled: open,
})
const onError = useApiError()
const mutation = useMutation({
mutationFn: () => setShowGenres(show.id, selected, primary),
onSuccess: () => {
onChanged()
setOpen(false)
},
onError,
})
const openDialog = () => {
setSelected(show.genres.map((g) => g.id))
setPrimary(show.genres.find((g) => g.isPrimary)?.id ?? null)
setOpen(true)
}
const toggle = (id: string) => {
setSelected((prev) => {
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
// Снятый жанр не может остаться основным — иначе сервер молча выберет другой.
if (!next.includes(id) && primary === id) setPrimary(next[0] ?? null)
if (next.includes(id) && primary === null) setPrimary(id)
return next
})
}
return (
<>
<div className="flex flex-wrap items-center gap-2">
{show.genres.length === 0 ? (
<span className="text-sm text-muted-foreground">{t('admin.shows.genresEmpty')}</span>
) : (
show.genres.map((genre) => (
<Badge key={genre.id} variant={genre.isPrimary ? 'default' : 'muted'}>
{genre.name}
</Badge>
))
)}
<Button size="sm" variant="outline" onClick={openDialog}>
<Tag className="h-4 w-4" /> {t('admin.shows.genresEdit')}
</Button>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('admin.shows.genresEdit')}</DialogTitle>
</DialogHeader>
<p className="text-xs text-muted-foreground">{t('admin.shows.genresHint')}</p>
<div className="flex max-h-80 flex-col gap-1 overflow-y-auto">
{(genres ?? []).map((genre) => {
const checked = selected.includes(genre.id)
return (
<div
key={genre.id}
className="flex items-center justify-between gap-3 rounded px-2 py-1 hover:bg-muted/40"
>
<label className="flex flex-1 items-center gap-2 text-sm">
<input type="checkbox" checked={checked} onChange={() => toggle(genre.id)} />
{genre.name}
</label>
{checked && (
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
<input
type="radio"
name="primaryGenre"
checked={primary === genre.id}
onChange={() => setPrimary(genre.id)}
/>
{t('admin.shows.genrePrimary')}
</label>
)}
</div>
)
})}
</div>
<DialogFooter>
<Button disabled={mutation.isPending} onClick={() => mutation.mutate()}>
{t('common.save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}