ыввы
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Divider from '@mui/material/Divider'
|
||||
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 { fetchAdminCatalogSlider } from '@/entities/catalog-slider'
|
||||
import {
|
||||
deleteGalleryImage,
|
||||
fetchAdminGallery,
|
||||
@@ -13,10 +20,11 @@ import {
|
||||
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 { GallerySliderSection } from './GallerySliderSection'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { Grid3x3, List } from 'lucide-react'
|
||||
|
||||
function getApiErrorMessage(error: unknown): string | null {
|
||||
const e = error as AxiosError<{ error?: string }>
|
||||
@@ -24,15 +32,102 @@ function getApiErrorMessage(error: unknown): string | null {
|
||||
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 (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Миниатюра</TableCell>
|
||||
<TableCell>Имя файла</TableCell>
|
||||
<TableCell>Статус</TableCell>
|
||||
<TableCell>Дата</TableCell>
|
||||
<TableCell>Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<Box
|
||||
component="img"
|
||||
src={item.url}
|
||||
alt=""
|
||||
sx={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 1, display: 'block' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{fileNameFromUrl(item.url)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={item.isResized ? 'Готово' : 'Не обработано'}
|
||||
size="small"
|
||||
color={item.isResized ? 'success' : 'warning'}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(item.createdAt)}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={0.5}>
|
||||
{!item.isResized && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
disabled={resizing === item.id}
|
||||
onClick={() => onResize(item.id)}
|
||||
>
|
||||
Resize
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
disabled={deleting}
|
||||
onClick={() => onDelete(item.id)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export function AdminGalleryPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [resizingId, setResizingId] = useState<string | null>(null)
|
||||
|
||||
const sliderQuery = useQuery({
|
||||
queryKey: ['admin', 'catalog-slider'],
|
||||
queryFn: fetchAdminCatalogSlider,
|
||||
})
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid')
|
||||
|
||||
const galleryQuery = useQuery({
|
||||
queryKey: ['admin', 'gallery'],
|
||||
@@ -82,29 +177,6 @@ export function AdminGalleryPage() {
|
||||
изображения доступны для добавления в карточку товара и слайдер.
|
||||
</Typography>
|
||||
|
||||
{sliderQuery.isError && (
|
||||
<Typography color="error" sx={{ mb: 2 }}>
|
||||
Не удалось загрузить настройки слайдера.
|
||||
</Typography>
|
||||
)}
|
||||
{sliderQuery.isLoading && (
|
||||
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
||||
Загрузка настроек слайдера…
|
||||
</Typography>
|
||||
)}
|
||||
{sliderQuery.isSuccess && (
|
||||
<GallerySliderSection
|
||||
key={sliderQuery.dataUpdatedAt}
|
||||
initialSlides={sliderQuery.data.slides.map((s) => ({
|
||||
galleryImageId: s.galleryImageId,
|
||||
caption: s.caption,
|
||||
}))}
|
||||
galleryItems={items}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Форматы: PNG, JPEG, WebP. На один файл — до {formatAdminImageMaxSizeHint()}.
|
||||
</Typography>
|
||||
@@ -125,6 +197,19 @@ export function AdminGalleryPage() {
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<ToggleButtonGroup
|
||||
value={viewMode}
|
||||
exclusive
|
||||
onChange={(_, v) => v && setViewMode(v)}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="grid" aria-label="Сетка">
|
||||
<Grid3x3 size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="table" aria-label="Таблица">
|
||||
<List size={16} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
{uploadMut.isPending && <Typography color="text.secondary">Загрузка…</Typography>}
|
||||
{uploadMut.isError && (
|
||||
<Typography color="error">
|
||||
@@ -145,13 +230,23 @@ export function AdminGalleryPage() {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<GalleryGrid
|
||||
items={items}
|
||||
deleting={deleteMut.isPending}
|
||||
resizing={resizingId}
|
||||
onDelete={(id) => deleteMut.mutate(id)}
|
||||
onResize={(id) => resizeMut.mutate(id)}
|
||||
/>
|
||||
{viewMode === 'grid' ? (
|
||||
<GalleryGrid
|
||||
items={items}
|
||||
deleting={deleteMut.isPending}
|
||||
resizing={resizingId}
|
||||
onDelete={(id) => deleteMut.mutate(id)}
|
||||
onResize={(id) => resizeMut.mutate(id)}
|
||||
/>
|
||||
) : (
|
||||
<GalleryTable
|
||||
items={items}
|
||||
deleting={deleteMut.isPending}
|
||||
resizing={resizingId}
|
||||
onDelete={(id) => deleteMut.mutate(id)}
|
||||
onResize={(id) => resizeMut.mutate(id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!galleryQuery.isLoading && items.length === 0 && (
|
||||
<Typography color="text.secondary">Пока нет загруженных изображений.</Typography>
|
||||
|
||||
Reference in New Issue
Block a user