From fddb87c5beb163d1e464e7ec1a51b905c4c13505 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:28:57 +0000 Subject: [PATCH] feat: Implement photo selection for entity edit forms --- src/components/admin/DesignerForm.tsx | 9 +- src/components/admin/ManufacturerForm.tsx | 9 +- src/components/admin/OperatorForm.tsx | 9 +- src/components/admin/ParkForm.tsx | 6 + src/components/admin/PropertyOwnerForm.tsx | 9 +- src/components/admin/RideForm.tsx | 9 +- src/components/admin/RideModelForm.tsx | 9 +- .../upload/EntityMultiImageUploader.tsx | 136 +++++++++++++++--- 8 files changed, 174 insertions(+), 22 deletions(-) diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index 7342563f..4014023c 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -45,7 +45,11 @@ type DesignerFormData = z.infer; interface DesignerFormProps { onSubmit: (data: DesignerFormData) => void; onCancel: () => void; - initialData?: Partial; + initialData?: Partial; } export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps) { @@ -193,6 +197,9 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr value={watch('images') || { uploaded: [] }} onChange={(images) => setValue('images', images)} entityType="designer" + entityId={initialData?.id} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Actions */} diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index c685f7e5..d48f2334 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -45,7 +45,11 @@ type ManufacturerFormData = z.infer; interface ManufacturerFormProps { onSubmit: (data: ManufacturerFormData) => void; onCancel: () => void; - initialData?: Partial; + initialData?: Partial; } export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps) { @@ -193,6 +197,9 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur value={watch('images') || { uploaded: [] }} onChange={(images) => setValue('images', images)} entityType="manufacturer" + entityId={initialData?.id} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Actions */} diff --git a/src/components/admin/OperatorForm.tsx b/src/components/admin/OperatorForm.tsx index e3c4fcc5..67fef765 100644 --- a/src/components/admin/OperatorForm.tsx +++ b/src/components/admin/OperatorForm.tsx @@ -45,7 +45,11 @@ type OperatorFormData = z.infer; interface OperatorFormProps { onSubmit: (data: OperatorFormData) => void; onCancel: () => void; - initialData?: Partial; + initialData?: Partial; } export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps) { @@ -193,6 +197,9 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr value={watch('images') || { uploaded: [] }} onChange={(images) => setValue('images', images)} entityType="operator" + entityId={initialData?.id} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Actions */} diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 46ea8be8..5f500105 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -55,8 +55,11 @@ interface ParkFormProps { }) => Promise; onCancel?: () => void; initialData?: Partial; isEditing?: boolean; } @@ -421,6 +424,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: value={watch('images')} onChange={(images: ImageAssignments) => setValue('images', images)} entityType="park" + entityId={isEditing ? initialData?.id : undefined} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Form Actions */} diff --git a/src/components/admin/PropertyOwnerForm.tsx b/src/components/admin/PropertyOwnerForm.tsx index 38105240..85bc95a5 100644 --- a/src/components/admin/PropertyOwnerForm.tsx +++ b/src/components/admin/PropertyOwnerForm.tsx @@ -45,7 +45,11 @@ type PropertyOwnerFormData = z.infer; interface PropertyOwnerFormProps { onSubmit: (data: PropertyOwnerFormData) => void; onCancel: () => void; - initialData?: Partial; + initialData?: Partial; } export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyOwnerFormProps) { @@ -193,6 +197,9 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO value={watch('images') || { uploaded: [] }} onChange={(images) => setValue('images', images)} entityType="property_owner" + entityId={initialData?.id} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Actions */} diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index d7d2cfea..eb41139c 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -84,7 +84,11 @@ type RideFormData = z.infer; interface RideFormProps { onSubmit: (data: RideFormData) => Promise; onCancel?: () => void; - initialData?: Partial; + initialData?: Partial; isEditing?: boolean; } @@ -752,6 +756,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: value={watch('images') || { uploaded: [] }} onChange={(images: ImageAssignments) => setValue('images', images)} entityType="ride" + entityId={isEditing ? initialData?.id : undefined} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Form Actions */} diff --git a/src/components/admin/RideModelForm.tsx b/src/components/admin/RideModelForm.tsx index 141c7efb..38f20b34 100644 --- a/src/components/admin/RideModelForm.tsx +++ b/src/components/admin/RideModelForm.tsx @@ -42,7 +42,11 @@ interface RideModelFormProps { manufacturerId?: string; onSubmit: (data: RideModelFormData) => void; onCancel: () => void; - initialData?: Partial; + initialData?: Partial; } const categories = [ @@ -193,6 +197,9 @@ export function RideModelForm({ value={watch('images') || { uploaded: [] }} onChange={(images) => setValue('images', images)} entityType="ride_model" + entityId={initialData?.id} + currentBannerUrl={initialData?.banner_image_url} + currentCardUrl={initialData?.card_image_url} /> {/* Actions */} diff --git a/src/components/upload/EntityMultiImageUploader.tsx b/src/components/upload/EntityMultiImageUploader.tsx index faf728d0..300b501c 100644 --- a/src/components/upload/EntityMultiImageUploader.tsx +++ b/src/components/upload/EntityMultiImageUploader.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +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'; @@ -10,6 +10,9 @@ import { 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'; export interface UploadedImage { url: string; @@ -31,15 +34,70 @@ interface EntityMultiImageUploaderProps { onChange: (assignments: ImageAssignments) => void; entityId?: string; entityType?: string; + currentBannerUrl?: string; + currentCardUrl?: string; } export function EntityMultiImageUploader({ mode, value, onChange, - entityType = 'entity' + 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]); + + 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) { + console.error('Failed to load entity photos:', error); + toast({ + title: 'Error', + description: 'Failed to load existing photos', + variant: 'destructive', + }); + } finally { + setLoadingPhotos(false); + } + }; const handleFilesAdded = (files: File[]) => { const currentCount = value.uploaded.length; @@ -118,12 +176,15 @@ export function EntityMultiImageUploader({ const renderImageCard = (image: UploadedImage, index: number) => { const isBanner = value.banner_assignment === index; const isCard = value.card_assignment === index; + const isExisting = !image.isLocal; return (
-
+
{`Upload )} + {isExisting && ( + + Existing + + )} {image.isLocal && ( Not uploaded @@ -152,6 +218,21 @@ export function EntityMultiImageUploader({ )}
+ {/* Remove button - only for new uploads */} + {image.isLocal && ( + + )} + {/* Hover hint */}

@@ -178,15 +259,18 @@ export function EntityMultiImageUploader({ {isCard ? 'Card (Current)' : 'Set as Card'} - - - handleRemoveImage(index)} - className="text-destructive focus:text-destructive" - > - - Remove Image - + {image.isLocal && ( + <> + + handleRemoveImage(index)} + className="text-destructive focus:text-destructive" + > + + Remove Image + + + )} ); @@ -197,18 +281,37 @@ export function EntityMultiImageUploader({ return 'Upload images to get started. Images will be uploaded when you submit the form.'; } - const pendingUploads = value.uploaded.filter(img => img.isLocal).length; + const existingCount = value.uploaded.filter(img => !img.isLocal).length; + const newCount = value.uploaded.filter(img => img.isLocal).length; const parts = []; - if (pendingUploads > 0) { - parts.push(`${pendingUploads} image${pendingUploads > 1 ? 's' : ''} ready to upload on submission`); + if (mode === 'edit' && existingCount > 0) { + parts.push(`${existingCount} existing photo${existingCount !== 1 ? 's' : ''}`); } - parts.push('Right-click any image to assign roles or remove'); + if (newCount > 0) { + parts.push(`${newCount} new image${newCount > 1 ? 's' : ''} ready to upload`); + } + + parts.push('Right-click to assign banner/card roles'); return parts.join(' • '); }; + // Loading state + if (loadingPhotos) { + return ( +

+
+ {[1, 2, 3].map((i) => ( + + ))} +
+

Loading existing photos...

+
+ ); + } + // Empty state: show large drag & drop zone if (value.uploaded.length === 0) { return ( @@ -222,6 +325,7 @@ export function EntityMultiImageUploader({

• 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

}
);