Add bumper variant management: implement API endpoints for adding, updating, and removing bumper text variants, enhance data models to support variant details, and update scheduling logic to utilize variants. Refactor related components for improved bumper template handling and ensure proper error management for variant operations.
This commit is contained in:
@@ -14,6 +14,9 @@ import type {
|
||||
BumperSelection,
|
||||
BumperSettings,
|
||||
BumperTemplateDto,
|
||||
BumperTextKind,
|
||||
BumperTextVariantDto,
|
||||
BumperTrigger,
|
||||
ChannelShowDto,
|
||||
OverrideMode,
|
||||
ScheduleEntryDto,
|
||||
@@ -29,6 +32,7 @@ import { listMedia } from '@/features/admin/media/api'
|
||||
import { listShows } from '@/features/admin/shows/api'
|
||||
import {
|
||||
addBumperTemplate,
|
||||
addBumperVariant,
|
||||
addChannelAd,
|
||||
addChannelShow,
|
||||
bumperPreviewPlaylistUrl,
|
||||
@@ -40,11 +44,13 @@ import {
|
||||
getSchedule,
|
||||
regenerateSchedule,
|
||||
removeBumperTemplate,
|
||||
removeBumperVariant,
|
||||
removeChannelAd,
|
||||
removeChannelShow,
|
||||
renderBumperPreview,
|
||||
setBumperTemplateBackground,
|
||||
updateBumperTemplate,
|
||||
updateBumperVariant,
|
||||
updateChannelSettings,
|
||||
updateChannelShow,
|
||||
uploadBumperTemplateAudio,
|
||||
@@ -483,31 +489,7 @@ function BumperCard({
|
||||
onChange={(e) => setField('minIntervalMinutes', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperNowLabel')}</Label>
|
||||
<Input
|
||||
value={bumper.nowLabel}
|
||||
maxLength={64}
|
||||
onChange={(e) => setField('nowLabel', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperNextLabel')}</Label>
|
||||
<Input
|
||||
value={bumper.nextLabel}
|
||||
maxLength={64}
|
||||
onChange={(e) => setField('nextLabel', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={bumper.onlyBetweenDifferentShows}
|
||||
onChange={(e) => setField('onlyBetweenDifferentShows', e.target.checked)}
|
||||
/>
|
||||
{t('admin.channels.bumperOnlyDifferent')}
|
||||
</label>
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
|
||||
{t('common.save')}
|
||||
@@ -589,6 +571,11 @@ function BumperTemplateEditor({
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
const addVariant = useMutation({
|
||||
mutationFn: () => addBumperVariant(channelId, template.id, ''),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
|
||||
const colorFields: { key: keyof typeof colors; label: string }[] = [
|
||||
{ key: 'backgroundColor', label: t('admin.channels.bumperBg') },
|
||||
@@ -677,6 +664,35 @@ function BumperTemplateEditor({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Подблоки (текст-варианты) */}
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<p className="text-sm font-medium">{t('admin.channels.bumperVariants')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.channels.bumperVariantsHint')}</p>
|
||||
{[...template.variants]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((variant) => (
|
||||
<BumperVariantEditor
|
||||
key={variant.id}
|
||||
channelId={channelId}
|
||||
templateId={template.id}
|
||||
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.channels.bumperAddVariant')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<BumperPreviewPlayer channelId={channelId} templateId={template.id} onError={onError} />
|
||||
</div>
|
||||
@@ -692,6 +708,166 @@ function BumperTemplateEditor({
|
||||
)
|
||||
}
|
||||
|
||||
function BumperVariantEditor({
|
||||
channelId,
|
||||
templateId,
|
||||
variant,
|
||||
canRemove,
|
||||
onChanged,
|
||||
onError,
|
||||
}: {
|
||||
channelId: string
|
||||
templateId: string
|
||||
variant: BumperTextVariantDto
|
||||
canRemove: boolean
|
||||
onChanged: () => void
|
||||
onError: (e: unknown) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [form, setForm] = useState({
|
||||
name: variant.name,
|
||||
kind: variant.kind,
|
||||
nowLabel: variant.nowLabel,
|
||||
nextLabel: variant.nextLabel,
|
||||
line1: variant.line1,
|
||||
line2: variant.line2,
|
||||
trigger: variant.trigger,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setForm({
|
||||
name: variant.name,
|
||||
kind: variant.kind,
|
||||
nowLabel: variant.nowLabel,
|
||||
nextLabel: variant.nextLabel,
|
||||
line1: variant.line1,
|
||||
line2: variant.line2,
|
||||
trigger: variant.trigger,
|
||||
})
|
||||
}, [variant])
|
||||
|
||||
const set = <K extends keyof typeof form>(key: K, value: (typeof form)[K]) =>
|
||||
setForm((f) => ({ ...f, [key]: value }))
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () =>
|
||||
updateBumperVariant(channelId, templateId, variant.id, { ...form, name: form.name.trim() }),
|
||||
onSuccess: () => {
|
||||
toast.success(t('settings.saved'))
|
||||
onChanged()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
const remove = useMutation({
|
||||
mutationFn: () => removeBumperVariant(channelId, templateId, variant.id),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-md border border-border bg-background/40 p-3">
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperVariantName')}</Label>
|
||||
<Input value={form.name} maxLength={64} onChange={(e) => set('name', e.target.value)} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperTextKind')}</Label>
|
||||
<Select value={form.kind} onValueChange={(v) => set('kind', v as BumperTextKind)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="NowNext">{t('admin.channels.bumperKindNowNext')}</SelectItem>
|
||||
<SelectItem value="Free">{t('admin.channels.bumperKindFree')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperTrigger')}</Label>
|
||||
<Select value={form.trigger} onValueChange={(v) => set('trigger', v as BumperTrigger)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="OnShowChange">
|
||||
{t('admin.channels.bumperTriggerOnShowChange')}
|
||||
</SelectItem>
|
||||
<SelectItem value="BetweenEpisodes">
|
||||
{t('admin.channels.bumperTriggerBetweenEpisodes')}
|
||||
</SelectItem>
|
||||
<SelectItem value="Both">{t('admin.channels.bumperTriggerBoth')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{form.kind === 'NowNext' ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperNowLabel')}</Label>
|
||||
<Input
|
||||
value={form.nowLabel}
|
||||
maxLength={64}
|
||||
onChange={(e) => set('nowLabel', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperNextLabel')}</Label>
|
||||
<Input
|
||||
value={form.nextLabel}
|
||||
maxLength={64}
|
||||
onChange={(e) => set('nextLabel', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperLine1')}</Label>
|
||||
<Input
|
||||
value={form.line1}
|
||||
maxLength={120}
|
||||
onChange={(e) => set('line1', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.channels.bumperLine2')}</Label>
|
||||
<Input
|
||||
value={form.line2}
|
||||
maxLength={120}
|
||||
onChange={(e) => set('line2', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{canRemove && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={remove.isPending}
|
||||
onClick={() => remove.mutate()}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={save.isPending || !form.name.trim()}
|
||||
onClick={() => save.mutate()}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BumperPreviewPlayer({
|
||||
channelId,
|
||||
templateId,
|
||||
|
||||
Reference in New Issue
Block a user