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 && ( Пока нет загруженных изображений. )} ) }