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:
@@ -0,0 +1,72 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AlertCircle, Check, ChevronDown, ChevronUp, Clock, Loader2, X } from 'lucide-react'
|
||||
import { cn } from '@/shared/lib/cn'
|
||||
import { type UploadItem, useUploadStore } from './upload-store'
|
||||
|
||||
function StatusIcon({ status }: { status: UploadItem['status'] }) {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return <Check className="h-3.5 w-3.5 text-primary" />
|
||||
case 'error':
|
||||
return <AlertCircle className="h-3.5 w-3.5 text-red-500" />
|
||||
case 'uploading':
|
||||
return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
default:
|
||||
return <Clock className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
}
|
||||
}
|
||||
|
||||
/** Глобальный индикатор загрузок: живёт вне страниц, поэтому загрузка идёт при любой навигации. */
|
||||
export function UploadSnackbar() {
|
||||
const { t } = useTranslation()
|
||||
const { items, active, minimized, toggleMinimize, dismiss } = useUploadStore()
|
||||
|
||||
if (items.length === 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 })
|
||||
|
||||
return (
|
||||
<div className="crt-panel fixed bottom-4 right-4 z-50 w-80 max-w-[calc(100vw-2rem)] rounded-md shadow-lg">
|
||||
<div className="flex items-center gap-2 border-b border-border px-3 py-2 text-sm">
|
||||
<span className="truncate font-medium">{header}</span>
|
||||
<button type="button" className="ml-auto opacity-70 hover:opacity-100" onClick={toggleMinimize}>
|
||||
{minimized ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
{!active && (
|
||||
<button type="button" className="opacity-70 hover:opacity-100" onClick={dismiss}>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!minimized && (
|
||||
<ul className="max-h-64 divide-y divide-border overflow-y-auto">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="flex flex-col gap-1 px-3 py-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status={item.status} />
|
||||
<span className="truncate" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
{item.status === 'uploading' && (
|
||||
<span className="ml-auto shrink-0 text-muted-foreground">{item.percent}%</span>
|
||||
)}
|
||||
</div>
|
||||
{item.status === 'uploading' && (
|
||||
<div className="h-1 w-full overflow-hidden rounded bg-muted">
|
||||
<div
|
||||
className={cn('h-full rounded bg-primary transition-[width]')}
|
||||
style={{ width: `${item.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user