import React from 'react'; import { toast } from 'react-toastify'; import uniqid from 'uniqid'; import { useQuery } from 'urql'; import { get } from '../../lib/util/get.js'; import './ImageUploader.scss'; import Spinner from '@components/admin/Spinner.js'; import { ImageUploaderSkeleton } from './ImageUploaderSkeleton.js'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; export interface Image { uuid: string; url: string; path?: string; } const Upload: React.FC<{ imageUploadUrl: string; targetPath?: string; onUpload: (images: Image[]) => void | Promise; isSingleMode?: boolean; }> = ({ imageUploadUrl, targetPath, onUpload, isSingleMode }) => { const [uploading, setUploading] = React.useState(false); const onChange = (e) => { setUploading(true); e.persist(); const formData = new FormData(); for (let i = 0; i < e.target.files.length; i += 1) { formData.append('images', e.target.files[i]); } formData.append('targetPath', targetPath || ''); fetch(imageUploadUrl + (targetPath || ''), { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then((response) => { const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new TypeError('Something wrong. Please try again'); } return response.json(); }) .then(async (response) => { if (!response.error) { await onUpload( get(response, 'data.files', []).map((i) => ({ uuid: uniqid(), url: i.url, path: i.path })) ); } else { toast.error(get(response, 'error.message', 'Failed!')); } }) .catch((error) => { toast.error(error.message); }) .finally(() => { e.target.value = null; setUploading(false); }); }; const id = uniqid(); return (
); }; const Image: React.FC<{ image: Image; allowDelete?: boolean; onDelete: (image) => void | Promise; isFirst?: boolean; isSingleMode?: boolean; }> = ({ image, allowDelete, onDelete, isFirst, isSingleMode }) => { const [deleting, setDeleting] = React.useState(false); // Use ref to track if component is mounted const isMounted = React.useRef(true); // Set up effect for cleanup React.useEffect(() => { return () => { // When component unmounts, set ref to false isMounted.current = false; }; }, []); // Assign classes based on mode const classes = isSingleMode ? 'image border border-border rounded-lg' : `image border border-border rounded-lg grid-item ${ isFirst ? 'first-item' : '' }`; return (
{allowDelete && ( { setDeleting(true); await onDelete(image); // Only update state if component is still mounted if (isMounted.current) { setDeleting(false); } }} onKeyDown={() => {}} > )} {deleting && (
)}
); }; const SortableImage: React.FC<{ image: Image; allowDelete?: boolean; onDelete: (image) => void | Promise; isFirst?: boolean; }> = (props) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.image.uuid }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
); }; const GetUploadApiQuery = ` query Query ($filters: [FilterInput!]) { imageUploadUrl: url(routeId: "imageUpload", params: [{key: "0", value: ""}]) } `; export interface ImageUploaderProps { currentImages?: Array; isMultiple?: boolean; allowDelete?: boolean; onDelete?: (image: Image) => void | Promise; onUpload?: (images: Image[]) => void | Promise; targetPath?: string; allowSwap?: boolean; onSortEnd?: (oldIndex: number, newIndex: number) => void; } interface ImagesProps extends ImageUploaderProps { addImage: (imageArray: Image[]) => void; imageUploadUrl: string; onDelete: (image: Image) => void | Promise; onUpload: (images: Image[]) => void | Promise; targetPath?: string; onSortEnd?: (oldIndex: number, newIndex: number) => void; } const Images: React.FC = ({ allowDelete = true, currentImages, imageUploadUrl, onDelete, onUpload, targetPath, isMultiple, allowSwap, onSortEnd }) => { const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id && onSortEnd && currentImages) { const oldIndex = currentImages.findIndex((img) => img.uuid === active.id); const newIndex = currentImages.findIndex((img) => img.uuid === over?.id); if (oldIndex !== -1 && newIndex !== -1) { onSortEnd(oldIndex, newIndex); } } }; if (!isMultiple) { const hasImage = currentImages && currentImages.length > 0; return (
{hasImage ? ( ) : null}
); } else if (allowSwap && currentImages && currentImages.length > 1) { return ( img.uuid)}> {currentImages.map((image, index) => ( ))} ); } // Multi-image mode without drag and drop return ( <> {(currentImages || []).map((image, index) => ( ))} ); }; export function ImageUploader({ currentImages = [], isMultiple = true, allowDelete = true, onDelete, onUpload, allowSwap = true, targetPath, onSortEnd }: ImageUploaderProps) { const [images, setImages] = React.useState( currentImages.map((image) => ({ uuid: image.uuid, url: image.url, path: image.path })) ); const handleSortEnd = (oldIndex: number, newIndex: number) => { setImages((items) => { return arrayMove(items, oldIndex, newIndex); }); if (onSortEnd) { onSortEnd(oldIndex, newIndex); } }; const addImage = (imageArray: Image[]) => { if (!isMultiple) { // For single image mode, replace the current image setImages(imageArray); } else { setImages(images.concat(imageArray)); } }; const removeImage = (imageUuid) => { setImages(images.filter((i) => i.uuid !== imageUuid)); }; const onDeleteFn = async (image: Image) => { if (onDelete) { await onDelete(image); } removeImage(image.uuid); }; const onUploadFn = async (imageArray: Image[]) => { if (onUpload) { await onUpload(imageArray); } addImage(imageArray); }; const [result] = useQuery({ query: GetUploadApiQuery }); const { data, fetching, error } = result; if (error) { return (

There was an error:{error.message}

); } else if (fetching) { return ; } else { return (
); } }