Refactor media upload handling in MediaPanel: remove direct upload logic and integrate useUploadStore for file enqueueing. Update translations for upload status and add UploadSnackbar component to display upload notifications in the UI.

This commit is contained in:
Leonid Pershin
2026-07-24 22:25:01 +03:00
parent 2523808e3b
commit 8f8ce5122a
7 changed files with 188 additions and 53 deletions
@@ -8,7 +8,8 @@ import { Badge, type BadgeProps } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { toast } from '@/shared/ui/toast-store'
import { deleteMedia, listMedia, uploadMedia } from './api'
import { deleteMedia, listMedia } from './api'
import { useUploadStore } from './upload-store'
const PAGE_SIZE = 20
@@ -42,10 +43,8 @@ export function MediaPanel() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const fileInput = useRef<HTMLInputElement>(null)
const [upload, setUpload] = useState<{ current: number; total: number; percent: number } | null>(
null,
)
const [filter, setFilter] = useState<MediaFilter>('active')
const enqueue = useUploadStore((s) => s.enqueue)
const { data, isLoading } = useQuery({
queryKey: ['admin', 'media', filter],
@@ -63,45 +62,6 @@ export function MediaPanel() {
const deleteMutation = useMutation({ mutationFn: deleteMedia, onSuccess: invalidate, onError })
// Массовая загрузка: файлы отправляются по одному (обработка всё равно в очереди по одному),
// прогресс — «текущий/всего · %». Дубликаты по имени пропускаются заранее (сервер тоже отклонит).
const handleFiles = async (files: FileList) => {
const list = Array.from(files)
let existing = new Set<string>()
try {
const all = await listMedia({
page: 1,
pageSize: 1000,
statuses: ['Pending', 'Processing', 'Ready'],
})
existing = new Set(all.items.map((a) => a.originalFileName))
} catch {
// Не удалось получить список — положимся на серверную проверку дубликатов.
}
const toUpload = list.filter((f) => !existing.has(f.name))
const skipped = list.length - toUpload.length
let uploaded = 0
for (let i = 0; i < toUpload.length; i++) {
setUpload({ current: i + 1, total: toUpload.length, percent: 0 })
try {
await uploadMedia(toUpload[i], (percent) =>
setUpload({ current: i + 1, total: toUpload.length, percent }),
)
uploaded++
invalidate()
} catch (error) {
onError(error)
}
}
setUpload(null)
if (fileInput.current) fileInput.current.value = ''
if (uploaded > 0) toast.success(t('admin.media.uploadedCount', { count: uploaded }))
if (skipped > 0) toast.message(t('admin.media.skippedDuplicates', { count: skipped }))
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-2">
@@ -120,11 +80,6 @@ export function MediaPanel() {
</Select>
</div>
<div className="flex items-center gap-3">
{upload != null && (
<span className="text-sm text-muted-foreground">
{upload.current}/{upload.total} · {upload.percent}%
</span>
)}
<input
ref={fileInput}
type="file"
@@ -133,10 +88,11 @@ export function MediaPanel() {
className="hidden"
onChange={(e) => {
const files = e.target.files
if (files && files.length > 0) void handleFiles(files)
if (files && files.length > 0) void enqueue(Array.from(files))
e.target.value = ''
}}
/>
<Button size="sm" disabled={upload != null} onClick={() => fileInput.current?.click()}>
<Button size="sm" onClick={() => fileInput.current?.click()}>
<Upload className="h-4 w-4" />
{t('admin.media.upload')}
</Button>