d18546c45a
chore(server): remove unused gallery.js
191 lines
7.5 KiB
TypeScript
191 lines
7.5 KiB
TypeScript
import { useState } from 'react'
|
||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'
|
||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'
|
||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'
|
||
import Box from '@mui/material/Box'
|
||
import Button from '@mui/material/Button'
|
||
import Dialog from '@mui/material/Dialog'
|
||
import DialogActions from '@mui/material/DialogActions'
|
||
import DialogContent from '@mui/material/DialogContent'
|
||
import DialogTitle from '@mui/material/DialogTitle'
|
||
import IconButton from '@mui/material/IconButton'
|
||
import Paper from '@mui/material/Paper'
|
||
import Stack from '@mui/material/Stack'
|
||
import TextField from '@mui/material/TextField'
|
||
import Typography from '@mui/material/Typography'
|
||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||
import { putAdminCatalogSlider } from '@/entities/catalog-slider'
|
||
import type { GalleryImageItem } from '@/entities/gallery'
|
||
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
||
|
||
export type SlideDraft = { galleryImageId: string; caption: string }
|
||
|
||
type Props = {
|
||
initialSlides: SlideDraft[]
|
||
galleryItems: GalleryImageItem[]
|
||
}
|
||
|
||
export function GallerySliderSection({ initialSlides, galleryItems }: Props) {
|
||
const queryClient = useQueryClient()
|
||
const [sliderDraft, setSliderDraft] = useState<SlideDraft[]>(initialSlides)
|
||
const [pickOpen, setPickOpen] = useState(false)
|
||
|
||
const usedIds = new Set(sliderDraft.map((s) => s.galleryImageId))
|
||
const pickCandidates = galleryItems.filter((i) => !usedIds.has(i.id) && i.isResized)
|
||
|
||
const saveSliderMut = useMutation({
|
||
mutationFn: () => putAdminCatalogSlider({ slides: sliderDraft }),
|
||
onSuccess: async () => {
|
||
await invalidateQueryKeys(queryClient, [['admin', 'catalog-slider'], ['catalog-slider']])
|
||
},
|
||
})
|
||
|
||
const moveSlide = (idx: number, dir: -1 | 1) => {
|
||
const next = idx + dir
|
||
if (next < 0 || next >= sliderDraft.length) return
|
||
setSliderDraft((prev) => {
|
||
const copy = [...prev]
|
||
const t = copy[idx]!
|
||
copy[idx] = copy[next]!
|
||
copy[next] = t
|
||
return copy
|
||
})
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Paper variant="outlined" sx={{ p: 2, mb: 3, borderRadius: 2 }}>
|
||
<Typography variant="h6" gutterBottom>
|
||
Слайдер главной (каталог)
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||
Сначала загрузите фото в галерею ниже, затем добавьте слайды, укажите подписи и сохраните. Порядок строк =
|
||
порядок показа на витрине.
|
||
</Typography>
|
||
|
||
<Stack spacing={1.5} sx={{ mb: 2 }}>
|
||
{sliderDraft.map((row, idx) => {
|
||
const img = galleryItems.find((g) => g.id === row.galleryImageId)
|
||
return (
|
||
<Stack
|
||
key={`${row.galleryImageId}-${idx}`}
|
||
direction={{ xs: 'column', sm: 'row' }}
|
||
spacing={1.5}
|
||
sx={{ alignItems: { sm: 'flex-start' } }}
|
||
>
|
||
<Box
|
||
sx={{
|
||
width: 100,
|
||
height: 100,
|
||
flexShrink: 0,
|
||
borderRadius: 1,
|
||
overflow: 'hidden',
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
}}
|
||
>
|
||
<Box
|
||
component="img"
|
||
src={img?.url ?? ''}
|
||
alt=""
|
||
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||
/>
|
||
</Box>
|
||
<TextField
|
||
label="Подпись на слайде"
|
||
fullWidth
|
||
multiline
|
||
minRows={2}
|
||
value={row.caption}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
setSliderDraft((prev) => {
|
||
const copy = [...prev]
|
||
copy[idx] = { ...copy[idx]!, caption: v }
|
||
return copy
|
||
})
|
||
}}
|
||
/>
|
||
<Stack direction="row" spacing={0.5}>
|
||
<IconButton size="small" aria-label="Выше" onClick={() => moveSlide(idx, -1)} disabled={idx === 0}>
|
||
<ArrowUpwardIcon fontSize="small" />
|
||
</IconButton>
|
||
<IconButton
|
||
size="small"
|
||
aria-label="Ниже"
|
||
onClick={() => moveSlide(idx, 1)}
|
||
disabled={idx >= sliderDraft.length - 1}
|
||
>
|
||
<ArrowDownwardIcon fontSize="small" />
|
||
</IconButton>
|
||
<IconButton
|
||
size="small"
|
||
color="error"
|
||
aria-label="Убрать из слайдера"
|
||
onClick={() => setSliderDraft((prev) => prev.filter((_, i) => i !== idx))}
|
||
>
|
||
<DeleteOutlineOutlinedIcon fontSize="small" />
|
||
</IconButton>
|
||
</Stack>
|
||
</Stack>
|
||
)
|
||
})}
|
||
</Stack>
|
||
|
||
<Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Button variant="outlined" disabled={pickCandidates.length === 0} onClick={() => setPickOpen(true)}>
|
||
Добавить слайд из галереи
|
||
</Button>
|
||
<Button variant="contained" disabled={saveSliderMut.isPending} onClick={() => saveSliderMut.mutate()}>
|
||
Сохранить слайдер
|
||
</Button>
|
||
{saveSliderMut.isError && (
|
||
<Typography color="error">
|
||
{saveSliderMut.error instanceof Error ? saveSliderMut.error.message : 'Ошибка сохранения'}
|
||
</Typography>
|
||
)}
|
||
</Stack>
|
||
</Paper>
|
||
|
||
<Dialog open={pickOpen} onClose={() => setPickOpen(false)} fullWidth maxWidth="sm">
|
||
<DialogTitle>Выберите изображение</DialogTitle>
|
||
<DialogContent dividers>
|
||
{pickCandidates.length === 0 ? (
|
||
<Typography color="text.secondary">Нет доступных файлов (все уже в слайдере или галерея пуста).</Typography>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
|
||
gap: 1.5,
|
||
pt: 1,
|
||
}}
|
||
>
|
||
{pickCandidates.map((item) => (
|
||
<Button
|
||
key={item.id}
|
||
sx={{ p: 0, minWidth: 0, display: 'block', borderRadius: 1, overflow: 'hidden' }}
|
||
onClick={() => {
|
||
setSliderDraft((prev) => [...prev, { galleryImageId: item.id, caption: '' }])
|
||
setPickOpen(false)
|
||
}}
|
||
>
|
||
<Box
|
||
component="img"
|
||
src={item.url}
|
||
alt=""
|
||
sx={{ width: '100%', aspectRatio: '1', objectFit: 'cover', display: 'block' }}
|
||
/>
|
||
</Button>
|
||
))}
|
||
</Box>
|
||
)}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setPickOpen(false)}>Закрыть</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
</>
|
||
)
|
||
}
|