120 lines
4.4 KiB
TypeScript
120 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,
|
|
}: Readonly<{ 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>
|
|
</>
|
|
)
|
|
}
|