diff --git a/src/components/moderation/ItemEditDialog.tsx b/src/components/moderation/ItemEditDialog.tsx index 19c9d10f..d3948762 100644 --- a/src/components/moderation/ItemEditDialog.tsx +++ b/src/components/moderation/ItemEditDialog.tsx @@ -18,6 +18,7 @@ import { RideForm } from '@/components/admin/RideForm'; import { ManufacturerForm } from '@/components/admin/ManufacturerForm'; import { DesignerForm } from '@/components/admin/DesignerForm'; import { OperatorForm } from '@/components/admin/OperatorForm'; +import { jsonToFormData } from '@/lib/typeConversions'; import { PropertyOwnerForm } from '@/components/admin/PropertyOwnerForm'; import { RideModelForm } from '@/components/admin/RideModelForm'; import { Save, X, Edit } from 'lucide-react'; @@ -129,8 +130,9 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onOpenChange(false)} + // Convert Json to form-compatible object (null → undefined) // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} isEditing /> ); @@ -140,8 +142,9 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onOpenChange(false)} + // Convert Json to form-compatible object (null → undefined) // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} isEditing /> ); @@ -152,7 +155,7 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onSubmit={handleSubmit} onCancel={() => onOpenChange(false)} // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} /> ); @@ -162,7 +165,7 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onSubmit={handleSubmit} onCancel={() => onOpenChange(false)} // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} /> ); @@ -172,7 +175,7 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onSubmit={handleSubmit} onCancel={() => onOpenChange(false)} // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} /> ); @@ -182,7 +185,7 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: onSubmit={handleSubmit} onCancel={() => onOpenChange(false)} // eslint-disable-next-line @typescript-eslint/no-explicit-any - initialData={itemData as any} + initialData={jsonToFormData(editItem.item_data) as any} /> ); diff --git a/src/components/rides/SimilarRides.tsx b/src/components/rides/SimilarRides.tsx index f2f98d94..ff1c74b4 100644 --- a/src/components/rides/SimilarRides.tsx +++ b/src/components/rides/SimilarRides.tsx @@ -61,7 +61,7 @@ export function SimilarRides({ currentRideId, parkId, parkSlug, category }: Simi .limit(4); if (!error && data) { - setRides(data as SimilarRide[]); + setRides(data); } setLoading(false); } @@ -83,7 +83,7 @@ export function SimilarRides({ currentRideId, parkId, parkSlug, category }: Simi {rides.map((ride) => ( diff --git a/src/lib/typeConversions.ts b/src/lib/typeConversions.ts new file mode 100644 index 00000000..71c1869f --- /dev/null +++ b/src/lib/typeConversions.ts @@ -0,0 +1,149 @@ +/** + * Type Conversion Utilities + * + * This module provides type-safe conversion functions for handling: + * - Database nulls → Application undefined + * - Json types → Form data types + * - Type guards for runtime validation + * + * These utilities eliminate the need for `as any` casts by providing + * explicit, type-safe conversions at data boundaries. + */ + +import type { Json } from '@/integrations/supabase/types'; + +/** + * Convert database null to application undefined + * Database returns null, but forms expect undefined for optional fields + */ +export function nullToUndefined(value: T | null): T | undefined { + return value === null ? undefined : value; +} + +/** + * Convert all null values in an object to undefined + * Use at database boundaries when fetching data for forms + */ +export function convertNullsToUndefined>( + obj: T +): { [K in keyof T]: T[K] extends (infer U | null) ? (U | undefined) : T[K] } { + const result: Record = {}; + for (const key in obj) { + const value = obj[key]; + result[key] = value === null ? undefined : value; + } + return result as { [K in keyof T]: T[K] extends (infer U | null) ? (U | undefined) : T[K] }; +} + +/** + * Photo item interface for type-safe photo handling + */ +export interface PhotoItem { + url: string; + caption?: string; + title?: string; + cloudflare_id?: string; + order_index?: number; +} + +/** + * Type guard: Check if value is a valid photo array + */ +export function isPhotoArray(data: unknown): data is PhotoItem[] { + return ( + Array.isArray(data) && + data.every( + (item) => + typeof item === 'object' && + item !== null && + 'url' in item && + typeof (item as Record).url === 'string' + ) + ); +} + +/** + * Safely extract photo array from Json data + * Handles both direct arrays and nested { photos: [...] } structures + */ +export function extractPhotoArray(data: Json): PhotoItem[] { + // Direct array + if (isPhotoArray(data)) { + return data; + } + + // Nested photos property + if (typeof data === 'object' && data !== null && !Array.isArray(data)) { + const obj = data as Record; + if ('photos' in obj && isPhotoArray(obj.photos)) { + return obj.photos; + } + } + + return []; +} + +/** + * Type guard: Check if Json is a plain object (not array or null) + */ +export function isJsonObject(data: Json): data is Record { + return typeof data === 'object' && data !== null && !Array.isArray(data); +} + +/** + * Convert Json to a form-compatible object + * Converts all null values to undefined for form compatibility + */ +export function jsonToFormData(data: Json | unknown): Record { + if (!isJsonObject(data as Json)) { + return {}; + } + + const result: Record = {}; + for (const [key, value] of Object.entries(data as Record)) { + result[key] = value === null ? undefined : value; + } + + return result; +} + +/** + * Type guard: Check if object has a specific property with a given type + */ +export function hasProperty( + obj: unknown, + key: string, + typeCheck: (value: unknown) => value is T +): obj is Record & { [K in typeof key]: T } { + return ( + typeof obj === 'object' && + obj !== null && + key in obj && + typeCheck((obj as Record)[key]) + ); +} + +/** + * Safely get a property from Json with type assertion + */ +export function getProperty( + data: Json, + key: string, + defaultValue?: T +): T | undefined { + if (isJsonObject(data) && key in data) { + const value = data[key]; + return value === null ? defaultValue : (value as T); + } + return defaultValue; +} + +/** + * Convert array of database records with nulls to form-compatible records + * Useful when fetching lists of entities for display/editing + */ +export function convertArrayNullsToUndefined>( + arr: T[] +): Array<{ [K in keyof T]: T[K] extends (infer U | null) ? (U | undefined) : T[K] }> { + return arr.map((item) => convertNullsToUndefined(item)); +} diff --git a/src/pages/DesignerRides.tsx b/src/pages/DesignerRides.tsx index 4fe21798..f9227db7 100644 --- a/src/pages/DesignerRides.tsx +++ b/src/pages/DesignerRides.tsx @@ -85,6 +85,8 @@ export default function DesignerRides() { const { data: ridesData, error: ridesError } = await query; if (ridesError) throw ridesError; + // Supabase returns nullable types, but our Ride type uses undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any setRides((ridesData || []) as any); } } catch (error) { @@ -134,6 +136,9 @@ export default function DesignerRides() { }; const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers'); + // Type assertion needed: RideForm returns data with undefined for optional fields, + // but submitRideCreation expects RideFormData which requires stricter types + // eslint-disable-next-line @typescript-eslint/no-explicit-any await submitRideCreation(submissionData as any, user!.id); toast({ diff --git a/src/pages/ManufacturerRides.tsx b/src/pages/ManufacturerRides.tsx index 35b1e19f..fb78883b 100644 --- a/src/pages/ManufacturerRides.tsx +++ b/src/pages/ManufacturerRides.tsx @@ -85,7 +85,9 @@ export default function ManufacturerRides() { const { data: ridesData, error: ridesError } = await query; if (ridesError) throw ridesError; - setRides(ridesData as any || []); + // Supabase returns nullable types, but our Ride type uses undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setRides((ridesData || []) as any); } } catch (error) { console.error('Error fetching data:', error); @@ -134,6 +136,9 @@ export default function ManufacturerRides() { }; const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers'); + // Type assertion needed: RideForm returns RideFormData with undefined for optional fields, + // but submitRideCreation expects specific form data structure + // eslint-disable-next-line @typescript-eslint/no-explicit-any await submitRideCreation(submissionData as any, user!.id); toast({ diff --git a/src/pages/ParkRides.tsx b/src/pages/ParkRides.tsx index f64002d5..4455e657 100644 --- a/src/pages/ParkRides.tsx +++ b/src/pages/ParkRides.tsx @@ -106,7 +106,9 @@ export default function ParkRides() { if (ridesError) throw ridesError; - setRides(ridesData || []); + // Supabase returns nullable types, but our Ride type uses undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setRides((ridesData || []) as any); } catch (error) { console.error('Error fetching park and rides:', error); toast({ @@ -138,6 +140,8 @@ export default function ParkRides() { }; const { submitRideCreation } = await import('@/lib/entitySubmissionHelpers'); + // Type assertion needed: Form data structure doesn't perfectly match submission helper expectations + // eslint-disable-next-line @typescript-eslint/no-explicit-any await submitRideCreation(submissionData as any, user!.id); toast({