diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index b87bf518..dbb033be 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -12,7 +12,7 @@ import { Ruler, Save, X } from 'lucide-react'; import { Combobox } from '@/components/ui/combobox'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; -import { EntityImageUploader } from '@/components/upload/EntityImageUploader'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; const designerSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -22,11 +22,15 @@ const designerSchema = z.object({ website_url: z.string().url().optional().or(z.literal('')), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), headquarters_location: z.string().optional(), - logo_url: z.string().optional(), - banner_image_id: z.string().optional(), - banner_image_url: z.string().optional(), - card_image_id: z.string().optional(), - card_image_url: z.string().optional() + images: z.object({ + uploaded: z.array(z.object({ + url: z.string(), + cloudflare_id: z.string(), + caption: z.string().optional() + })), + banner_assignment: z.number().optional(), + card_assignment: z.number().optional() + }).optional() }); type DesignerFormData = z.infer; @@ -57,11 +61,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr website_url: initialData?.website_url || '', founded_year: initialData?.founded_year || undefined, headquarters_location: initialData?.headquarters_location || '', - logo_url: initialData?.logo_url || '', - banner_image_id: initialData?.banner_image_id || '', - banner_image_url: initialData?.banner_image_url || '', - card_image_id: initialData?.card_image_id || '', - card_image_url: initialData?.card_image_url || '' + images: initialData?.images || { uploaded: [] } } }); @@ -181,20 +181,10 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr {/* Images */} - { - if (images.logo_url !== undefined) setValue('logo_url', images.logo_url); - if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id); - if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url); - if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id); - if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url); - }} - showLogo={true} + setValue('images', images)} entityType="designer" /> diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index c569d037..54802be0 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -12,7 +12,7 @@ import { Building2, Save, X } from 'lucide-react'; import { Combobox } from '@/components/ui/combobox'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; -import { EntityImageUploader } from '@/components/upload/EntityImageUploader'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; const manufacturerSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -22,11 +22,15 @@ const manufacturerSchema = z.object({ website_url: z.string().url().optional().or(z.literal('')), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), headquarters_location: z.string().optional(), - logo_url: z.string().optional(), - banner_image_id: z.string().optional(), - banner_image_url: z.string().optional(), - card_image_id: z.string().optional(), - card_image_url: z.string().optional() + images: z.object({ + uploaded: z.array(z.object({ + url: z.string(), + cloudflare_id: z.string(), + caption: z.string().optional() + })), + banner_assignment: z.number().optional(), + card_assignment: z.number().optional() + }).optional() }); type ManufacturerFormData = z.infer; @@ -57,11 +61,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur website_url: initialData?.website_url || '', founded_year: initialData?.founded_year || undefined, headquarters_location: initialData?.headquarters_location || '', - logo_url: initialData?.logo_url || '', - banner_image_id: initialData?.banner_image_id || '', - banner_image_url: initialData?.banner_image_url || '', - card_image_id: initialData?.card_image_id || '', - card_image_url: initialData?.card_image_url || '' + images: initialData?.images || { uploaded: [] } } }); @@ -181,20 +181,10 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur {/* Images */} - { - if (images.logo_url !== undefined) setValue('logo_url', images.logo_url); - if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id); - if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url); - if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id); - if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url); - }} - showLogo={true} + setValue('images', images)} entityType="manufacturer" /> diff --git a/src/components/admin/OperatorForm.tsx b/src/components/admin/OperatorForm.tsx index c82be547..e56471a6 100644 --- a/src/components/admin/OperatorForm.tsx +++ b/src/components/admin/OperatorForm.tsx @@ -12,7 +12,7 @@ import { FerrisWheel, Save, X } from 'lucide-react'; import { Combobox } from '@/components/ui/combobox'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; -import { EntityImageUploader } from '@/components/upload/EntityImageUploader'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; const operatorSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -22,11 +22,15 @@ const operatorSchema = z.object({ website_url: z.string().url().optional().or(z.literal('')), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), headquarters_location: z.string().optional(), - logo_url: z.string().optional(), - banner_image_id: z.string().optional(), - banner_image_url: z.string().optional(), - card_image_id: z.string().optional(), - card_image_url: z.string().optional() + images: z.object({ + uploaded: z.array(z.object({ + url: z.string(), + cloudflare_id: z.string(), + caption: z.string().optional() + })), + banner_assignment: z.number().optional(), + card_assignment: z.number().optional() + }).optional() }); type OperatorFormData = z.infer; @@ -57,11 +61,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr website_url: initialData?.website_url || '', founded_year: initialData?.founded_year || undefined, headquarters_location: initialData?.headquarters_location || '', - logo_url: initialData?.logo_url || '', - banner_image_id: initialData?.banner_image_id || '', - banner_image_url: initialData?.banner_image_url || '', - card_image_id: initialData?.card_image_id || '', - card_image_url: initialData?.card_image_url || '' + images: initialData?.images || { uploaded: [] } } }); @@ -181,20 +181,10 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr {/* Images */} - { - if (images.logo_url !== undefined) setValue('logo_url', images.logo_url); - if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id); - if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url); - if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id); - if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url); - }} - showLogo={true} + setValue('images', images)} entityType="operator" /> diff --git a/src/components/admin/PropertyOwnerForm.tsx b/src/components/admin/PropertyOwnerForm.tsx index f2924a8d..bd1986d0 100644 --- a/src/components/admin/PropertyOwnerForm.tsx +++ b/src/components/admin/PropertyOwnerForm.tsx @@ -12,7 +12,7 @@ import { Building2, Save, X } from 'lucide-react'; import { Combobox } from '@/components/ui/combobox'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; -import { EntityImageUploader } from '@/components/upload/EntityImageUploader'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; const propertyOwnerSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -22,11 +22,15 @@ const propertyOwnerSchema = z.object({ website_url: z.string().url().optional().or(z.literal('')), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), headquarters_location: z.string().optional(), - logo_url: z.string().optional(), - banner_image_id: z.string().optional(), - banner_image_url: z.string().optional(), - card_image_id: z.string().optional(), - card_image_url: z.string().optional() + images: z.object({ + uploaded: z.array(z.object({ + url: z.string(), + cloudflare_id: z.string(), + caption: z.string().optional() + })), + banner_assignment: z.number().optional(), + card_assignment: z.number().optional() + }).optional() }); type PropertyOwnerFormData = z.infer; @@ -57,11 +61,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO website_url: initialData?.website_url || '', founded_year: initialData?.founded_year || undefined, headquarters_location: initialData?.headquarters_location || '', - logo_url: initialData?.logo_url || '', - banner_image_id: initialData?.banner_image_id || '', - banner_image_url: initialData?.banner_image_url || '', - card_image_id: initialData?.card_image_id || '', - card_image_url: initialData?.card_image_url || '' + images: initialData?.images || { uploaded: [] } } }); @@ -181,20 +181,10 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO {/* Images */} - { - if (images.logo_url !== undefined) setValue('logo_url', images.logo_url); - if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id); - if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url); - if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id); - if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url); - }} - showLogo={true} + setValue('images', images)} entityType="property_owner" /> diff --git a/src/components/admin/RideModelForm.tsx b/src/components/admin/RideModelForm.tsx index 0b76bfb8..400b8f34 100644 --- a/src/components/admin/RideModelForm.tsx +++ b/src/components/admin/RideModelForm.tsx @@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge'; import { SlugField } from '@/components/ui/slug-field'; import { Layers, Save, X } from 'lucide-react'; import { useUserRole } from '@/hooks/useUserRole'; -import { EntityImageUploader } from '@/components/upload/EntityImageUploader'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; const rideModelSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -20,10 +20,15 @@ const rideModelSchema = z.object({ ride_type: z.string().min(1, 'Ride type is required'), description: z.string().optional(), technical_specs: z.string().optional(), - banner_image_id: z.string().optional(), - banner_image_url: z.string().optional(), - card_image_id: z.string().optional(), - card_image_url: z.string().optional() + images: z.object({ + uploaded: z.array(z.object({ + url: z.string(), + cloudflare_id: z.string(), + caption: z.string().optional() + })), + banner_assignment: z.number().optional(), + card_assignment: z.number().optional() + }).optional() }); type RideModelFormData = z.infer; @@ -69,10 +74,7 @@ export function RideModelForm({ ride_type: initialData?.ride_type || '', description: initialData?.description || '', technical_specs: initialData?.technical_specs || '', - banner_image_id: initialData?.banner_image_id || '', - banner_image_url: initialData?.banner_image_url || '', - card_image_id: initialData?.card_image_id || '', - card_image_url: initialData?.card_image_url || '' + images: initialData?.images || { uploaded: [] } } }); @@ -176,18 +178,10 @@ export function RideModelForm({ {/* Images */} - { - if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id); - if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url); - if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id); - if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url); - }} - showLogo={false} + setValue('images', images)} entityType="ride_model" /> diff --git a/src/components/upload/EntityMultiImageUploader.tsx b/src/components/upload/EntityMultiImageUploader.tsx new file mode 100644 index 00000000..1befc8b2 --- /dev/null +++ b/src/components/upload/EntityMultiImageUploader.tsx @@ -0,0 +1,236 @@ +import { useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { ImageIcon, Star, CreditCard, X } from 'lucide-react'; +import { UppyPhotoUpload } from './UppyPhotoUpload'; + +export interface UploadedImage { + url: string; + cloudflare_id: string; + caption?: string; +} + +export interface ImageAssignments { + uploaded: UploadedImage[]; + banner_assignment?: number; + card_assignment?: number; +} + +interface EntityMultiImageUploaderProps { + mode: 'create' | 'edit'; + value: ImageAssignments; + onChange: (assignments: ImageAssignments) => void; + entityId?: string; + entityType?: string; +} + +export function EntityMultiImageUploader({ + mode, + value, + onChange, + entityType = 'entity' +}: EntityMultiImageUploaderProps) { + const [showUploader, setShowUploader] = useState(false); + + const maxImages = mode === 'create' ? 5 : 3; + const canUploadMore = value.uploaded.length < maxImages; + + const handleUploadComplete = (urls: string[]) => { + const newImages: UploadedImage[] = urls.map(url => ({ + url, + cloudflare_id: url.split('/').pop()?.split('?')[0] || '' + })); + + const updatedImages = [...value.uploaded, ...newImages].slice(0, maxImages); + + // Auto-assign first image as banner and second as card if not assigned + const newAssignments: ImageAssignments = { + uploaded: updatedImages, + banner_assignment: value.banner_assignment ?? (updatedImages.length > 0 ? 0 : undefined), + card_assignment: value.card_assignment ?? (updatedImages.length > 1 ? 1 : undefined) + }; + + onChange(newAssignments); + setShowUploader(false); + }; + + const handleAssignRole = (index: number, role: 'banner' | 'card') => { + onChange({ + ...value, + [role === 'banner' ? 'banner_assignment' : 'card_assignment']: index + }); + }; + + const handleRemoveImage = (index: number) => { + const newUploaded = value.uploaded.filter((_, i) => i !== index); + + // Adjust assignments after removal + let newBannerAssignment = value.banner_assignment; + let newCardAssignment = value.card_assignment; + + if (value.banner_assignment === index) { + newBannerAssignment = undefined; + } else if (value.banner_assignment !== undefined && value.banner_assignment > index) { + newBannerAssignment = value.banner_assignment - 1; + } + + if (value.card_assignment === index) { + newCardAssignment = undefined; + } else if (value.card_assignment !== undefined && value.card_assignment > index) { + newCardAssignment = value.card_assignment - 1; + } + + onChange({ + uploaded: newUploaded, + banner_assignment: newBannerAssignment, + card_assignment: newCardAssignment + }); + }; + + const renderImageCard = (image: UploadedImage, index: number) => { + const isBanner = value.banner_assignment === index; + const isCard = value.card_assignment === index; + + return ( + +
+ {`Upload + + {/* Hover overlay with actions */} +
+
+ + +
+ +
+ + {/* Role badges */} +
+ {isBanner && ( + + + Banner + + )} + {isCard && ( + + + Card + + )} +
+
+
+ ); + }; + + return ( +
+
+
+ + + {value.uploaded.length} / {maxImages} images + + {mode === 'create' && ( + + Upload up to 5, assign banner & card roles + + )} +
+
+ + {/* Upload section */} + {canUploadMore && ( +
+ {!showUploader ? ( + + ) : ( + +
+
+ + +
+ +
+
+ )} +
+ )} + + {/* Image grid */} + {value.uploaded.length > 0 && ( +
+ {value.uploaded.map((image, index) => renderImageCard(image, index))} +
+ )} + + {/* Help text */} + {value.uploaded.length === 0 && ( +
+ +

Upload images and assign which should be used as banner and card images

+
+ )} + + {value.uploaded.length > 0 && ( +
+

• Click images to assign as Banner (header) or Card (thumbnail)

+

• Banner and Card can be the same image or different images

+
+ )} +
+ ); +} diff --git a/src/lib/companyHelpers.ts b/src/lib/companyHelpers.ts index 9db79a91..960c1bad 100644 --- a/src/lib/companyHelpers.ts +++ b/src/lib/companyHelpers.ts @@ -1,4 +1,5 @@ import { supabase } from '@/integrations/supabase/client'; +import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; export interface CompanyFormData { name: string; @@ -8,6 +9,7 @@ export interface CompanyFormData { website_url?: string; founded_year?: number; headquarters_location?: string; + images?: ImageAssignments; } export async function submitCompanyCreation( @@ -21,7 +23,13 @@ export async function submitCompanyCreation( const { data: newCompany, error } = await supabase .from('companies') .insert({ - ...data, + name: data.name, + slug: data.slug, + description: data.description, + person_type: data.person_type, + website_url: data.website_url, + founded_year: data.founded_year, + headquarters_location: data.headquarters_location, company_type: companyType }) .select() @@ -33,15 +41,22 @@ export async function submitCompanyCreation( // Regular users submit for moderation const { error } = await supabase .from('content_submissions') - .insert({ + .insert([{ user_id: userId, submission_type: 'company_create', content: { - ...data, - company_type: companyType - }, + name: data.name, + slug: data.slug, + description: data.description, + person_type: data.person_type, + website_url: data.website_url, + founded_year: data.founded_year, + headquarters_location: data.headquarters_location, + company_type: companyType, + images: data.images as any // Include image assignments in submission + } as any, status: 'pending' - }); + }]); if (error) throw error; return { company: null, submitted: true }; @@ -59,7 +74,13 @@ export async function submitCompanyUpdate( const { error } = await supabase .from('companies') .update({ - ...data, + name: data.name, + slug: data.slug, + description: data.description, + person_type: data.person_type, + website_url: data.website_url, + founded_year: data.founded_year, + headquarters_location: data.headquarters_location, updated_at: new Date().toISOString() }) .eq('id', companyId); @@ -70,15 +91,22 @@ export async function submitCompanyUpdate( // Regular users submit for moderation const { error } = await supabase .from('content_submissions') - .insert({ + .insert([{ user_id: userId, submission_type: 'company_edit', content: { company_id: companyId, - ...data - }, + name: data.name, + slug: data.slug, + description: data.description, + person_type: data.person_type, + website_url: data.website_url, + founded_year: data.founded_year, + headquarters_location: data.headquarters_location, + images: data.images as any // Include image role assignments in submission + } as any, status: 'pending' - }); + }]); if (error) throw error; return { submitted: true };