Files
TeleWave/frontend/src/features/admin/bumpers/components/BumperTemplateEditor.tsx
T
Leonid Pershin ba3721eb92
ci / build-backend (push) Successful in 1m39s
ci / build-frontend (push) Failing after 26s
ci / tests (push) Skipped
ci / sonar (push) Skipped
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.
2026-07-27 22:01:56 +03:00

221 lines
8.1 KiB
TypeScript

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>
)
}