import { useRef, useState } from 'react'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import ToggleButton from '@mui/material/ToggleButton'
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
deleteGalleryImage,
fetchAdminGallery,
GalleryGrid,
resizeGalleryImage,
uploadGalleryImages,
} from '@/entities/gallery'
import type { GalleryImageItem } from '@/entities/gallery'
import { formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
import type { AxiosError } from 'axios'
import { Grid3x3, List } from 'lucide-react'
function getApiErrorMessage(error: unknown): string | null {
const e = error as AxiosError<{ error?: string }>
const msg = e?.response?.data?.error
return msg ? String(msg) : null
}
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' })
} catch {
return ''
}
}
function fileNameFromUrl(url: string): string {
const parts = url.split('/')
return parts[parts.length - 1] || url
}
function GalleryTable({
items,
deleting,
resizing,
onDelete,
onResize,
}: {
items: GalleryImageItem[]
deleting: boolean
resizing: string | null
onDelete: (id: string) => void
onResize: (id: string) => void
}) {
return (
Миниатюра
Имя файла
Статус
Дата
Действия
{items.map((item) => (
{fileNameFromUrl(item.url)}
{formatDate(item.createdAt)}
{!item.isResized && (
)}
))}
)
}
export function AdminGalleryPage() {
const queryClient = useQueryClient()
const fileInputRef = useRef(null)
const [resizingId, setResizingId] = useState(null)
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid')
const galleryQuery = useQuery({
queryKey: ['admin', 'gallery'],
queryFn: fetchAdminGallery,
})
const uploadMut = useMutation({
mutationFn: (files: File[]) => uploadGalleryImages(files),
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'gallery']])
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
},
})
const deleteMut = useMutation({
mutationFn: (id: string) => deleteGalleryImage(id),
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'gallery'], ['admin', 'catalog-slider'], ['catalog-slider']])
},
})
const resizeMut = useMutation({
mutationFn: async (id: string) => {
setResizingId(id)
try {
return await resizeGalleryImage(id)
} finally {
setResizingId(null)
}
},
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'gallery'], ['admin', 'catalog-slider'], ['catalog-slider']])
},
})
const items = galleryQuery.data?.items ?? []
return (
Галерея
Изображения загружаются без обработки. После загрузки нажмите «Resize» для подготовки к публикации. Обработанные
изображения доступны для добавления в карточку товара и слайдер.
Форматы: PNG, JPEG, WebP. На один файл — до {formatAdminImageMaxSizeHint()}.
v && setViewMode(v)}
size="small"
>
{uploadMut.isPending && Загрузка…}
{uploadMut.isError && (
{uploadMut.error instanceof Error ? uploadMut.error.message : 'Ошибка загрузки'}
)}
{deleteMut.isError && (
{getApiErrorMessage(deleteMut.error) ?? 'Ошибка удаления'}
)}
{resizeMut.isError && (
{getApiErrorMessage(resizeMut.error) ?? 'Ошибка обработки'}
)}
{galleryQuery.isError && (
Не удалось загрузить список.
)}
{viewMode === 'grid' ? (
deleteMut.mutate(id)}
onResize={(id) => resizeMut.mutate(id)}
/>
) : (
deleteMut.mutate(id)}
onResize={(id) => resizeMut.mutate(id)}
/>
)}
{!galleryQuery.isLoading && items.length === 0 && (
Пока нет загруженных изображений.
)}
)
}