diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 32323306..fb4b05e1 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; -import { UppyPhotoUpload } from '@/components/upload/UppyPhotoUpload'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DatePicker } from '@/components/ui/date-picker'; import { SlugField } from '@/components/ui/slug-field'; @@ -31,27 +31,28 @@ const parkSchema = z.object({ phone: z.string().optional(), email: z.string().email().optional().or(z.literal('')), operator_id: z.string().uuid().optional(), - property_owner_id: z.string().uuid().optional() + property_owner_id: z.string().uuid().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 ParkFormData = z.infer; interface ParkFormProps { onSubmit: (data: ParkFormData & { - banner_image_url?: string; - card_image_url?: string; - banner_image_id?: string; - card_image_id?: string; operator_id?: string; property_owner_id?: string; _compositeSubmission?: any; }) => Promise; onCancel?: () => void; initialData?: Partial; @@ -81,10 +82,6 @@ const statusOptions = [ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: ParkFormProps) { const { isModerator } = useUserRole(); const [submitting, setSubmitting] = useState(false); - const [bannerImage, setBannerImage] = useState(initialData?.banner_image_url || ''); - const [cardImage, setCardImage] = useState(initialData?.card_image_url || ''); - const [bannerImageId, setBannerImageId] = useState(initialData?.banner_image_id || ''); - const [cardImageId, setCardImageId] = useState(initialData?.card_image_id || ''); // Operator state const [selectedOperatorId, setSelectedOperatorId] = useState(initialData?.operator_id || ''); @@ -100,12 +97,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: const { operators, loading: operatorsLoading } = useOperators(); const { propertyOwners, loading: ownersLoading } = usePropertyOwners(); - // Extract Cloudflare image ID from URL - const extractImageId = (url: string): string => { - const match = url.match(/\/([a-f0-9-]{36})\//); - return match ? match[1] : ''; - }; - const { register, handleSubmit, @@ -126,7 +117,8 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: phone: initialData?.phone || '', email: initialData?.email || '', operator_id: initialData?.operator_id || undefined, - property_owner_id: initialData?.property_owner_id || undefined + property_owner_id: initialData?.property_owner_id || undefined, + images: { uploaded: [] } } }); @@ -136,13 +128,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: try { // Build composite submission if new entities were created const submissionContent: any = { - park: { - ...data, - banner_image_url: bannerImage || undefined, - card_image_url: cardImage || undefined, - banner_image_id: bannerImageId || undefined, - card_image_id: cardImageId || undefined - }, + park: data, }; // Add new operator if created @@ -159,10 +145,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: await onSubmit({ ...data, - banner_image_url: bannerImage || undefined, - card_image_url: cardImage || undefined, - banner_image_id: bannerImageId || undefined, - card_image_id: cardImageId || undefined, operator_id: tempNewOperator ? undefined : (selectedOperatorId || undefined), property_owner_id: tempNewPropertyOwner ? undefined : (selectedPropertyOwnerId || undefined), _compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined @@ -432,73 +414,12 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: {/* Images */} -
-
- - {bannerImage && ( -
- Current banner -
- )} - { - const url = urls[0]; - if (url) { - setBannerImage(url); - const imageId = extractImageId(url); - if (imageId) { - setBannerImageId(imageId); - } - } - }} - onUploadError={(error) => { - toast({ - title: "Upload Error", - description: error.message, - variant: "destructive" - }); - }} - /> -

- High-resolution banner image for the park detail page (recommended: 1200x400px) -

-
- -
- - {cardImage && ( -
- Current card -
- )} - { - const url = urls[0]; - if (url) { - setCardImage(url); - const imageId = extractImageId(url); - if (imageId) { - setCardImageId(imageId); - } - } - }} - onUploadError={(error) => { - toast({ - title: "Upload Error", - description: error.message, - variant: "destructive" - }); - }} - /> -

- Square or rectangular image for park cards and listings (recommended: 400x300px) -

-
-
+ setValue('images', images)} + entityType="park" + /> {/* Form Actions */}
diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index 3e5989d8..64920a72 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; -import { UppyPhotoUpload } from '@/components/upload/UppyPhotoUpload'; +import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DatePicker } from '@/components/ui/date-picker'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -61,27 +61,25 @@ const rideSchema = z.object({ technical_specs: z.string().optional(), // Manufacturer and model manufacturer_id: z.string().uuid().optional(), - ride_model_id: z.string().uuid().optional() + ride_model_id: z.string().uuid().optional(), + // Images + 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 RideFormData = z.infer; interface RideFormProps { - onSubmit: (data: RideFormData & { - image_url?: string; - banner_image_url?: string; - banner_image_id?: string; - card_image_url?: string; - card_image_id?: string; - }) => Promise; + onSubmit: (data: RideFormData) => Promise; onCancel?: () => void; - initialData?: Partial; + initialData?: Partial; isEditing?: boolean; } @@ -131,10 +129,6 @@ const intensityLevels = [ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) { const { isModerator } = useUserRole(); const [submitting, setSubmitting] = useState(false); - const [bannerImageUrl, setBannerImageUrl] = useState(initialData?.banner_image_url || ''); - const [bannerImageId, setBannerImageId] = useState(initialData?.banner_image_id || ''); - const [cardImageUrl, setCardImageUrl] = useState(initialData?.card_image_url || ''); - const [cardImageId, setCardImageId] = useState(initialData?.card_image_id || ''); const { preferences } = useUnitPreferences(); const measurementSystem = preferences.measurement_system; @@ -197,7 +191,8 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: coaster_stats: initialData?.coaster_stats || '', technical_specs: initialData?.technical_specs || '', manufacturer_id: initialData?.manufacturer_id || undefined, - ride_model_id: initialData?.ride_model_id || undefined + ride_model_id: initialData?.ride_model_id || undefined, + images: { uploaded: [] } } }); @@ -224,11 +219,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: : undefined, drop_height_meters: data.drop_height_meters ? convertDistanceToMetric(data.drop_height_meters, measurementSystem) - : undefined, - banner_image_url: bannerImageUrl || undefined, - banner_image_id: bannerImageId || undefined, - card_image_url: cardImageUrl || undefined, - card_image_id: cardImageId || undefined + : undefined }; // Build composite submission if new entities were created @@ -714,63 +705,12 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
{/* Images */} -
-

Images

- -
- - { - if (urls[0]) { - setBannerImageUrl(urls[0]); - // Extract image ID from Cloudflare URL (format: https://imagedelivery.net///) - const idMatch = urls[0].match(/\/([^/]+)\/public$/); - if (idMatch) { - setBannerImageId(idMatch[1]); - } - } - }} - showPreview={true} - /> - {bannerImageUrl && ( -
- Banner preview -
- )} -

- High-resolution banner image for the ride detail page (recommended: 1200x400px) -

-
- -
- - { - if (urls[0]) { - setCardImageUrl(urls[0]); - // Extract image ID from Cloudflare URL - const idMatch = urls[0].match(/\/([^/]+)\/public$/); - if (idMatch) { - setCardImageId(idMatch[1]); - } - } - }} - showPreview={true} - /> - {cardImageUrl && ( -
- Card preview -
- )} -

- Square or rectangular image for ride cards and listings (recommended: 400x300px) -

-
-
+ setValue('images', images)} + entityType="ride" + /> {/* Form Actions */}
diff --git a/src/components/upload/EntityMultiImageUploader.tsx b/src/components/upload/EntityMultiImageUploader.tsx index 1befc8b2..4cbaa866 100644 --- a/src/components/upload/EntityMultiImageUploader.tsx +++ b/src/components/upload/EntityMultiImageUploader.tsx @@ -202,7 +202,7 @@ export function EntityMultiImageUploader({ onUploadComplete={handleUploadComplete} maxFiles={maxImages - value.uploaded.length} variant="compact" - allowedFileTypes={['image/jpeg', 'image/jpg', 'image/png', 'image/webp']} + allowedFileTypes={['image/*']} />
diff --git a/src/components/upload/UppyPhotoUpload.tsx b/src/components/upload/UppyPhotoUpload.tsx index 63e15ef3..b92a0879 100644 --- a/src/components/upload/UppyPhotoUpload.tsx +++ b/src/components/upload/UppyPhotoUpload.tsx @@ -77,15 +77,20 @@ export function UppyPhotoUpload({ return `File "${file.name}" exceeds ${maxSizeMB}MB limit`; } - const allowedTypes = allowedFileTypes.map(type => - type.replace('*', '').replace('/', '') - ); - - if (!allowedTypes.includes('image') && !allowedFileTypes.includes('image/*')) { - const fileType = file.type.split('/')[0]; - if (!allowedTypes.includes(fileType)) { - return `File type "${file.type}" is not allowed`; + // Check if file type is allowed + // Support both wildcard (image/*) and specific types (image/jpeg, image/png) + const isWildcardMatch = allowedFileTypes.some(type => { + if (type.includes('*')) { + const prefix = type.split('/')[0]; + return file.type.startsWith(prefix + '/'); } + return false; + }); + + const isExactMatch = allowedFileTypes.includes(file.type); + + if (!isWildcardMatch && !isExactMatch) { + return `File type "${file.type}" is not allowed`; } return null; diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index 7540a8b6..b6be337b 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -667,10 +667,6 @@ export default function ParkDetail() { website_url: park?.website_url, phone: park?.phone, email: park?.email, - banner_image_url: park?.banner_image_url, - banner_image_id: park?.banner_image_id, - card_image_url: park?.card_image_url, - card_image_id: park?.card_image_id, operator_id: park?.operator?.id, property_owner_id: park?.property_owner?.id }} diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index 42c9db23..c4650fe2 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -770,10 +770,6 @@ export default function RideDetail() { intensity_level: ride.intensity_level, drop_height_meters: ride.drop_height_meters, max_g_force: ride.max_g_force, - banner_image_url: ride.banner_image_url, - banner_image_id: ride.banner_image_id, - card_image_url: ride.card_image_url, - card_image_id: ride.card_image_id, manufacturer_id: ride.manufacturer?.id, ride_model_id: ride.ride_model?.id }}