diff --git a/frontend/src/features/admin/media/UploadSnackbar.tsx b/frontend/src/features/admin/media/UploadSnackbar.tsx index a97a910..d3868c8 100644 --- a/frontend/src/features/admin/media/UploadSnackbar.tsx +++ b/frontend/src/features/admin/media/UploadSnackbar.tsx @@ -6,46 +6,71 @@ import { type UploadItem, useUploadStore } from './upload-store' function StatusIcon({ status }: { status: UploadItem['status'] }) { switch (status) { case 'done': - return + return case 'error': - return + return case 'uploading': - return + return default: - return + return } } +function barWidth(item: UploadItem): number { + if (item.status === 'done' || item.status === 'error') return 100 + if (item.status === 'uploading') return item.percent + return 0 +} + /** Глобальный индикатор загрузок: живёт вне страниц, поэтому загрузка идёт при любой навигации. */ export function UploadSnackbar() { const { t } = useTranslation() - const { items, active, minimized, toggleMinimize, dismiss } = useUploadStore() + const { items, active, minimized, skipped, toggleMinimize, dismiss } = useUploadStore() - if (items.length === 0) return null + if (items.length === 0 && skipped === 0) return null const done = items.filter((i) => i.status === 'done').length - const header = active - ? t('admin.media.uploadingCount', { done, total: items.length }) - : t('admin.media.uploadedCount', { count: done }) + const hasItems = items.length > 0 + const header = hasItems + ? active + ? t('admin.media.uploadingCount', { done, total: items.length }) + : t('admin.media.uploadedCount', { count: done }) + : t('admin.media.skippedDuplicates', { count: skipped }) return ( -
+
{header} - + {hasItems && ( + + )} {!active && ( - )}
- {!minimized && ( -
    + {hasItems && skipped > 0 && ( +
    + {t('admin.media.skippedDuplicates', { count: skipped })} +
    + )} + + {hasItems && !minimized && ( +
      {items.map((item) => ( -
    • +
    • @@ -55,14 +80,15 @@ export function UploadSnackbar() { {item.percent}% )}
      - {item.status === 'uploading' && ( -
      -
      -
      - )} +
      +
      +
    • ))}
    diff --git a/frontend/src/features/admin/media/upload-store.ts b/frontend/src/features/admin/media/upload-store.ts index 518e31f..ffbbfb1 100644 --- a/frontend/src/features/admin/media/upload-store.ts +++ b/frontend/src/features/admin/media/upload-store.ts @@ -1,7 +1,5 @@ import { create } from 'zustand' import { queryClient } from '@/shared/api/query-client' -import i18n from '@/shared/lib/i18n' -import { toast } from '@/shared/ui/toast-store' import { listMedia, uploadMedia } from './api' export type UploadItem = { @@ -15,6 +13,7 @@ type UploadStore = { items: UploadItem[] active: boolean minimized: boolean + skipped: number enqueue: (files: File[]) => Promise toggleMinimize: () => void dismiss: () => void @@ -35,14 +34,12 @@ async function pump() { running = true useUploadStore.setState({ active: true, minimized: false }) - let uploaded = 0 while (queue.length > 0) { const job = queue.shift()! patch(job.id, { status: 'uploading', percent: 0 }) try { await uploadMedia(job.file, (percent) => patch(job.id, { percent })) patch(job.id, { status: 'done', percent: 100 }) - uploaded++ void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] }) } catch { patch(job.id, { status: 'error' }) @@ -51,13 +48,13 @@ async function pump() { running = false useUploadStore.setState({ active: false }) - if (uploaded > 0) toast.success(i18n.t('admin.media.uploadedCount', { count: uploaded })) } export const useUploadStore = create((set) => ({ items: [], active: false, minimized: false, + skipped: 0, enqueue: async (files) => { // Пропускаем дубликаты: уже в библиотеке (не проваленные) и уже в текущей очереди. @@ -82,18 +79,20 @@ export const useUploadStore = create((set) => ({ const toAdd = files.filter((f) => !existing.has(f.name) && !inFlight.has(f.name)) const skipped = files.length - toAdd.length - if (toAdd.length > 0) { - const newItems: UploadItem[] = toAdd.map((file) => { - const id = `u${++counter}` - queue.push({ id, file }) - return { id, name: file.name, percent: 0, status: 'queued' } - }) - set((s) => ({ items: [...s.items, ...newItems], minimized: false })) - void pump() - } - if (skipped > 0) toast.message(i18n.t('admin.media.skippedDuplicates', { count: skipped })) + const newItems: UploadItem[] = toAdd.map((file) => { + const id = `u${++counter}` + queue.push({ id, file }) + return { id, name: file.name, percent: 0, status: 'queued' } + }) + + set((s) => ({ + items: newItems.length ? [...s.items, ...newItems] : s.items, + skipped: s.skipped + skipped, + minimized: newItems.length ? false : s.minimized, + })) + if (newItems.length) void pump() }, toggleMinimize: () => set((s) => ({ minimized: !s.minimized })), - dismiss: () => set({ items: [] }), + dismiss: () => set({ items: [], skipped: 0 }), }))