146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import { useMutation } from '@tanstack/react-query'
|
|
import { GripVertical, Trash2 } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { addCollectionShow, createCollection } from '@/features/admin/collections/api'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { Input } from '@/shared/ui/input'
|
|
import { cn } from '@/shared/lib/cn'
|
|
import { type ClipDragItem, readDragItem } from './dnd'
|
|
import { formatClock } from './format'
|
|
|
|
/**
|
|
* Сборка рекламного блока: ролики перетаскиваются в упорядоченный список, под ним — суммарная
|
|
* длительность. Сохраняется обычной коллекцией — отдельной сущности «блок» в модели нет (см. 3.7).
|
|
*/
|
|
export function BlockBuilder({
|
|
onSaved,
|
|
onError,
|
|
}: {
|
|
onSaved: () => void
|
|
onError: (error: unknown) => void
|
|
}) {
|
|
const { t } = useTranslation()
|
|
const [name, setName] = useState('')
|
|
const [items, setItems] = useState<ClipDragItem[]>([])
|
|
const [over, setOver] = useState(false)
|
|
const [dragged, setDragged] = useState<number | null>(null)
|
|
|
|
const total = items.reduce((sum, item) => sum + item.seconds, 0)
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const { id } = await createCollection({ name: name.trim() })
|
|
// Порядок задаётся порядком добавления: коллекция ставит позицию в конец.
|
|
for (const item of items) await addCollectionShow(id, item.id)
|
|
},
|
|
onSuccess: () => {
|
|
setName('')
|
|
setItems([])
|
|
onSaved()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
const drop = (event: React.DragEvent) => {
|
|
event.preventDefault()
|
|
setOver(false)
|
|
const item = readDragItem(event)
|
|
// Блок из блоков собрать нельзя: коллекция хранит шоу, а не вложенные коллекции.
|
|
if (!item || item.kind !== 'clip') return
|
|
setItems((current) => [...current, item])
|
|
}
|
|
|
|
/** Перестановка внутри черновика — до сохранения всё живёт в состоянии, запросов нет. */
|
|
const reorder = (target: number) => {
|
|
if (dragged === null || dragged === target) return
|
|
setItems((current) => {
|
|
const next = [...current]
|
|
const [moved] = next.splice(dragged, 1)
|
|
next.splice(target, 0, moved)
|
|
return next
|
|
})
|
|
setDragged(null)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{t('admin.interstitials.blockBuilder')}
|
|
</h3>
|
|
|
|
<div
|
|
onDragOver={(e) => {
|
|
e.preventDefault()
|
|
setOver(true)
|
|
}}
|
|
onDragLeave={() => setOver(false)}
|
|
onDrop={drop}
|
|
className={cn(
|
|
'crt-panel flex min-h-32 flex-col rounded-md border border-dashed border-border',
|
|
over && 'border-primary bg-primary/5',
|
|
)}
|
|
>
|
|
{items.length === 0 ? (
|
|
<p className="px-3 py-6 text-center text-xs text-muted-foreground">
|
|
{t('admin.interstitials.dropHint')}
|
|
</p>
|
|
) : (
|
|
<ul className="divide-y divide-border text-sm">
|
|
{items.map((item, index) => (
|
|
<li
|
|
key={`${item.id}-${index}`}
|
|
draggable
|
|
onDragStart={() => setDragged(index)}
|
|
onDragOver={(e) => e.preventDefault()}
|
|
onDrop={(e) => {
|
|
e.stopPropagation()
|
|
reorder(index)
|
|
}}
|
|
className="flex items-center gap-2 px-3 py-1.5"
|
|
>
|
|
<GripVertical className="h-4 w-4 shrink-0 cursor-grab text-muted-foreground" />
|
|
<span className="w-5 shrink-0 text-muted-foreground">{index + 1}</span>
|
|
<span className="min-w-0 flex-1 truncate" title={item.name}>
|
|
{item.name}
|
|
</span>
|
|
<span className="shrink-0 tabular-nums text-muted-foreground">
|
|
{formatClock(item.seconds)}
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setItems((c) => c.filter((_, i) => i !== index))}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>{t('admin.interstitials.blockTotal')}</span>
|
|
<span className="tabular-nums">{formatClock(total)}</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder={t('admin.interstitials.blockName')}
|
|
value={name}
|
|
maxLength={256}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
disabled={!name.trim() || items.length === 0 || saveMutation.isPending}
|
|
onClick={() => saveMutation.mutate()}
|
|
>
|
|
{t('common.save')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|