Drag between products fix

This commit is contained in:
2025-02-26 19:04:35 -05:00
parent e3361cf098
commit 2d62cac5f7

View File

@@ -27,7 +27,8 @@ import {
useDndMonitor,
DragMoveEvent,
closestCorners,
CollisionDetection
CollisionDetection,
useDroppable
} from '@dnd-kit/core';
import {
arrayMove,
@@ -68,26 +69,6 @@ type ProductImageSortable = ProductImage & {
id: string;
};
// Custom collision detection algorithm that combines multiple strategies
const customCollisionDetection: CollisionDetection = (args) => {
// First, try pointer intersection
const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) {
return pointerCollisions;
}
// If no pointer collisions, try rect intersection
const rectCollisions = rectIntersection(args);
if (rectCollisions.length > 0) {
return rectCollisions;
}
// If still no collisions, use closest corners
return closestCorners(args);
};
export const ImageUploadStep = <T extends string>({
data,
file,
@@ -121,6 +102,77 @@ export const ImageUploadStep = <T extends string>({
// Track which product container is being hovered over
const [activeDroppableId, setActiveDroppableId] = useState<string | null>(null);
// Custom collision detection algorithm that prioritizes product containers
const customCollisionDetection: CollisionDetection = (args) => {
const { droppableContainers, active, pointerCoordinates } = args;
if (!pointerCoordinates) {
return [];
}
// Get the active container (product index)
const activeContainer = findContainer(active.id.toString());
// Get all collisions
const pointerCollisions = pointerWithin(args);
const rectCollisions = rectIntersection(args);
// Combine collision methods for more reliable detection
const allCollisions = [...pointerCollisions, ...rectCollisions];
// Check for image collisions first - for reordering within the same container
const imageCollisions = allCollisions.filter(collision => {
const collisionId = collision.id.toString();
// Only detect other images, not the active image
if (collisionId === active.id.toString()) return false;
// Check if it's an image by looking for it in productImages
return productImages.some(img => img.id === collisionId);
});
// If we have image collisions within the same container, prioritize those for reordering
if (activeContainer && imageCollisions.length > 0) {
const sameContainerCollisions = imageCollisions.filter(collision => {
const image = productImages.find(img => img.id === collision.id);
return image && image.productIndex.toString() === activeContainer;
});
if (sameContainerCollisions.length > 0) {
return [sameContainerCollisions[0]]; // Return the first image collision in the same container
}
}
// If no image collisions in the same container, check for product container collisions
const productContainerCollisions = allCollisions.filter(
collision => typeof collision.id === 'string' && collision.id.toString().startsWith('product-')
);
// If the active container is different from container collisions, prioritize those
if (activeContainer && productContainerCollisions.length > 0) {
const differentContainerCollisions = productContainerCollisions.filter(collision => {
const containerIndex = collision.id.toString().split('-')[1];
return containerIndex !== activeContainer;
});
if (differentContainerCollisions.length > 0) {
return [differentContainerCollisions[0]];
}
}
// If we have any product container collisions, use those
if (productContainerCollisions.length > 0) {
return [productContainerCollisions[0]];
}
// Finally, check images in different containers
if (imageCollisions.length > 0) {
return [imageCollisions[0]];
}
// Fall back to all collisions
return allCollisions.length > 0 ? [allCollisions[0]] : [];
};
// Handle drag start to set active image and prevent default behavior
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
@@ -134,21 +186,30 @@ export const ImageUploadStep = <T extends string>({
// Handle drag over to track which product container is being hovered
const handleDragOver = (event: DragMoveEvent) => {
const { over } = event;
const { active, over } = event;
if (!over) {
setActiveDroppableId(null);
return;
}
// Check if we're over a product container
if (typeof over.id === 'string' && over.id.startsWith('product-')) {
setActiveDroppableId(over.id);
} else {
// We might be over another image, so get its product container
const activeContainer = findContainer(active.id.toString());
let overContainer = null;
// Check if we're over a product container directly
if (typeof over.id === 'string' && over.id.toString().startsWith('product-')) {
overContainer = over.id.toString();
setActiveDroppableId(overContainer);
// Log the hover state for debugging
console.log('Hovering over product container:', overContainer);
}
// Otherwise check if we're over another image
else {
const overImage = productImages.find(img => img.id === over.id);
if (overImage) {
setActiveDroppableId(`product-${overImage.productIndex}`);
overContainer = `product-${overImage.productIndex}`;
setActiveDroppableId(overContainer);
} else {
setActiveDroppableId(null);
}
@@ -171,12 +232,94 @@ export const ImageUploadStep = <T extends string>({
};
}, [activeId]);
// Function to find the container (productIndex) an image belongs to
// Function to find container (productIndex) an image belongs to
const findContainer = (id: string) => {
const image = productImages.find(img => img.id === id);
return image ? image.productIndex.toString() : null;
};
// Function to get images for a specific product
const getProductImages = (productIndex: number) => {
return productImages.filter(img => img.productIndex === productIndex);
};
// Add product IDs to the valid droppable elements
useEffect(() => {
// Add data-droppable attributes to make product containers easier to identify
data.forEach((_, index) => {
const container = document.getElementById(`product-${index}`);
if (container) {
container.setAttribute('data-droppable', 'true');
container.setAttribute('aria-dropeffect', 'move');
// Check if the container has images
const hasImages = getProductImages(index).length > 0;
// Add stronger attributes for empty containers to ensure they stand out as drop targets
if (!hasImages) {
container.setAttribute('data-empty', 'true');
// Setting tabindex makes the element focusable which can help with accessibility
container.setAttribute('tabindex', '0');
// Add ARIA attributes to improve accessibility and recognition
container.setAttribute('aria-label', `Empty drop zone for product ${index + 1}`);
// Ensure the empty container has sufficient size to be a drop target
if (container.offsetHeight < 100) {
container.style.minHeight = '100px';
}
} else {
container.setAttribute('data-empty', 'false');
}
}
});
}, [data, productImages]); // Add productImages as a dependency to re-run when images change
// Effect to register browser-level drag events on product containers
useEffect(() => {
// For each product container
data.forEach((_, index) => {
const container = document.getElementById(`product-${index}`);
if (container) {
// Define handlers for native browser drag events
const handleNativeDragOver = (e: DragEvent) => {
e.preventDefault();
if (getProductImages(index).length === 0) {
// Highlight empty containers during dragover
container.classList.add('border-primary', 'bg-primary/5');
setActiveDroppableId(`product-${index}`);
}
};
const handleNativeDragLeave = (e: DragEvent) => {
if (getProductImages(index).length === 0) {
// Remove highlight when drag leaves
container.classList.remove('border-primary', 'bg-primary/5');
if (activeDroppableId === `product-${index}`) {
setActiveDroppableId(null);
}
}
};
// Add these handlers
container.addEventListener('dragover', handleNativeDragOver);
container.addEventListener('dragleave', handleNativeDragLeave);
// Log this for debugging
console.log(`Added native drag handlers to product-${index}`, {
isEmpty: getProductImages(index).length === 0
});
// Return cleanup function
return () => {
container.removeEventListener('dragover', handleNativeDragOver);
container.removeEventListener('dragleave', handleNativeDragLeave);
};
}
});
}, [data, productImages]); // Re-run when data or productImages change
// Function to remove an image URL from a product
const removeImageFromProduct = (productIndex: number, imageUrl: string) => {
// Create a copy of the data
@@ -221,19 +364,37 @@ export const ImageUploadStep = <T extends string>({
const activeContainer = findContainer(activeId.toString());
let overContainer = null;
// Determine the target container:
// 1. First check if the over.id is a product container ID directly
if (typeof overId === 'string' && overId.startsWith('product-')) {
overContainer = overId.split('-')[1];
// Check if overId is a product container directly
if (typeof overId === 'string' && overId.toString().startsWith('product-')) {
overContainer = overId.toString().split('-')[1];
console.log('Dropping directly onto product container:', overContainer);
}
// 2. Otherwise, it might be another image, so find its container
// Otherwise check if it's an image, so find its container
else {
overContainer = findContainer(overId.toString());
console.log('Dropping onto another image in container:', overContainer);
}
// If we couldn't determine either container, do nothing
if (!activeContainer || !overContainer) {
console.log('Could not determine containers', { activeContainer, overContainer, activeId, overId });
// Log what was detected for debugging
console.log('Drag end detected:', {
activeId,
overId,
overContainer,
activeContainer,
isOverProduct: typeof overId === 'string' && overId.toString().startsWith('product-')
});
// If we couldn't determine active container, do nothing
if (!activeContainer) {
console.log('Could not determine source container', { activeContainer, activeId });
setActiveId(null);
setActiveImage(null);
return;
}
// If we couldn't determine the over container, do nothing
if (!overContainer) {
console.log('Could not determine target container', { overContainer, overId });
setActiveId(null);
setActiveImage(null);
return;
@@ -251,14 +412,88 @@ export const ImageUploadStep = <T extends string>({
return;
}
// If source and target are the same product, handle reordering
if (sourceProductIndex === targetProductIndex) {
// Only if overId is an image id (not a product container) - otherwise we'd just drop at the end
if (!overId.toString().startsWith('product-')) {
// IMPORTANT: If source and target are different products, ALWAYS prioritize moving over reordering
if (sourceProductIndex !== targetProductIndex) {
console.log('Moving image between products', { sourceProductIndex, targetProductIndex });
// Create a copy of the image with the new product index
const newImage: ProductImageSortable = {
...activeImage,
productIndex: targetProductIndex,
// Generate a new ID for the image in its new location
id: `image-${targetProductIndex}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
};
// Remove the image from the source product and add to target product
setProductImages(items => {
// Remove the image from its current product
const filteredItems = items.filter(item => item.id !== activeId);
// Add the image to the target product
filteredItems.push(newImage);
// Check if the operation was successful
console.log('Image movement operation', {
originalCount: items.length,
newCount: filteredItems.length,
sourceImages: items.filter(item => item.productIndex === sourceProductIndex).length,
targetImagesBefore: items.filter(item => item.productIndex === targetProductIndex).length,
targetImagesAfter: filteredItems.filter(item => item.productIndex === targetProductIndex).length
});
// Update both products' image_url fields - creating new objects to ensure state updates
let updatedData = [...data]; // Start with a fresh copy
// First remove from source
updatedData = removeImageFromProduct(sourceProductIndex, activeImage.imageUrl);
// Then add to target
updatedData = addImageToProduct(targetProductIndex, activeImage.imageUrl);
// Show notification
toast.success(`Image moved from ${data[sourceProductIndex].name || `Product #${sourceProductIndex + 1}`} to ${data[targetProductIndex].name || `Product #${targetProductIndex + 1}`}`);
return filteredItems;
});
}
// Source and target are the same product - this is a reordering operation
else {
// Only attempt reordering if we have at least 2 images in this container
const productImages = getProductImages(sourceProductIndex);
if (productImages.length >= 2) {
console.log('Reordering images within product', { sourceProductIndex, imagesCount: productImages.length });
// Handle reordering regardless of whether we're over a container or another image
setProductImages(items => {
// Filter to get only the images for this product
const productFilteredItems = items.filter(item => item.productIndex === sourceProductIndex);
// If dropping onto the container itself, put at the end
if (overId.toString().startsWith('product-')) {
console.log('Dropping onto container - moving to end');
// Find active index
const activeIndex = productFilteredItems.findIndex(item => item.id === activeId);
if (activeIndex === -1) {
return items; // No change needed
}
// Move active item to end (remove and push to end)
const newFilteredItems = [...productFilteredItems];
const [movedItem] = newFilteredItems.splice(activeIndex, 1);
newFilteredItems.push(movedItem);
// Create a new full list replacing the items for this product with the reordered ones
const newItems = items.filter(item => item.productIndex !== sourceProductIndex);
newItems.push(...newFilteredItems);
// Update the product data with the new image order
const updatedData = updateProductImageOrder(sourceProductIndex, newFilteredItems);
return newItems;
}
// Find indices within the filtered list
const activeIndex = productFilteredItems.findIndex(item => item.id === activeId);
const overIndex = productFilteredItems.findIndex(item => item.id === overId);
@@ -282,39 +517,6 @@ export const ImageUploadStep = <T extends string>({
});
}
}
// If source and target are different products, handle reassigning
else {
// Create a copy of the image with the new product index
const newImage: ProductImageSortable = {
...activeImage,
productIndex: targetProductIndex,
// Generate a new ID for the image in its new location
id: `image-${targetProductIndex}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
};
// Remove the image from the source product and add to target product
setProductImages(items => {
// Remove the image from its current product
const filteredItems = items.filter(item => item.id !== activeId);
// Add the image to the target product
filteredItems.push(newImage);
// Update both products' image_url fields - creating new objects to ensure state updates
let updatedData = [...data]; // Start with a fresh copy
// First remove from source
updatedData = removeImageFromProduct(sourceProductIndex, activeImage.imageUrl);
// Then add to target
updatedData = addImageToProduct(targetProductIndex, activeImage.imageUrl);
// Show notification
toast.success(`Image moved from ${data[sourceProductIndex].name || `Product #${sourceProductIndex + 1}`} to ${data[targetProductIndex].name || `Product #${targetProductIndex + 1}`}`);
return filteredItems;
});
}
setActiveId(null);
setActiveImage(null);
@@ -621,17 +823,71 @@ export const ImageUploadStep = <T extends string>({
};
}, []);
// Add product IDs to the valid droppable elements
useEffect(() => {
// Add data-droppable attributes to make product containers easier to identify
data.forEach((_, index) => {
const container = document.getElementById(`product-${index}`);
if (container) {
container.setAttribute('data-droppable', 'true');
container.setAttribute('aria-dropeffect', 'move');
// Function to remove an image
const removeImage = async (imageIndex: number) => {
const image = productImages[imageIndex];
if (!image) return;
try {
// Extract the filename from the URL
const urlParts = image.imageUrl.split('/');
const filename = urlParts[urlParts.length - 1];
// Call API to delete the image
const response = await fetch(`${config.apiUrl}/import/delete-image`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageUrl: image.imageUrl,
filename
}),
});
if (!response.ok) {
throw new Error('Failed to delete image');
}
});
}, [data]);
// Remove the image from our state
setProductImages(prev => prev.filter((_, idx) => idx !== imageIndex));
// Remove the image URL from the product data
const updatedData = removeImageFromProduct(image.productIndex, image.imageUrl);
toast.success('Image removed successfully');
} catch (error) {
console.error('Delete error:', error);
toast.error(`Failed to remove image: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
// Handle calling onSubmit with the current data
const handleSubmit = useCallback(async () => {
setIsSubmitting(true);
try {
await onSubmit(data, file);
} catch (error) {
console.error('Submit error:', error);
toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSubmitting(false);
}
}, [data, file, onSubmit]);
// Function to ensure URLs are properly formatted with absolute paths
const getFullImageUrl = (url: string): string => {
// If the URL is already absolute (starts with http:// or https://) return it as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Otherwise, it's a relative URL, prepend the domain
const baseUrl = 'https://inventory.acot.site';
// Make sure url starts with / for path
const path = url.startsWith('/') ? url : `/${url}`;
return `${baseUrl}${path}`;
};
// Generic dropzone component
const GenericDropzone = () => {
@@ -719,77 +975,6 @@ export const ImageUploadStep = <T extends string>({
);
};
// Function to remove an image
const removeImage = async (imageIndex: number) => {
const image = productImages[imageIndex];
if (!image) return;
try {
// Extract the filename from the URL
const urlParts = image.imageUrl.split('/');
const filename = urlParts[urlParts.length - 1];
// Call API to delete the image
const response = await fetch(`${config.apiUrl}/import/delete-image`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageUrl: image.imageUrl,
filename
}),
});
if (!response.ok) {
throw new Error('Failed to delete image');
}
// Remove the image from our state
setProductImages(prev => prev.filter((_, idx) => idx !== imageIndex));
// Remove the image URL from the product data
const updatedData = removeImageFromProduct(image.productIndex, image.imageUrl);
toast.success('Image removed successfully');
} catch (error) {
console.error('Delete error:', error);
toast.error(`Failed to remove image: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
// Handle calling onSubmit with the current data
const handleSubmit = useCallback(async () => {
setIsSubmitting(true);
try {
await onSubmit(data, file);
} catch (error) {
console.error('Submit error:', error);
toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSubmitting(false);
}
}, [data, file, onSubmit]);
// Get images for a specific product
const getProductImages = (productIndex: number) => {
return productImages.filter(img => img.productIndex === productIndex);
};
// Function to ensure URLs are properly formatted with absolute paths
const getFullImageUrl = (url: string): string => {
// If the URL is already absolute (starts with http:// or https://) return it as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Otherwise, it's a relative URL, prepend the domain
const baseUrl = 'https://inventory.acot.site';
// Make sure url starts with / for path
const path = url.startsWith('/') ? url : `/${url}`;
return `${baseUrl}${path}`;
};
// Component for individual unassigned image item
const UnassignedImageItem = ({ image, index }: { image: UnassignedImage; index: number }) => {
const [dialogOpen, setDialogOpen] = useState(false);
@@ -980,12 +1165,18 @@ export const ImageUploadStep = <T extends string>({
}
});
// Create a new style object with fixed dimensions to prevent distortion
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 10 : 0, // Higher z-index when dragging
touchAction: 'none' as 'none', // Prevent touch scrolling during drag
width: '96px',
height: '96px',
flexShrink: 0,
flexGrow: 0,
position: 'relative' as 'relative',
};
// Create a ref for the buttons to exclude them from drag listeners
@@ -996,7 +1187,7 @@ export const ImageUploadStep = <T extends string>({
<div
ref={setNodeRef}
style={style}
className="relative border rounded-md overflow-hidden h-24 w-24 flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag"
className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag"
{...attributes}
{...listeners}
>
@@ -1015,7 +1206,7 @@ export const ImageUploadStep = <T extends string>({
/>
<button
ref={deleteButtonRef}
className="absolute top-1 right-1 bg-black/60 rounded-full p-1.5 text-white z-10 hover:bg-black/80"
className="absolute top-1 right-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag listeners
@@ -1031,7 +1222,7 @@ export const ImageUploadStep = <T extends string>({
e.stopPropagation(); // Prevent drag from starting on touch
}}
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className="h-3 w-3" />
</button>
{/* Fix zoom button with proper state management */}
@@ -1039,7 +1230,7 @@ export const ImageUploadStep = <T extends string>({
<DialogTrigger asChild>
<button
ref={zoomButtonRef}
className="absolute bottom-1 left-1 bg-black/60 rounded-full p-1.5 text-white z-10 hover:bg-black/80"
className="absolute bottom-1 left-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag listeners
@@ -1055,7 +1246,7 @@ export const ImageUploadStep = <T extends string>({
e.stopPropagation(); // Prevent drag from starting on touch
}}
>
<Maximize2 className="h-3.5 w-3.5" />
<Maximize2 className="h-3 w-3" />
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[650px] max-h-[90vh] p-6 bg-background/95 backdrop-blur-sm border border-border rounded-lg shadow-2xl">
@@ -1091,15 +1282,43 @@ export const ImageUploadStep = <T extends string>({
const getProductContainerClasses = (index: number) => {
const isValidDropTarget = activeId && findContainer(activeId) !== index.toString();
const isActiveDropTarget = activeDroppableId === `product-${index}`;
const hasImages = getProductImages(index).length > 0;
return cn(
"flex flex-wrap gap-2 overflow-x-auto flex-1 min-h-[6rem] rounded-md p-2 transition-all",
isValidDropTarget && "border border-dashed",
"flex-1 min-h-[6rem] rounded-md p-2 transition-all",
// Always add a border for empty containers to make them visible as drop targets
!hasImages && "border-2 border-dashed border-secondary-foreground/30",
// Active drop target styling
isValidDropTarget && isActiveDropTarget
? "border-primary bg-primary/10 border-2"
: isValidDropTarget
? "border-muted-foreground/30 hover:border-primary/50 hover:bg-primary/5"
: ""
? "border-2 border-dashed border-primary bg-primary/10"
: isValidDropTarget && !hasImages
? "border-2 border-dashed border-muted-foreground/40 bg-muted/20"
: isValidDropTarget
? "border border-dashed border-muted-foreground/30"
: ""
);
};
// Add a DroppableContainer component for empty product containers
const DroppableContainer = ({ id, children, isEmpty }: { id: string; children: React.ReactNode; isEmpty: boolean }) => {
const { setNodeRef } = useDroppable({
id,
data: {
type: 'container',
isEmpty
}
});
return (
<div
ref={setNodeRef}
id={id}
data-droppable="true"
data-empty={isEmpty ? "true" : "false"}
className={`w-full h-full ${!isEmpty ? "flex flex-row flex-wrap gap-2" : ""}`}
>
{children}
</div>
);
};
@@ -1159,7 +1378,7 @@ export const ImageUploadStep = <T extends string>({
"ring-2 ring-primary bg-primary/5"
)}
>
<CardContent id={`product-${index}`} className="p-0">
<CardContent className="p-0">
<div className="flex flex-col gap-2">
<div className="flex flex-col lg:flex-row lg:items-baseline gap-0.5 lg:gap-4">
<h3 className="text-base font-medium">{product.name || `Product #${index + 1}`}</h3>
@@ -1176,22 +1395,44 @@ export const ImageUploadStep = <T extends string>({
{/* Images appear to the right of the dropzone in a sortable container */}
<div
className={getProductContainerClasses(index)}
data-product-id={`product-${index}`}
id={`product-${index}`}
style={{
pointerEvents: 'auto',
touchAction: 'none',
minHeight: getProductImages(index).length === 0 ? '100px' : 'auto'
}}
onDragOver={(e) => {
// This is a native event handler to ensure the browser recognizes the drop zone
e.preventDefault();
e.stopPropagation();
if (getProductImages(index).length === 0) {
setActiveDroppableId(`product-${index}`);
}
}}
>
<SortableContext
items={getProductImages(index).map(img => img.id)}
strategy={horizontalListSortingStrategy}
<DroppableContainer
id={`product-${index}`}
isEmpty={getProductImages(index).length === 0}
>
{getProductImages(index).map((image, imgIndex) => (
<SortableImage
key={image.id}
image={image}
productIndex={index}
imgIndex={imgIndex}
/>
))}
</SortableContext>
{getProductImages(index).length > 0 ? (
<SortableContext
items={getProductImages(index).map(img => img.id)}
strategy={horizontalListSortingStrategy}
>
{getProductImages(index).map((image, imgIndex) => (
<SortableImage
key={image.id}
image={image}
productIndex={index}
imgIndex={imgIndex}
/>
))}
</SortableContext>
) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground text-sm py-8">
Drop images here
</div>
)}
</DroppableContainer>
</div>
{/* Hidden file input for backwards compatibility */}
@@ -1211,9 +1452,17 @@ export const ImageUploadStep = <T extends string>({
</div>
{/* Drag overlay for showing the dragged image */}
<DragOverlay adjustScale zIndex={1000} dropAnimation={null}>
<DragOverlay adjustScale={false} zIndex={1000} dropAnimation={null}>
{activeId && activeImage && (
<div className="relative border rounded-md overflow-hidden h-24 w-24 flex items-center justify-center opacity-90 cursor-grabbing shadow-xl no-native-drag">
<div
className="relative border rounded-md overflow-hidden h-24 w-24 flex items-center justify-center opacity-90 cursor-grabbing shadow-xl no-native-drag"
style={{
width: "96px",
height: "96px",
transform: "none",
willChange: "transform"
}}
>
<img
src={getFullImageUrl(activeImage.imageUrl)}
alt="Dragging image"