Refactor: Update image upload components

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 18:36:43 +00:00
parent 1c7efe28c1
commit 69ce1a8132
8 changed files with 390 additions and 10 deletions

View File

@@ -12,6 +12,7 @@ import { Ruler, Save, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
const designerSchema = z.object({ const designerSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@@ -20,7 +21,12 @@ const designerSchema = z.object({
person_type: z.enum(['company', 'individual', 'firm', 'organization']), person_type: z.enum(['company', 'individual', 'firm', 'organization']),
website_url: z.string().url().optional().or(z.literal('')), website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
headquarters_location: z.string().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()
}); });
type DesignerFormData = z.infer<typeof designerSchema>; type DesignerFormData = z.infer<typeof designerSchema>;
@@ -50,7 +56,12 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
person_type: initialData?.person_type || 'company', person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '', website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined, founded_year: initialData?.founded_year || undefined,
headquarters_location: initialData?.headquarters_location || '' 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 || ''
} }
}); });
@@ -169,6 +180,24 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
)} )}
</div> </div>
{/* Images */}
<EntityImageUploader
images={{
logo: { url: watch('logo_url') },
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
card: { url: watch('card_image_url'), id: watch('card_image_id') }
}}
onImagesChange={(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}
entityType="designer"
/>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

@@ -12,6 +12,7 @@ import { Building2, Save, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
const manufacturerSchema = z.object({ const manufacturerSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@@ -20,7 +21,12 @@ const manufacturerSchema = z.object({
person_type: z.enum(['company', 'individual', 'firm', 'organization']), person_type: z.enum(['company', 'individual', 'firm', 'organization']),
website_url: z.string().url().optional().or(z.literal('')), website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
headquarters_location: z.string().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()
}); });
type ManufacturerFormData = z.infer<typeof manufacturerSchema>; type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
@@ -50,7 +56,12 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
person_type: initialData?.person_type || 'company', person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '', website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined, founded_year: initialData?.founded_year || undefined,
headquarters_location: initialData?.headquarters_location || '' 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 || ''
} }
}); });
@@ -169,6 +180,24 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
)} )}
</div> </div>
{/* Images */}
<EntityImageUploader
images={{
logo: { url: watch('logo_url') },
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
card: { url: watch('card_image_url'), id: watch('card_image_id') }
}}
onImagesChange={(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}
entityType="manufacturer"
/>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

@@ -12,6 +12,7 @@ import { FerrisWheel, Save, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
const operatorSchema = z.object({ const operatorSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@@ -20,7 +21,12 @@ const operatorSchema = z.object({
person_type: z.enum(['company', 'individual', 'firm', 'organization']), person_type: z.enum(['company', 'individual', 'firm', 'organization']),
website_url: z.string().url().optional().or(z.literal('')), website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
headquarters_location: z.string().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()
}); });
type OperatorFormData = z.infer<typeof operatorSchema>; type OperatorFormData = z.infer<typeof operatorSchema>;
@@ -50,7 +56,12 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
person_type: initialData?.person_type || 'company', person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '', website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined, founded_year: initialData?.founded_year || undefined,
headquarters_location: initialData?.headquarters_location || '' 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 || ''
} }
}); });
@@ -169,6 +180,24 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
)} )}
</div> </div>
{/* Images */}
<EntityImageUploader
images={{
logo: { url: watch('logo_url') },
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
card: { url: watch('card_image_url'), id: watch('card_image_id') }
}}
onImagesChange={(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}
entityType="operator"
/>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

@@ -12,6 +12,7 @@ import { Building2, Save, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
const propertyOwnerSchema = z.object({ const propertyOwnerSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@@ -20,7 +21,12 @@ const propertyOwnerSchema = z.object({
person_type: z.enum(['company', 'individual', 'firm', 'organization']), person_type: z.enum(['company', 'individual', 'firm', 'organization']),
website_url: z.string().url().optional().or(z.literal('')), website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(), founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
headquarters_location: z.string().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()
}); });
type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>; type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>;
@@ -50,7 +56,12 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
person_type: initialData?.person_type || 'company', person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '', website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined, founded_year: initialData?.founded_year || undefined,
headquarters_location: initialData?.headquarters_location || '' 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 || ''
} }
}); });
@@ -169,6 +180,24 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
)} )}
</div> </div>
{/* Images */}
<EntityImageUploader
images={{
logo: { url: watch('logo_url') },
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
card: { url: watch('card_image_url'), id: watch('card_image_id') }
}}
onImagesChange={(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}
entityType="property_owner"
/>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

@@ -11,6 +11,7 @@ import { Badge } from '@/components/ui/badge';
import { SlugField } from '@/components/ui/slug-field'; import { SlugField } from '@/components/ui/slug-field';
import { Layers, Save, X } from 'lucide-react'; import { Layers, Save, X } from 'lucide-react';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
const rideModelSchema = z.object({ const rideModelSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@@ -18,7 +19,11 @@ const rideModelSchema = z.object({
category: z.string().min(1, 'Category is required'), category: z.string().min(1, 'Category is required'),
ride_type: z.string().min(1, 'Ride type is required'), ride_type: z.string().min(1, 'Ride type is required'),
description: z.string().optional(), description: z.string().optional(),
technical_specs: 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()
}); });
type RideModelFormData = z.infer<typeof rideModelSchema>; type RideModelFormData = z.infer<typeof rideModelSchema>;
@@ -63,7 +68,11 @@ export function RideModelForm({
category: initialData?.category || '', category: initialData?.category || '',
ride_type: initialData?.ride_type || '', ride_type: initialData?.ride_type || '',
description: initialData?.description || '', description: initialData?.description || '',
technical_specs: initialData?.technical_specs || '' 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 || ''
} }
}); });
@@ -166,6 +175,22 @@ export function RideModelForm({
</p> </p>
</div> </div>
{/* Images */}
<EntityImageUploader
images={{
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
card: { url: watch('card_image_url'), id: watch('card_image_id') }
}}
onImagesChange={(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}
entityType="ride_model"
/>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

@@ -0,0 +1,202 @@
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 { Image as ImageIcon, ImagePlus, X } from 'lucide-react';
import { UppyPhotoUpload } from './UppyPhotoUpload';
export type ImageType = 'logo' | 'banner' | 'card';
interface ImageSlot {
type: ImageType;
url?: string;
id?: string;
}
interface EntityImageUploaderProps {
images: {
logo?: { url?: string; id?: string };
banner?: { url?: string; id?: string };
card?: { url?: string; id?: string };
};
onImagesChange: (images: {
logo_url?: string;
banner_image_id?: string;
banner_image_url?: string;
card_image_id?: string;
card_image_url?: string;
}) => void;
showLogo?: boolean;
entityType?: string;
}
const IMAGE_SPECS = {
logo: { label: 'Logo', aspect: '1:1', dimensions: '400x400', description: 'Square logo image' },
banner: { label: 'Banner', aspect: '21:9', dimensions: '1920x820', description: 'Wide header image' },
card: { label: 'Card', aspect: '16:9', dimensions: '1200x675', description: 'Preview thumbnail' }
};
export function EntityImageUploader({
images,
onImagesChange,
showLogo = true,
entityType = 'entity'
}: EntityImageUploaderProps) {
const [activeSlot, setActiveSlot] = useState<ImageType | null>(null);
const handleUploadComplete = (type: ImageType, urls: string[]) => {
if (urls.length === 0) return;
const url = urls[0];
const id = url.split('/').pop()?.split('?')[0] || '';
const updates: any = {};
if (type === 'logo') {
updates.logo_url = url;
} else if (type === 'banner') {
updates.banner_image_id = id;
updates.banner_image_url = url;
} else if (type === 'card') {
updates.card_image_id = id;
updates.card_image_url = url;
}
onImagesChange({
logo_url: type === 'logo' ? url : images.logo?.url,
banner_image_id: type === 'banner' ? id : images.banner?.id,
banner_image_url: type === 'banner' ? url : images.banner?.url,
card_image_id: type === 'card' ? id : images.card?.id,
card_image_url: type === 'card' ? url : images.card?.url
});
setActiveSlot(null);
};
const handleRemoveImage = (type: ImageType) => {
const updates: any = {
logo_url: images.logo?.url,
banner_image_id: images.banner?.id,
banner_image_url: images.banner?.url,
card_image_id: images.card?.id,
card_image_url: images.card?.url
};
if (type === 'logo') {
updates.logo_url = undefined;
} else if (type === 'banner') {
updates.banner_image_id = undefined;
updates.banner_image_url = undefined;
} else if (type === 'card') {
updates.card_image_id = undefined;
updates.card_image_url = undefined;
}
onImagesChange(updates);
};
const renderImageSlot = (type: ImageType) => {
if (type === 'logo' && !showLogo) return null;
const spec = IMAGE_SPECS[type];
const currentImage = type === 'logo' ? images.logo : type === 'banner' ? images.banner : images.card;
const hasImage = currentImage?.url;
if (activeSlot === type) {
return (
<Card key={type} className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label className="text-base font-semibold">{spec.label}</Label>
<Badge variant="outline" className="text-xs">
{spec.aspect} {spec.dimensions}
</Badge>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setActiveSlot(null)}
>
<X className="w-4 h-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">{spec.description}</p>
<UppyPhotoUpload
onUploadComplete={(urls) => handleUploadComplete(type, urls)}
maxFiles={1}
variant="compact"
allowedFileTypes={['image/jpeg', 'image/jpg', 'image/png', 'image/webp']}
/>
</div>
</Card>
);
}
return (
<Card key={type} className="overflow-hidden">
{hasImage ? (
<div className="relative aspect-[16/9] bg-muted">
<img
src={currentImage.url}
alt={`${spec.label} preview`}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => setActiveSlot(type)}
>
<ImagePlus className="w-4 h-4 mr-2" />
Replace
</Button>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => handleRemoveImage(type)}
>
<X className="w-4 h-4 mr-2" />
Remove
</Button>
</div>
<Badge className="absolute top-2 left-2">{spec.label}</Badge>
</div>
) : (
<button
type="button"
onClick={() => setActiveSlot(type)}
className="w-full aspect-[16/9] flex flex-col items-center justify-center gap-2 bg-muted hover:bg-muted/80 transition-colors"
>
<ImageIcon className="w-8 h-8 text-muted-foreground" />
<div className="text-center">
<p className="text-sm font-medium">{spec.label}</p>
<p className="text-xs text-muted-foreground">{spec.dimensions}</p>
</div>
</button>
)}
</Card>
);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Label className="text-base">Images</Label>
<Badge variant="secondary" className="text-xs">
Recommended formats: JPG, PNG, WebP
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{showLogo && renderImageSlot('logo')}
{renderImageSlot('banner')}
{renderImageSlot('card')}
</div>
</div>
);
}

View File

@@ -77,6 +77,10 @@ export type Database = {
companies: { companies: {
Row: { Row: {
average_rating: number | null average_rating: number | null
banner_image_id: string | null
banner_image_url: string | null
card_image_id: string | null
card_image_url: string | null
company_type: string company_type: string
created_at: string created_at: string
description: string | null description: string | null
@@ -93,6 +97,10 @@ export type Database = {
} }
Insert: { Insert: {
average_rating?: number | null average_rating?: number | null
banner_image_id?: string | null
banner_image_url?: string | null
card_image_id?: string | null
card_image_url?: string | null
company_type: string company_type: string
created_at?: string created_at?: string
description?: string | null description?: string | null
@@ -109,6 +117,10 @@ export type Database = {
} }
Update: { Update: {
average_rating?: number | null average_rating?: number | null
banner_image_id?: string | null
banner_image_url?: string | null
card_image_id?: string | null
card_image_url?: string | null
company_type?: string company_type?: string
created_at?: string created_at?: string
description?: string | null description?: string | null
@@ -806,6 +818,10 @@ export type Database = {
} }
ride_models: { ride_models: {
Row: { Row: {
banner_image_id: string | null
banner_image_url: string | null
card_image_id: string | null
card_image_url: string | null
category: string category: string
created_at: string created_at: string
description: string | null description: string | null
@@ -818,6 +834,10 @@ export type Database = {
updated_at: string updated_at: string
} }
Insert: { Insert: {
banner_image_id?: string | null
banner_image_url?: string | null
card_image_id?: string | null
card_image_url?: string | null
category: string category: string
created_at?: string created_at?: string
description?: string | null description?: string | null
@@ -830,6 +850,10 @@ export type Database = {
updated_at?: string updated_at?: string
} }
Update: { Update: {
banner_image_id?: string | null
banner_image_url?: string | null
card_image_id?: string | null
card_image_url?: string | null
category?: string category?: string
created_at?: string created_at?: string
description?: string | null description?: string | null

View File

@@ -0,0 +1,13 @@
-- Add image fields to companies table
ALTER TABLE public.companies
ADD COLUMN IF NOT EXISTS banner_image_id TEXT,
ADD COLUMN IF NOT EXISTS banner_image_url TEXT,
ADD COLUMN IF NOT EXISTS card_image_id TEXT,
ADD COLUMN IF NOT EXISTS card_image_url TEXT;
-- Add image fields to ride_models table
ALTER TABLE public.ride_models
ADD COLUMN IF NOT EXISTS banner_image_id TEXT,
ADD COLUMN IF NOT EXISTS banner_image_url TEXT,
ADD COLUMN IF NOT EXISTS card_image_id TEXT,
ADD COLUMN IF NOT EXISTS card_image_url TEXT;