mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 01:11:13 -05:00
Refactor RideForm image handling
This commit is contained in:
@@ -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 */}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user