Refactor RideForm image handling

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 22:44:23 +00:00
parent 2c6acf21fe
commit af00cefc1c
6 changed files with 112 additions and 28 deletions

View File

@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { UppyPhotoUpload } from '@/components/upload/UppyPhotoUpload';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DatePicker } from '@/components/ui/date-picker'; import { DatePicker } from '@/components/ui/date-picker';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
@@ -65,9 +65,21 @@ const rideSchema = z.object({
type RideFormData = z.infer<typeof rideSchema>; type RideFormData = z.infer<typeof rideSchema>;
interface RideFormProps { interface RideFormProps {
onSubmit: (data: RideFormData & { image_url?: string }) => Promise<void>; onSubmit: (data: RideFormData & {
image_url?: string;
banner_image_url?: string;
banner_image_id?: string;
card_image_url?: string;
card_image_id?: string;
}) => Promise<void>;
onCancel?: () => void; onCancel?: () => void;
initialData?: Partial<RideFormData & { image_url?: string }>; initialData?: Partial<RideFormData & {
image_url?: string;
banner_image_url?: string;
banner_image_id?: string;
card_image_url?: string;
card_image_id?: string;
}>;
isEditing?: boolean; isEditing?: boolean;
} }
@@ -116,7 +128,10 @@ const intensityLevels = [
export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) { export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [rideImage, setRideImage] = useState<string>(initialData?.image_url || ''); const [bannerImageUrl, setBannerImageUrl] = useState<string>(initialData?.banner_image_url || '');
const [bannerImageId, setBannerImageId] = useState<string>(initialData?.banner_image_id || '');
const [cardImageUrl, setCardImageUrl] = useState<string>(initialData?.card_image_url || '');
const [cardImageId, setCardImageId] = useState<string>(initialData?.card_image_id || '');
const { preferences } = useUnitPreferences(); const { preferences } = useUnitPreferences();
const measurementSystem = preferences.measurement_system; const measurementSystem = preferences.measurement_system;
@@ -221,7 +236,10 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
drop_height_meters: data.drop_height_meters drop_height_meters: data.drop_height_meters
? convertDistanceToMetric(data.drop_height_meters, measurementSystem) ? convertDistanceToMetric(data.drop_height_meters, measurementSystem)
: undefined, : undefined,
image_url: rideImage || undefined banner_image_url: bannerImageUrl || undefined,
banner_image_id: bannerImageId || undefined,
card_image_url: cardImageUrl || undefined,
card_image_id: cardImageId || undefined
}; };
// Build composite submission if new entities were created // Build composite submission if new entities were created
@@ -715,25 +733,63 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
</div> </div>
</div> </div>
{/* Image */} {/* Images */}
<div className="space-y-2"> <div className="space-y-6">
<Label>Ride Image</Label> <h3 className="text-lg font-semibold">Images</h3>
<PhotoUpload
maxFiles={1} <div className="space-y-2">
variant="default" <Label>Banner Image</Label>
existingPhotos={rideImage ? [rideImage] : []} <UppyPhotoUpload
onUploadComplete={(urls) => setRideImage(urls[0] || '')} maxFiles={1}
onError={(error) => { variant="public"
toast({ onUploadComplete={(urls) => {
title: "Upload Error", if (urls[0]) {
description: error, setBannerImageUrl(urls[0]);
variant: "destructive" // Extract image ID from Cloudflare URL (format: https://imagedelivery.net/<hash>/<id>/<variant>)
}); const idMatch = urls[0].match(/\/([^/]+)\/public$/);
}} if (idMatch) {
/> setBannerImageId(idMatch[1]);
<p className="text-xs text-muted-foreground"> }
High-quality image of the ride (recommended: 800x600px) }
</p> }}
showPreview={true}
/>
{bannerImageUrl && (
<div className="mt-2">
<img src={bannerImageUrl} alt="Banner preview" className="w-full h-32 object-cover rounded" />
</div>
)}
<p className="text-xs text-muted-foreground">
High-resolution banner image for the ride detail page (recommended: 1200x400px)
</p>
</div>
<div className="space-y-2">
<Label>Card Image</Label>
<UppyPhotoUpload
maxFiles={1}
variant="public"
onUploadComplete={(urls) => {
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 && (
<div className="mt-2">
<img src={cardImageUrl} alt="Card preview" className="w-full h-32 object-cover rounded" />
</div>
)}
<p className="text-xs text-muted-foreground">
Square or rectangular image for ride cards and listings (recommended: 400x300px)
</p>
</div>
</div> </div>
{/* Form Actions */} {/* Form Actions */}

View File

@@ -53,9 +53,9 @@ export function RideCard({ ride, showParkName = true, className }: RideCardProps
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
{/* Image/Icon Section */} {/* Image/Icon Section */}
<div className="aspect-video bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 flex items-center justify-center relative"> <div className="aspect-video bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 flex items-center justify-center relative">
{ride.image_url ? ( {(ride.card_image_url || ride.image_url) ? (
<img <img
src={ride.image_url} src={ride.card_image_url || ride.image_url}
alt={ride.name} alt={ride.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/> />

View File

@@ -614,7 +614,11 @@ export type Database = {
Row: { Row: {
age_requirement: number | null age_requirement: number | null
average_rating: number | null average_rating: number | null
banner_image_id: string | null
banner_image_url: string | null
capacity_per_hour: number | null capacity_per_hour: number | null
card_image_id: string | null
card_image_url: string | null
category: string category: string
closing_date: string | null closing_date: string | null
coaster_stats: Json | null coaster_stats: Json | null
@@ -650,7 +654,11 @@ export type Database = {
Insert: { Insert: {
age_requirement?: number | null age_requirement?: number | null
average_rating?: number | null average_rating?: number | null
banner_image_id?: string | null
banner_image_url?: string | null
capacity_per_hour?: number | null capacity_per_hour?: number | null
card_image_id?: string | null
card_image_url?: string | null
category: string category: string
closing_date?: string | null closing_date?: string | null
coaster_stats?: Json | null coaster_stats?: Json | null
@@ -686,7 +694,11 @@ export type Database = {
Update: { Update: {
age_requirement?: number | null age_requirement?: number | null
average_rating?: number | null average_rating?: number | null
banner_image_id?: string | null
banner_image_url?: string | null
capacity_per_hour?: number | null capacity_per_hour?: number | null
card_image_id?: string | null
card_image_url?: string | null
category?: string category?: string
closing_date?: string | null closing_date?: string | null
coaster_stats?: Json | null coaster_stats?: Json | null

View File

@@ -168,9 +168,9 @@ export default function RideDetail() {
{/* Hero Section */} {/* Hero Section */}
<div className="relative mb-8"> <div className="relative mb-8">
<div className="aspect-[21/9] bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 rounded-lg overflow-hidden relative"> <div className="aspect-[21/9] bg-gradient-to-br from-primary/20 via-secondary/20 to-accent/20 rounded-lg overflow-hidden relative">
{ride.image_url ? ( {(ride.banner_image_url || ride.card_image_url || ride.image_url) ? (
<img <img
src={ride.image_url} src={ride.banner_image_url || ride.card_image_url || ride.image_url}
alt={ride.name} alt={ride.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />

View File

@@ -88,6 +88,10 @@ export interface Ride {
average_rating: number; average_rating: number;
review_count: number; review_count: number;
image_url?: string; image_url?: string;
banner_image_url?: string;
banner_image_id?: string;
card_image_url?: string;
card_image_id?: string;
// New roller coaster specific fields // New roller coaster specific fields
coaster_type?: string; coaster_type?: string;
seating_type?: string; seating_type?: string;

View File

@@ -0,0 +1,12 @@
-- Add banner and card image fields to rides table
ALTER TABLE public.rides
ADD COLUMN IF NOT EXISTS banner_image_url TEXT,
ADD COLUMN IF NOT EXISTS banner_image_id TEXT,
ADD COLUMN IF NOT EXISTS card_image_url TEXT,
ADD COLUMN IF NOT EXISTS card_image_id TEXT;
-- Add comment to document the fields
COMMENT ON COLUMN public.rides.banner_image_url IS 'Full Cloudflare URL for banner image displayed on ride detail page';
COMMENT ON COLUMN public.rides.banner_image_id IS 'Cloudflare image ID for banner';
COMMENT ON COLUMN public.rides.card_image_url IS 'Full Cloudflare URL for card image displayed in listings';
COMMENT ON COLUMN public.rides.card_image_id IS 'Cloudflare image ID for card';