Refactor admin panels to utilize CreateNameDialog for creating new entities
Updated the BumpersPanel, ChannelsPanel, CollectionsPanel, GroupsPanel, ClipGroupPanel, and ShowsPanel components to replace direct input fields with a CreateNameDialog for creating new items. This change enhances user experience by centralizing the creation process and improving UI consistency. Localization strings were also updated to reflect new dialog titles and labels in both English and Russian.
This commit is contained in:
@@ -5,9 +5,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { listChannels } from '@/features/admin/channels/api'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { useCreateDialog } from '@/shared/lib/use-create-dialog'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { CreateNameDialog } from '@/shared/ui/create-name-dialog'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { createBumperTemplate, listBumperTemplates } from './api'
|
||||
import { BumperTemplateEditor } from './components/BumperTemplateEditor'
|
||||
@@ -21,7 +22,7 @@ export function BumpersPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const onError = useApiError()
|
||||
const [name, setName] = useState('')
|
||||
const createDialog = useCreateDialog()
|
||||
const [channelId, setChannelId] = useState<string>('')
|
||||
|
||||
const { data: templates } = useQuery({
|
||||
@@ -35,9 +36,9 @@ export function BumpersPanel() {
|
||||
}
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: () => createBumperTemplate(name.trim()),
|
||||
mutationFn: () => createBumperTemplate(createDialog.value.trim()),
|
||||
onSuccess: () => {
|
||||
setName('')
|
||||
createDialog.close()
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
@@ -69,22 +70,8 @@ export function BumpersPanel() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-end gap-2">
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<Label>{t('admin.bumpers.newName')}</Label>
|
||||
<Input
|
||||
placeholder={t('admin.bumpers.newNamePlaceholder')}
|
||||
value={name}
|
||||
maxLength={64}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!name.trim() || create.isPending}
|
||||
onClick={() => create.mutate()}
|
||||
>
|
||||
<div className="flex flex-1 items-end justify-end">
|
||||
<Button size="sm" onClick={createDialog.show}>
|
||||
<Plus className="h-4 w-4" /> {t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -106,6 +93,18 @@ export function BumpersPanel() {
|
||||
<p className="text-sm text-muted-foreground">{t('admin.bumpers.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{createDialog.open && (
|
||||
<CreateNameDialog
|
||||
title={t('admin.bumpers.createTitle')}
|
||||
label={t('admin.bumpers.newName')}
|
||||
value={createDialog.value}
|
||||
onChange={createDialog.setValue}
|
||||
pending={create.isPending}
|
||||
onCreate={() => create.mutate()}
|
||||
onClose={createDialog.close}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { useCreateDialog } from '@/shared/lib/use-create-dialog'
|
||||
import { CreateNameDialog } from '@/shared/ui/create-name-dialog'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { createChannel, listChannels } from './api'
|
||||
|
||||
function slugify(value: string) {
|
||||
@@ -19,7 +23,7 @@ function slugify(value: string) {
|
||||
export function ChannelsPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const create = useCreateDialog()
|
||||
const [slug, setSlug] = useState('')
|
||||
|
||||
const { data, isLoading } = useQuery({ queryKey: qk.channels.all, queryFn: listChannels })
|
||||
@@ -27,10 +31,14 @@ export function ChannelsPanel() {
|
||||
const onError = useApiError()
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createChannel({ name: name.trim(), slug: slug || slugify(name) }),
|
||||
mutationFn: () =>
|
||||
createChannel({
|
||||
name: create.value.trim(),
|
||||
slug: slug || slugify(create.value),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setName('')
|
||||
setSlug('')
|
||||
create.close()
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
@@ -38,26 +46,16 @@ export function ChannelsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.channels.title')}</h2>
|
||||
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.channels.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.channels.slug')}
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(slugify(e.target.value))}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.channels.title')}</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
onClick={() => {
|
||||
setSlug('')
|
||||
create.show()
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -103,6 +101,28 @@ export function ChannelsPanel() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{create.open && (
|
||||
<CreateNameDialog
|
||||
title={t('admin.channels.createTitle')}
|
||||
label={t('admin.channels.name')}
|
||||
value={create.value}
|
||||
onChange={create.setValue}
|
||||
pending={createMutation.isPending}
|
||||
onCreate={() => createMutation.mutate()}
|
||||
onClose={create.close}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.slug')}</Label>
|
||||
{/* Пустой slug выводится из названия — поле нужно только тогда, когда хочется своё. */}
|
||||
<Input
|
||||
placeholder={slugify(create.value)}
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(slugify(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</CreateNameDialog>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Image as ImageIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Image as ImageIcon, Plus } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { imageUrl } from '@/features/admin/images/api'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { useCreateDialog } from '@/shared/lib/use-create-dialog'
|
||||
import { CreateNameDialog } from '@/shared/ui/create-name-dialog'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { sortRows, useTableSort } from '@/shared/lib/table-sort'
|
||||
import { SortHeader } from '@/shared/ui/sortable'
|
||||
@@ -17,7 +17,7 @@ import { CollectionSuggestions } from './CollectionSuggestions'
|
||||
export function CollectionsPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const create = useCreateDialog()
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
@@ -29,9 +29,9 @@ export function CollectionsPanel() {
|
||||
const onError = useApiError()
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createCollection({ name: name.trim() }),
|
||||
mutationFn: () => createCollection({ name: create.value.trim() }),
|
||||
onSuccess: () => {
|
||||
setName('')
|
||||
create.close()
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
@@ -58,26 +58,18 @@ export function CollectionsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.collections.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('admin.collections.hint')}</p>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.collections.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('admin.collections.hint')}</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={create.show}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.collections.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
|
||||
{/* Постер коллекции — обложка её первой части: своей картинки у франшизы нет, а без
|
||||
обложки она теряется в списке одинаковых строк. */}
|
||||
<Button
|
||||
@@ -166,6 +158,19 @@ export function CollectionsPanel() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{create.open && (
|
||||
<CreateNameDialog
|
||||
title={t('admin.collections.createTitle')}
|
||||
label={t('admin.collections.name')}
|
||||
description={t('admin.collections.hint')}
|
||||
value={create.value}
|
||||
onChange={create.setValue}
|
||||
pending={createMutation.isPending}
|
||||
onCreate={() => createMutation.mutate()}
|
||||
onClose={create.close}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { useCreateDialog } from '@/shared/lib/use-create-dialog'
|
||||
import { CreateNameDialog } from '@/shared/ui/create-name-dialog'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { sortRows, useTableSort } from '@/shared/lib/table-sort'
|
||||
import { SortHeader } from '@/shared/ui/sortable'
|
||||
@@ -16,7 +19,7 @@ import { GroupSuggestions } from './GroupSuggestions'
|
||||
export function GroupsPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const create = useCreateDialog()
|
||||
const [query, setQuery] = useState('')
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
|
||||
@@ -26,9 +29,9 @@ export function GroupsPanel() {
|
||||
const onError = useApiError()
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createGroup({ name: name.trim() }),
|
||||
mutationFn: () => createGroup({ name: create.value.trim() }),
|
||||
onSuccess: () => {
|
||||
setName('')
|
||||
create.close()
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
@@ -48,23 +51,13 @@ export function GroupsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.groups.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('admin.groups.hint')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.groups.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.groups.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('admin.groups.hint')}</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={create.show}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -156,6 +149,19 @@ export function GroupsPanel() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{create.open && (
|
||||
<CreateNameDialog
|
||||
title={t('admin.groups.createTitle')}
|
||||
label={t('admin.groups.name')}
|
||||
description={t('admin.groups.hint')}
|
||||
value={create.value}
|
||||
onChange={create.setValue}
|
||||
pending={createMutation.isPending}
|
||||
onCreate={() => createMutation.mutate()}
|
||||
onClose={create.close}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { addGroupElements, createGroup, listGroups } from '@/features/admin/groups/api'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { useCreateDialog } from '@/shared/lib/use-create-dialog'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { CreateNameDialog } from '@/shared/ui/create-name-dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { cn } from '@/shared/lib/cn'
|
||||
import { readDragItem } from './dnd'
|
||||
@@ -19,7 +21,7 @@ export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown)
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [selected, setSelected] = useState('')
|
||||
const [newName, setNewName] = useState('')
|
||||
const create = useCreateDialog()
|
||||
const [over, setOver] = useState(false)
|
||||
|
||||
const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups })
|
||||
@@ -28,9 +30,9 @@ export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown)
|
||||
}
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createGroup({ name: newName.trim() }),
|
||||
mutationFn: () => createGroup({ name: create.value.trim() }),
|
||||
onSuccess: ({ id }) => {
|
||||
setNewName('')
|
||||
create.close()
|
||||
setSelected(id)
|
||||
invalidate()
|
||||
},
|
||||
@@ -112,22 +114,22 @@ export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('admin.interstitials.newGroupName')}
|
||||
value={newName}
|
||||
maxLength={256}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
<Button size="sm" variant="outline" onClick={create.show}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
|
||||
{create.open && (
|
||||
<CreateNameDialog
|
||||
title={t('admin.interstitials.createGroupTitle')}
|
||||
label={t('admin.interstitials.newGroupName')}
|
||||
value={create.value}
|
||||
onChange={create.setValue}
|
||||
pending={createMutation.isPending}
|
||||
onCreate={() => createMutation.mutate()}
|
||||
onClose={create.close}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!newName.trim() || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,7 +277,11 @@ export function InterstitialsPanel() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{preview?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{preview?.mediaAssetId && <HlsVideo src={mediaPreviewUrl(preview.mediaAssetId)} />}
|
||||
{/* Кнопка в списке — это «плей», а не «открыть карточку»: жать ещё раз внутри окна
|
||||
незачем, ролик на тридцать секунд. */}
|
||||
{preview?.mediaAssetId && (
|
||||
<HlsVideo src={mediaPreviewUrl(preview.mediaAssetId)} autoPlay />
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AUDIENCE_UNSET,
|
||||
SHOW_AUDIENCES,
|
||||
type ShowAudience,
|
||||
type ShowKind,
|
||||
} from '@/shared/api/types'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { createShow } from './api'
|
||||
|
||||
/**
|
||||
* Заведение шоу. Отдельным окном, а не строкой над таблицей: поля создания стояли вплотную к полям
|
||||
* фильтра, и в них постоянно начинали искать.
|
||||
*/
|
||||
export function CreateShowDialog({
|
||||
onClose,
|
||||
onCreated,
|
||||
}: Readonly<{ onClose: () => void; onCreated: () => void }>) {
|
||||
const { t } = useTranslation()
|
||||
const onError = useApiError()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [originalName, setOriginalName] = useState('')
|
||||
const [kind, setKind] = useState<ShowKind>('Series')
|
||||
// Новое шоу заводится без рейтинга: проставят метаданные либо админ руками.
|
||||
const [audience, setAudience] = useState<ShowAudience | null>(null)
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createShow({
|
||||
name: name.trim(),
|
||||
kind,
|
||||
originalName: originalName.trim() || undefined,
|
||||
audience,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
onCreated()
|
||||
onClose()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
if (name.trim() && !createMutation.isPending) createMutation.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.shows.createTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.shows.name')}</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
// Enter в единственном обязательном поле — самый быстрый путь: форма короткая.
|
||||
onKeyDown={(e) => e.key === 'Enter' && submit()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.shows.originalName')}</Label>
|
||||
<Input value={originalName} onChange={(e) => setOriginalName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.shows.kind')}</Label>
|
||||
<Select value={kind} onValueChange={(v) => setKind(v as ShowKind)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Series">{t('admin.shows.kinds.Series')}</SelectItem>
|
||||
<SelectItem value="Single">{t('admin.shows.kinds.Single')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.shows.audience')}</Label>
|
||||
<Select
|
||||
value={audience ?? AUDIENCE_UNSET}
|
||||
onValueChange={(v) => setAudience(v === AUDIENCE_UNSET ? null : (v as ShowAudience))}
|
||||
>
|
||||
<SelectTrigger className="whitespace-nowrap">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AUDIENCE_UNSET}>{t('admin.shows.audienceUnset')}</SelectItem>
|
||||
{SHOW_AUDIENCES.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(`admin.shows.audiences.${value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button size="sm" variant="outline" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" disabled={!name.trim() || createMutation.isPending} onClick={submit}>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import {
|
||||
AUDIENCE_UNSET,
|
||||
SHOW_AUDIENCES,
|
||||
type ShowAudience,
|
||||
type ShowKind,
|
||||
type ShowSummaryDto,
|
||||
} from '@/shared/api/types'
|
||||
import type { ShowKind, ShowSummaryDto } from '@/shared/api/types'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
@@ -19,7 +14,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { sortRows, useTableSort } from '@/shared/lib/table-sort'
|
||||
import { SortHeader } from '@/shared/ui/sortable'
|
||||
import { listGenres } from '@/features/admin/genres/api'
|
||||
import { createShow, deleteShow, listShows } from './api'
|
||||
import { deleteShow, listShows } from './api'
|
||||
import { CreateShowDialog } from './CreateShowDialog'
|
||||
import { DeleteShowDialog } from './DeleteShowDialog'
|
||||
import { BulkTagBar } from './BulkTagBar'
|
||||
|
||||
@@ -28,11 +24,7 @@ const PAGE_SIZE = 20
|
||||
export function ShowsPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const [originalName, setOriginalName] = useState('')
|
||||
const [kind, setKind] = useState<ShowKind>('Series')
|
||||
// Новое шоу заводится без рейтинга: проставят метаданные либо админ руками.
|
||||
const [audience, setAudience] = useState<ShowAudience | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
@@ -97,21 +89,6 @@ export function ShowsPanel() {
|
||||
current.includes(showId) ? current.filter((id) => id !== showId) : [...current, showId],
|
||||
)
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createShow({
|
||||
name: name.trim(),
|
||||
kind,
|
||||
originalName: originalName.trim() || undefined,
|
||||
audience,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setName('')
|
||||
setOriginalName('')
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
// Шоу к удалению: сначала спрашиваем про файлы, и только потом удаляем.
|
||||
const [toDelete, setToDelete] = useState<ShowSummaryDto | null>(null)
|
||||
const deleteMutation = useMutation({
|
||||
@@ -133,52 +110,10 @@ export function ShowsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.shows.title')}</h2>
|
||||
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.shows.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder={t('admin.shows.originalName')}
|
||||
value={originalName}
|
||||
onChange={(e) => setOriginalName(e.target.value)}
|
||||
/>
|
||||
<Select value={kind} onValueChange={(v) => setKind(v as ShowKind)}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Series">{t('admin.shows.kinds.Series')}</SelectItem>
|
||||
<SelectItem value="Single">{t('admin.shows.kinds.Single')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={audience ?? AUDIENCE_UNSET}
|
||||
onValueChange={(v) => setAudience(v === AUDIENCE_UNSET ? null : (v as ShowAudience))}
|
||||
>
|
||||
{/* Та же ширина, что у селекта на карточке шоу: расшифровки рейтингов длинные. */}
|
||||
<SelectTrigger className="w-64 whitespace-nowrap">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AUDIENCE_UNSET}>{t('admin.shows.audienceUnset')}</SelectItem>
|
||||
{SHOW_AUDIENCES.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(`admin.shows.audiences.${value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="crt-glow text-xl font-semibold">{t('admin.shows.title')}</h2>
|
||||
<Button size="sm" onClick={() => setCreating(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -340,6 +275,8 @@ export function ShowsPanel() {
|
||||
|
||||
<Pager page={page} totalPages={totalPages} onChange={setPage} />
|
||||
|
||||
{creating && <CreateShowDialog onClose={() => setCreating(false)} onCreated={invalidate} />}
|
||||
|
||||
{toDelete && (
|
||||
<DeleteShowDialog
|
||||
show={toDelete}
|
||||
|
||||
Reference in New Issue
Block a user