import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Pencil, Plus, Trash2 } from 'lucide-react' import { useState } from 'react' import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { z } from 'zod' import { qk } from '@/shared/api/query-keys' import type { GenreDto } 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 { Input } from '@/shared/ui/input' import { Label } from '@/shared/ui/label' import { sortRows, useTableSort } from '@/shared/lib/table-sort' import { SortHeader } from '@/shared/ui/sortable' import { createGenre, deleteGenre, listGenres, updateGenre } from './api' const createSchema = z.object({ name: z.string().min(1).max(128), slug: z .string() .min(1) .max(64) .regex(/^[a-z0-9-]+$/), aliases: z.string().max(1024).optional(), }) const editSchema = z.object({ name: z.string().min(1).max(128), sortOrder: z.number().int().min(0), aliases: z.string().max(1024).optional(), }) type CreateValues = z.infer type EditValues = z.infer /** Псевдонимы вводятся одной строкой через запятую — их редко больше десятка. */ function parseAliases(value: string | undefined): string[] { return (value ?? '') .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0) } export function GenresPanel() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: genres, isLoading } = useQuery({ queryKey: qk.genres.all, queryFn: listGenres, }) const { sort, toggle } = useTableSort('sortOrder', false) const sortedGenres = sortRows(genres ?? [], sort, { sortOrder: (g) => g.sortOrder, name: (g) => g.name.toLowerCase(), slug: (g) => g.slug, showCount: (g) => g.showCount, }) const invalidate = () => queryClient.invalidateQueries({ queryKey: qk.genres.all }) const reportError = useApiError() const createMutation = useMutation({ mutationFn: createGenre, onSuccess: invalidate }) const updateMutation = useMutation({ mutationFn: ({ id, ...body }: Readonly<{ id: string name: string sortOrder: number aliases: string[] }>) => updateGenre(id, body), onSuccess: invalidate, }) const deleteMutation = useMutation({ mutationFn: deleteGenre, onSuccess: invalidate, onError: reportError, }) const [createOpen, setCreateOpen] = useState(false) const [editing, setEditing] = useState(null) const createForm = useForm({ resolver: zodResolver(createSchema) }) const editForm = useForm({ resolver: zodResolver(editSchema) }) const openEdit = (genre: GenreDto) => { setEditing(genre) editForm.reset({ name: genre.name, sortOrder: genre.sortOrder, aliases: genre.aliases.join(', '), }) } const onCreate = async (values: CreateValues) => { try { await createMutation.mutateAsync({ name: values.name, slug: values.slug, aliases: parseAliases(values.aliases), }) createForm.reset() setCreateOpen(false) } catch (error) { reportError(error) } } const onEdit = async (values: EditValues) => { if (!editing) return try { await updateMutation.mutateAsync({ id: editing.id, name: values.name, sortOrder: values.sortOrder, aliases: parseAliases(values.aliases), }) setEditing(null) } catch (error) { reportError(error) } } return (

{t('admin.genres.title')}

{t('admin.genres.hint')}

{isLoading && ( )} {sortedGenres.map((genre) => ( ))}
{t('admin.genres.aliases')} {t('common.actions')}
{t('common.loading')}
{genre.sortOrder}
{genre.name} {genre.isSystem && {t('admin.genres.system')}}
{genre.slug} {genre.aliases.join(', ')} {genre.showCount}
{t('admin.genres.create')}

{t('admin.genres.slugHint')}

{t('admin.genres.aliasesHint')}

!open && setEditing(null)}> {t('admin.genres.edit')}

{t('admin.genres.aliasesHint')}

) }