import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Upload, Star, CreditCard, Trash2, ImagePlus } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu';
import { DragDropZone } from './DragDropZone';
import { supabase } from '@/integrations/supabase/client';
import { toast } from '@/hooks/use-toast';
import { Skeleton } from '@/components/ui/skeleton';
import { getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
export interface UploadedImage {
url: string;
cloudflare_id?: string;
file?: File;
isLocal?: boolean;
caption?: string;
}
export interface ImageAssignments {
uploaded: UploadedImage[];
banner_assignment?: number | null;
card_assignment?: number | null;
}
interface EntityMultiImageUploaderProps {
mode: 'create' | 'edit';
value: ImageAssignments;
onChange: (assignments: ImageAssignments) => void;
entityId?: string;
entityType?: string;
currentBannerUrl?: string;
currentCardUrl?: string;
}
export function EntityMultiImageUploader({
mode,
value,
onChange,
entityType = 'entity',
entityId,
currentBannerUrl,
currentCardUrl
}: EntityMultiImageUploaderProps) {
const maxImages = mode === 'create' ? 5 : 3;
const [loadingPhotos, setLoadingPhotos] = useState(false);
// Fetch existing photos when in edit mode
useEffect(() => {
if (mode === 'edit' && entityId && entityType) {
fetchEntityPhotos();
}
}, [mode, entityId, entityType]);
// Cleanup blob URLs when component unmounts or images change
useEffect(() => {
const currentImages = value.uploaded;
return () => {
// Revoke all blob URLs on cleanup
currentImages.forEach(image => {
if (image.isLocal && image.url.startsWith('blob:')) {
try {
URL.revokeObjectURL(image.url);
} catch (error: unknown) {
logger.error('Failed to revoke object URL', { error: getErrorMessage(error) });
}
}
});
};
}, [value.uploaded]);
const fetchEntityPhotos = async () => {
setLoadingPhotos(true);
try {
const { data, error } = await supabase
.from('photos')
.select('id, cloudflare_image_url, cloudflare_image_id, caption, title')
.eq('entity_type', entityType)
.eq('entity_id', entityId)
.order('created_at', { ascending: false });
if (error) throw error;
// Map to UploadedImage format
const mappedPhotos: UploadedImage[] = data?.map(photo => ({
url: photo.cloudflare_image_url,
cloudflare_id: photo.cloudflare_image_id,
caption: photo.caption || photo.title || '',
isLocal: false,
})) || [];
// Find banner and card indices based on currentBannerUrl/currentCardUrl
const bannerIndex = mappedPhotos.findIndex(p => p.url === currentBannerUrl);
const cardIndex = mappedPhotos.findIndex(p => p.url === currentCardUrl);
// Initialize with existing photos and current assignments
onChange({
uploaded: mappedPhotos,
banner_assignment: bannerIndex >= 0 ? bannerIndex : null,
card_assignment: cardIndex >= 0 ? cardIndex : null,
});
} catch (error: unknown) {
toast({
title: 'Error',
description: getErrorMessage(error),
variant: 'destructive',
});
} finally {
setLoadingPhotos(false);
}
};
const handleFilesAdded = (files: File[]) => {
const currentCount = value.uploaded.length;
const availableSlots = maxImages - currentCount;
if (availableSlots <= 0) {
return;
}
const filesToAdd = files.slice(0, availableSlots);
const newImages: UploadedImage[] = filesToAdd.map(file => ({
url: URL.createObjectURL(file),
file,
isLocal: true,
}));
const updatedUploaded = [...value.uploaded, ...newImages];
const updatedValue: ImageAssignments = {
uploaded: updatedUploaded,
banner_assignment: value.banner_assignment,
card_assignment: value.card_assignment,
};
// Auto-assign banner if not set
if (updatedValue.banner_assignment === undefined || updatedValue.banner_assignment === null) {
if (updatedUploaded.length > 0) {
updatedValue.banner_assignment = 0;
}
}
// Auto-assign card if not set
if (updatedValue.card_assignment === undefined || updatedValue.card_assignment === null) {
if (updatedUploaded.length > 0) {
updatedValue.card_assignment = 0;
}
}
onChange(updatedValue);
};
const handleAssignRole = (index: number, role: 'banner' | 'card') => {
const updatedValue: ImageAssignments = {
...value,
[role === 'banner' ? 'banner_assignment' : 'card_assignment']: index,
};
onChange(updatedValue);
};
const handleRemoveImage = (index: number) => {
const imageToRemove = value.uploaded[index];
// Revoke object URL if it's a local file
if (imageToRemove.isLocal && imageToRemove.url.startsWith('blob:')) {
URL.revokeObjectURL(imageToRemove.url);
}
const updatedUploaded = value.uploaded.filter((_, i) => i !== index);
const updatedValue: ImageAssignments = {
uploaded: updatedUploaded,
banner_assignment: value.banner_assignment === index
? (updatedUploaded.length > 0 ? 0 : null)
: value.banner_assignment !== null && value.banner_assignment !== undefined && value.banner_assignment > index
? value.banner_assignment - 1
: value.banner_assignment,
card_assignment: value.card_assignment === index
? (updatedUploaded.length > 0 ? 0 : null)
: value.card_assignment !== null && value.card_assignment !== undefined && value.card_assignment > index
? value.card_assignment - 1
: value.card_assignment,
};
onChange(updatedValue);
};
const renderImageCard = (image: UploadedImage, index: number) => {
const isBanner = value.banner_assignment === index;
const isCard = value.card_assignment === index;
const isExisting = !image.isLocal;
return (
Right-click for options
Loading existing photos...
• Right-click images to set as banner or card
• Images will be uploaded when you submit the form
{mode === 'edit' &&• No existing photos found for this entity
}Add More Images
Drag & drop or click to browse ({value.uploaded.length}/{maxImages})
{getHelperText()}
Banner: Main header image for the {entityType} detail page {value.banner_assignment !== null && value.banner_assignment !== undefined && ` (Image ${value.banner_assignment + 1})`}
Card: Thumbnail in {entityType} listings and search results {value.card_assignment !== null && value.card_assignment !== undefined && ` (Image ${value.card_assignment + 1})`}