Implement BumperEndpoints and remove deprecated bumper-related functionality
Added new BumperEndpoints to the API for managing bumper templates and variants, enhancing the channel management capabilities. Removed outdated bumper-related commands and handlers from the application, streamlining the codebase and improving maintainability. Updated ChannelEndpoints to reflect these changes and ensure proper routing for the new endpoints.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cssColor } from '@/features/admin/channels/lib/format'
|
||||
import type { BumperFont, BumperTemplateDto } from '@/shared/api/types'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { addBumperVariant, deleteBumperTemplate, updateBumperTemplate } from '../api'
|
||||
import { BumperAudioField, BumperBackgroundField } from './BumperFileFields'
|
||||
import { BumperPreviewPlayer } from './BumperPreviewPlayer'
|
||||
import { BumperVariantEditor } from './BumperVariantEditor'
|
||||
|
||||
export function BumperTemplateEditor({
|
||||
template,
|
||||
channelId,
|
||||
onChanged,
|
||||
onError,
|
||||
}: Readonly<{
|
||||
template: BumperTemplateDto
|
||||
/** Канал, чьими глазами смотрим на образцы подстановки; null — общие заглушки. */
|
||||
channelId: string | null
|
||||
onChanged: () => void
|
||||
onError: (e: unknown) => void
|
||||
}>) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [style, setStyle] = useState({
|
||||
name: template.name,
|
||||
font: template.font,
|
||||
backgroundColor: template.backgroundColor,
|
||||
backgroundColor2: template.backgroundColor2,
|
||||
accentColor: template.accentColor,
|
||||
textColor: template.textColor,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setStyle({
|
||||
name: template.name,
|
||||
font: template.font,
|
||||
backgroundColor: template.backgroundColor,
|
||||
backgroundColor2: template.backgroundColor2,
|
||||
accentColor: template.accentColor,
|
||||
textColor: template.textColor,
|
||||
})
|
||||
}, [template])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () => updateBumperTemplate(template.id, { ...style, name: style.name.trim() }),
|
||||
onSuccess: () => {
|
||||
toast.success(t('settings.saved'))
|
||||
onChanged()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
const remove = useMutation({
|
||||
mutationFn: () => deleteBumperTemplate(template.id),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
const addVariant = useMutation({
|
||||
mutationFn: () => addBumperVariant(template.id, t('admin.bumpers.newVariantName')),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
|
||||
const colorFields = [
|
||||
{ key: 'backgroundColor', label: t('admin.bumpers.colorBg') },
|
||||
{ key: 'backgroundColor2', label: t('admin.bumpers.colorBg2') },
|
||||
{ key: 'accentColor', label: t('admin.bumpers.colorAccent') },
|
||||
{ key: 'textColor', label: t('admin.bumpers.colorText') },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-md border border-border bg-muted/30 p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-expanded={open}
|
||||
className="flex flex-1 cursor-pointer flex-wrap items-center gap-2 text-left"
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
<span className="text-sm font-medium">{template.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{template.hasAudio && template.audioDurationSeconds != null
|
||||
? `≈${Math.round(template.audioDurationSeconds)} ${t('admin.bumpers.seconds')}`
|
||||
: t('admin.bumpers.defaultDuration')}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
· {t('admin.bumpers.variantsCount', { count: template.variants.length })}
|
||||
</span>
|
||||
{/* Блок общий: сколько врезок на него ссылается — не справка, а условие правки. */}
|
||||
{template.usageCount > 0 && (
|
||||
<Badge variant="muted">
|
||||
{t('admin.bumpers.usedInJunctions', { count: template.usageCount })}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={remove.isPending || template.usageCount > 0}
|
||||
title={template.usageCount > 0 ? t('admin.bumpers.cannotDeleteUsed') : undefined}
|
||||
onClick={() => remove.mutate()}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.bumpers.name')}</Label>
|
||||
<Input
|
||||
value={style.name}
|
||||
maxLength={64}
|
||||
onChange={(e) => setStyle((s) => ({ ...s, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.bumpers.font')}</Label>
|
||||
<Select
|
||||
value={style.font}
|
||||
onValueChange={(v) => setStyle((s) => ({ ...s, font: v as BumperFont }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Sans">{t('admin.bumpers.fontSans')}</SelectItem>
|
||||
<SelectItem value="Serif">{t('admin.bumpers.fontSerif')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{colorFields.map(({ key, label }) => (
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<Label>{label}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-8 w-8 shrink-0 rounded border border-border"
|
||||
style={{ backgroundColor: cssColor(style[key]) }}
|
||||
/>
|
||||
<Input
|
||||
value={style[key]}
|
||||
onChange={(e) => setStyle((s) => ({ ...s, [key]: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 border-t border-border pt-3 sm:grid-cols-2">
|
||||
<BumperAudioField
|
||||
templateId={template.id}
|
||||
hasAudio={template.hasAudio}
|
||||
onChanged={onChanged}
|
||||
onError={onError}
|
||||
/>
|
||||
<BumperBackgroundField
|
||||
templateId={template.id}
|
||||
backgroundImageId={template.backgroundImageId}
|
||||
onChanged={onChanged}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
|
||||
{t('admin.bumpers.saveStyle')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<p className="text-sm font-medium">{t('admin.bumpers.variants')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.bumpers.variantsHint')}</p>
|
||||
{[...template.variants]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((variant) => (
|
||||
<BumperVariantEditor
|
||||
key={variant.id}
|
||||
template={template}
|
||||
variant={variant}
|
||||
canRemove={template.variants.length > 1}
|
||||
onChanged={onChanged}
|
||||
onError={onError}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={addVariant.isPending}
|
||||
onClick={() => addVariant.mutate()}
|
||||
>
|
||||
{t('admin.bumpers.addVariant')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<BumperPreviewPlayer
|
||||
templateId={template.id}
|
||||
channelId={channelId}
|
||||
variants={template.variants}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user