From f57ae0d3cea9961a6d7400635cde983e64db99b4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:09:51 +0000 Subject: [PATCH] Refactor: Implement Cloudflare Image Variants --- src/components/designers/DesignerCard.tsx | 6 +- .../manufacturers/ManufacturerCard.tsx | 14 ++++- src/components/operators/OperatorCard.tsx | 14 ++++- src/components/park-owners/ParkOwnerCard.tsx | 14 ++++- src/components/parks/ParkCard.tsx | 11 +++- src/components/rides/RideCard.tsx | 9 ++- src/components/rides/RideModelCard.tsx | 9 ++- src/components/upload/EntityImageUploader.tsx | 11 +++- src/components/upload/UppyPhotoUpload.tsx | 3 +- src/lib/cloudflareImageUtils.ts | 61 +++++++++++++++++++ src/lib/imageUploadHelper.ts | 4 +- src/pages/DesignerDetail.tsx | 18 ++++-- src/pages/ManufacturerDetail.tsx | 18 ++++-- src/pages/OperatorDetail.tsx | 18 ++++-- src/pages/ParkDetail.tsx | 18 ++++-- src/pages/PropertyOwnerDetail.tsx | 18 ++++-- src/pages/RideDetail.tsx | 18 ++++-- 17 files changed, 216 insertions(+), 48 deletions(-) create mode 100644 src/lib/cloudflareImageUtils.ts diff --git a/src/components/designers/DesignerCard.tsx b/src/components/designers/DesignerCard.tsx index 4294aa60..68d4f570 100644 --- a/src/components/designers/DesignerCard.tsx +++ b/src/components/designers/DesignerCard.tsx @@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge'; import { Star, MapPin, Ruler, FerrisWheel } from 'lucide-react'; import { Company } from '@/types/database'; import { useNavigate } from 'react-router-dom'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface DesignerCardProps { company: Company; @@ -38,11 +39,12 @@ export function DesignerCard({ company }: DesignerCardProps) { {/* Logo or Icon */}
- {company.logo_url ? ( + {(company.logo_url || (company as any).logo_image_id) ? ( {`${company.name} ) : (
diff --git a/src/components/manufacturers/ManufacturerCard.tsx b/src/components/manufacturers/ManufacturerCard.tsx index 0d9edbca..335c50c8 100644 --- a/src/components/manufacturers/ManufacturerCard.tsx +++ b/src/components/manufacturers/ManufacturerCard.tsx @@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Company } from '@/types/database'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface ManufacturerCardProps { company: Company; @@ -42,9 +43,15 @@ export function ManufacturerCard({ company }: ManufacturerCardProps) {
{(company.card_image_url || company.card_image_id) ? ( {company.name} ) : ( <> @@ -59,12 +66,13 @@ export function ManufacturerCard({ company }: ManufacturerCardProps) { {/* Logo Display */}
- {company.logo_url ? ( + {(company.logo_url || (company as any).logo_image_id) ? (
{`${company.name}
) : ( diff --git a/src/components/operators/OperatorCard.tsx b/src/components/operators/OperatorCard.tsx index a6a5c969..5b17a1c9 100644 --- a/src/components/operators/OperatorCard.tsx +++ b/src/components/operators/OperatorCard.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Building, Star, MapPin } from 'lucide-react'; import { Company } from '@/types/database'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface OperatorCardProps { company: Company; @@ -29,9 +30,15 @@ const OperatorCard = ({ company }: OperatorCardProps) => {
{(company.card_image_url || company.card_image_id) ? ( {company.name} ) : ( <> @@ -46,12 +53,13 @@ const OperatorCard = ({ company }: OperatorCardProps) => { {/* Logo Display */}
- {company.logo_url ? ( + {(company.logo_url || (company as any).logo_image_id) ? (
{`${company.name}
) : ( diff --git a/src/components/park-owners/ParkOwnerCard.tsx b/src/components/park-owners/ParkOwnerCard.tsx index 64eec2b5..36121e04 100644 --- a/src/components/park-owners/ParkOwnerCard.tsx +++ b/src/components/park-owners/ParkOwnerCard.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Building2, Star, MapPin } from 'lucide-react'; import { Company } from '@/types/database'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface ParkOwnerCardProps { company: Company; @@ -29,9 +30,15 @@ const ParkOwnerCard = ({ company }: ParkOwnerCardProps) => {
{(company.card_image_url || company.card_image_id) ? ( {company.name} ) : ( <> @@ -46,12 +53,13 @@ const ParkOwnerCard = ({ company }: ParkOwnerCardProps) => { {/* Logo Display */}
- {company.logo_url ? ( + {(company.logo_url || (company as any).logo_image_id) ? (
{`${company.name}
) : ( diff --git a/src/components/parks/ParkCard.tsx b/src/components/parks/ParkCard.tsx index e4c0e051..20c526d8 100644 --- a/src/components/parks/ParkCard.tsx +++ b/src/components/parks/ParkCard.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Park } from '@/types/database'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface ParkCardProps { park: Park; @@ -48,9 +49,15 @@ export function ParkCard({ park }: ParkCardProps) {
{(park.card_image_url || park.card_image_id) ? ( {park.name} ) : (
diff --git a/src/components/rides/RideCard.tsx b/src/components/rides/RideCard.tsx index 91bd3078..56ba76d5 100644 --- a/src/components/rides/RideCard.tsx +++ b/src/components/rides/RideCard.tsx @@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge'; import { Star, MapPin, Clock, Zap, FerrisWheel, Waves, Theater, Train } from 'lucide-react'; import { MeasurementDisplay } from '@/components/ui/measurement-display'; import { Ride } from '@/types/database'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface RideCardProps { ride: Ride; @@ -57,9 +58,15 @@ export function RideCard({ ride, showParkName = true, className, parkSlug }: Rid
{(ride.card_image_url || ride.card_image_id || ride.image_url) ? ( {ride.name} ) : (
diff --git a/src/components/rides/RideModelCard.tsx b/src/components/rides/RideModelCard.tsx index 802bc105..77192770 100644 --- a/src/components/rides/RideModelCard.tsx +++ b/src/components/rides/RideModelCard.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'; import { FerrisWheel } from 'lucide-react'; import { RideModel } from '@/types/database'; import { useNavigate } from 'react-router-dom'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; interface RideModelCardProps { model: RideModel; @@ -45,9 +46,15 @@ export function RideModelCard({ model, manufacturerSlug }: RideModelCardProps) { > {(cardImageUrl || cardImageId) ? ( {model.name} ) : (
diff --git a/src/components/upload/EntityImageUploader.tsx b/src/components/upload/EntityImageUploader.tsx index b5cf357f..99087060 100644 --- a/src/components/upload/EntityImageUploader.tsx +++ b/src/components/upload/EntityImageUploader.tsx @@ -5,6 +5,7 @@ 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'; +import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; export type ImageType = 'logo' | 'banner' | 'card'; @@ -140,7 +141,15 @@ export function EntityImageUploader({ {hasImage ? (
{`${spec.label} diff --git a/src/components/upload/UppyPhotoUpload.tsx b/src/components/upload/UppyPhotoUpload.tsx index b92a0879..e57aead3 100644 --- a/src/components/upload/UppyPhotoUpload.tsx +++ b/src/components/upload/UppyPhotoUpload.tsx @@ -142,7 +142,8 @@ export function UppyPhotoUpload({ if (statusResponse.ok) { const status: UploadSuccessResponse = await statusResponse.json(); if (status.uploaded && status.urls) { - return status.urls.public; + const CLOUDFLARE_ACCOUNT_HASH = import.meta.env.VITE_CLOUDFLARE_ACCOUNT_HASH; + return `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${cloudflareId}/public`; } } diff --git a/src/lib/cloudflareImageUtils.ts b/src/lib/cloudflareImageUtils.ts new file mode 100644 index 00000000..2e7a12c0 --- /dev/null +++ b/src/lib/cloudflareImageUtils.ts @@ -0,0 +1,61 @@ +/** + * Cloudflare Images variant utilities + * Generates properly formatted URLs for Cloudflare Image variants + */ + +export type CloudflareVariant = + | 'avatar' + | 'banner' + | 'bannermobile' + | 'card' + | 'cardthumb' + | 'logo' + | 'public'; + +const CLOUDFLARE_ACCOUNT_HASH = import.meta.env.VITE_CLOUDFLARE_ACCOUNT_HASH; + +/** + * Build a Cloudflare image URL with specified variant + */ +export function getCloudflareImageUrl( + imageId: string | undefined, + variant: CloudflareVariant = 'public' +): string | undefined { + if (!imageId) return undefined; + return `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${imageId}/${variant}`; +} + +/** + * Generate responsive image srcset for card images + * Useful for elements + */ +export function getCloudflareImageSrcSet(imageId: string | undefined): string | undefined { + if (!imageId) return undefined; + + return [ + `${getCloudflareImageUrl(imageId, 'cardthumb')} 600w`, + `${getCloudflareImageUrl(imageId, 'card')} 1200w`, + `${getCloudflareImageUrl(imageId, 'public')} 1366w` + ].join(', '); +} + +/** + * Get responsive banner URLs for mobile and desktop + */ +export function getBannerUrls(imageId: string | undefined) { + if (!imageId) return { mobile: undefined, desktop: undefined }; + + return { + mobile: getCloudflareImageUrl(imageId, 'bannermobile'), + desktop: getCloudflareImageUrl(imageId, 'banner') + }; +} + +/** + * Extract Cloudflare image ID from various URL formats + */ +export function extractCloudflareImageId(url: string): string | null { + // Match imagedelivery.net URLs + const match = url.match(/imagedelivery\.net\/[^\/]+\/([a-f0-9-]+)\//i); + return match ? match[1] : null; +} diff --git a/src/lib/imageUploadHelper.ts b/src/lib/imageUploadHelper.ts index fa97775d..23c2936d 100644 --- a/src/lib/imageUploadHelper.ts +++ b/src/lib/imageUploadHelper.ts @@ -60,9 +60,11 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise
{(designer.banner_image_url || designer.banner_image_id) ? ( - {designer.name} + + + {designer.name} + ) : designer.logo_url ? (
{(manufacturer.banner_image_url || manufacturer.banner_image_id) ? ( - {manufacturer.name} + + + {manufacturer.name} + ) : manufacturer.logo_url ? (
{(operator.banner_image_url || operator.banner_image_id) ? ( - {operator.name} + + + {operator.name} + ) : operator.logo_url ? (
{(park.banner_image_url || park.banner_image_id) ? ( - {park.name} + + + {park.name} + ) : (
diff --git a/src/pages/PropertyOwnerDetail.tsx b/src/pages/PropertyOwnerDetail.tsx index 727f6fdb..8234ffc5 100644 --- a/src/pages/PropertyOwnerDetail.tsx +++ b/src/pages/PropertyOwnerDetail.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Header } from '@/components/layout/Header'; +import { getBannerUrls } from '@/lib/cloudflareImageUtils'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; @@ -214,11 +215,18 @@ export default function PropertyOwnerDetail() {
{(owner.banner_image_url || owner.banner_image_id) ? ( - {owner.name} + + + {owner.name} + ) : owner.logo_url ? (
{(ride.banner_image_url || ride.banner_image_id) ? ( - {ride.name} + + + {ride.name} + ) : (