Implement BumperEndpoints and remove deprecated bumper-related functionality
ci / build-backend (push) Successful in 1m39s
ci / build-frontend (push) Failing after 26s
ci / tests (push) Skipped
ci / sonar (push) Skipped

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:
Leonid Pershin
2026-07-27 22:01:56 +03:00
parent ee0b4d2d01
commit ba3721eb92
140 changed files with 7326 additions and 3609 deletions
@@ -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>
)
}