import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Trash2, Upload } from 'lucide-react' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { qk } from '@/shared/api/query-keys' import type { ImageCategory } from '@/shared/api/types' import { useApiError } from '@/shared/lib/use-api-error' import { Button } from '@/shared/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' import { cn } from '@/shared/lib/cn' import { deleteImage, imageUrl, listImages, uploadImage } from './api' const CATEGORIES: ImageCategory[] = ['Library', 'ShowPoster', 'EpisodeStill', 'BumperBackground'] type ImageOrder = 'new' | 'old' | 'az' | 'za' type ImagePick = { id: string; url: string } /** * Внутренность галереи: вкладки по категориям, загрузка и удаление. Если передан onSelect — * работает как пикер (клик по картинке или загрузка новой возвращает её и закрывает через onClose). * Новые загрузки идут в активную вкладку (по умолчанию — категорию вызова). */ export function GalleryBrowser({ category = 'Library', onSelect, onClose, fill = false, }: Readonly<{ category?: ImageCategory onSelect?: (image: ImagePick) => void onClose?: () => void /** Занять экран целиком: на отдельной странице прокрутка должна быть одна — внутри сетки. */ fill?: boolean }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [active, setActive] = useState(category) const fileInput = useRef(null) const onError = useApiError() const [order, setOrder] = useState('new') const { data: images, isLoading } = useQuery({ queryKey: qk.images.byCategory(active), queryFn: () => listImages(active), }) const sorted = useMemo(() => { const arr = [...(images ?? [])] arr.sort((a, b) => { switch (order) { case 'old': return a.createdAt.localeCompare(b.createdAt) case 'az': return (a.originalFileName ?? '').localeCompare(b.originalFileName ?? '') case 'za': return (b.originalFileName ?? '').localeCompare(a.originalFileName ?? '') default: return b.createdAt.localeCompare(a.createdAt) } }) return arr }, [images, order]) const invalidate = () => queryClient.invalidateQueries({ queryKey: qk.images.byCategory(active) }) const pick = (id: string) => { if (!onSelect) return onSelect({ id, url: imageUrl(id) }) onClose?.() } const upload = useMutation({ mutationFn: (file: File) => uploadImage(active, file), onSuccess: (created) => { invalidate() // В режиме пикера загрузка = «загрузить и выбрать»: возвращаем новую картинку и закрываем. if (onSelect) pick(created.id) }, onError, }) const remove = useMutation({ mutationFn: (id: string) => deleteImage(id), onSuccess: invalidate, onError, }) return ( {CATEGORIES.map((c) => ( setActive(c)} className={`rounded-md px-3 py-1.5 text-sm ${ active === c ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted' }`} > {t(`admin.gallery.categories.${c}`)} ))} {onSelect ? t('admin.gallery.pickHint') : t('admin.gallery.browseHint')} setOrder(v as ImageOrder)}> {t('admin.gallery.sort.newest')} {t('admin.gallery.sort.oldest')} {t('admin.gallery.sort.nameAsc')} {t('admin.gallery.sort.nameDesc')} { const file = e.target.files?.[0] if (file) upload.mutate(file) e.target.value = '' }} /> fileInput.current?.click()} > {t('admin.gallery.upload')} {isLoading && ( {t('common.loading')} )} {!isLoading && sorted.length === 0 && ( {t('admin.gallery.empty')} )} {!isLoading && sorted.length > 0 && ( {sorted.map((img) => ( pick(img.id)} className={`block aspect-square w-full overflow-hidden rounded-md border border-border bg-muted/30 ${ onSelect ? 'cursor-pointer hover:border-primary' : 'cursor-default' }`} title={img.originalFileName ?? ''} > remove.mutate(img.id)} aria-label={t('common.delete')} className="absolute right-1 top-1 rounded bg-black/60 p-1 text-white opacity-0 transition-opacity hover:bg-red-600 group-hover:opacity-100" > ))} )} ) } /** Общая галерея изображений в модальном окне (используется как пикер в местах выбора картинки). */ export function ImageGallery({ open, onOpenChange, category, onSelect, }: Readonly<{ open: boolean onOpenChange: (open: boolean) => void category?: ImageCategory onSelect?: (image: ImagePick) => void }>) { const { t } = useTranslation() return ( {t('admin.gallery.title')} {open && ( onOpenChange(false)} /> )} ) }
{t('common.loading')}
{t('admin.gallery.empty')}