diff --git a/src/components/profile/RideCreditsManager.tsx b/src/components/profile/RideCreditsManager.tsx index 3c08eae9..0e7a430e 100644 --- a/src/components/profile/RideCreditsManager.tsx +++ b/src/components/profile/RideCreditsManager.tsx @@ -121,7 +121,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { const { data, error } = await query; if (error) throw error; - let processedData = (data || []) as any[]; + let processedData = data || []; // Sort by name client-side if needed if (sortBy === 'name') { diff --git a/src/hooks/moderation/useEntityCache.ts b/src/hooks/moderation/useEntityCache.ts index 2601b909..a11fa0ae 100644 --- a/src/hooks/moderation/useEntityCache.ts +++ b/src/hooks/moderation/useEntityCache.ts @@ -170,7 +170,7 @@ export function useEntityCache() { // Collect all entity IDs from submissions submissions.forEach(submission => { - const content = submission.content as any; + const content = submission.content; if (content && typeof content === 'object') { if (content.ride_id) rideIds.add(content.ride_id); if (content.park_id) parkIds.add(content.park_id); diff --git a/src/hooks/useCoasterStats.ts b/src/hooks/useCoasterStats.ts new file mode 100644 index 00000000..5a0f9830 --- /dev/null +++ b/src/hooks/useCoasterStats.ts @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; + +export interface CoasterStat { + id: string; + ride_id: string; + stat_name: string; + stat_value: number; + unit?: string | null; + category?: string | null; + description?: string | null; + display_order: number; + created_at: string; +} + +export function useCoasterStats(rideId: string | undefined) { + return useQuery({ + queryKey: ['coaster-stats', rideId], + queryFn: async () => { + if (!rideId) return []; + + const { data, error } = await (supabase as any) + .from('ride_coaster_stats') + .select('*') + .eq('ride_id', rideId) + .order('display_order'); + + if (error) throw error; + + return (data || []).map((stat: any) => ({ + id: stat.id, + ride_id: stat.ride_id, + stat_name: stat.stat_name, + stat_value: stat.stat_value, + unit: stat.unit || null, + category: stat.category || null, + description: stat.description || null, + display_order: stat.display_order, + created_at: stat.created_at, + })) as CoasterStat[]; + }, + enabled: !!rideId + }); +} diff --git a/src/hooks/useEntityVersions.ts b/src/hooks/useEntityVersions.ts index 078e28bd..12bd45b6 100644 --- a/src/hooks/useEntityVersions.ts +++ b/src/hooks/useEntityVersions.ts @@ -41,8 +41,8 @@ export function useEntityVersions(entityType: EntityType, entityId: string) { const versionTable = `${entityType}_versions`; const entityIdCol = `${entityType}_id`; - const { data, error } = await supabase - .from(versionTable as any) + const { data, error } = await (supabase as any) + .from(versionTable) .select(` *, profiles:created_by(username, display_name, avatar_url) @@ -63,7 +63,7 @@ export function useEntityVersions(entityType: EntityType, entityId: string) { return; } - const versionsWithProfiles = (data as any[]).map((v: any) => ({ + const versionsWithProfiles = (data || []).map((v: any) => ({ ...v, profiles: v.profiles || { username: 'Unknown', diff --git a/src/hooks/useTechnicalSpecifications.ts b/src/hooks/useTechnicalSpecifications.ts new file mode 100644 index 00000000..ffd749ba --- /dev/null +++ b/src/hooks/useTechnicalSpecifications.ts @@ -0,0 +1,52 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; + +export interface TechnicalSpecification { + id: string; + entity_type: 'ride' | 'ride_model'; + entity_id: string; + spec_name: string; + spec_value: string; + spec_unit?: string | null; + category?: string | null; + display_order: number; + created_at: string; +} + +export function useTechnicalSpecifications( + entityType: 'ride' | 'ride_model', + entityId: string | undefined +) { + return useQuery({ + queryKey: ['technical-specifications', entityType, entityId], + queryFn: async () => { + if (!entityId) return []; + + const tableName = entityType === 'ride' + ? 'ride_technical_specifications' + : 'ride_model_technical_specifications'; + const idColumn = entityType === 'ride' ? 'ride_id' : 'ride_model_id'; + + const { data, error } = await (supabase as any) + .from(tableName) + .select('*') + .eq(idColumn, entityId) + .order('display_order'); + + if (error) throw error; + + return (data || []).map((spec: any) => ({ + id: spec.id, + entity_type: entityType, + entity_id: entityId, + spec_name: spec.spec_name, + spec_value: spec.spec_value, + spec_unit: spec.spec_unit || null, + category: spec.category || null, + display_order: spec.display_order, + created_at: spec.created_at, + })) as TechnicalSpecification[]; + }, + enabled: !!entityId + }); +} diff --git a/src/lib/runtimeValidation.ts b/src/lib/runtimeValidation.ts index 43c6bfab..06fbeceb 100644 --- a/src/lib/runtimeValidation.ts +++ b/src/lib/runtimeValidation.ts @@ -121,7 +121,7 @@ export const rideModelRuntimeSchema = z.object({ manufacturer_id: z.string().uuid().nullable().optional(), category: z.string(), description: z.string().nullable().optional(), - technical_specs: z.array(z.unknown()).nullable().optional(), + // Note: technical_specs deprecated - use ride_model_technical_specifications table created_at: z.string().optional(), updated_at: z.string().optional(), }).passthrough(); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 6eaaf57d..622790a7 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -19,7 +19,7 @@ import { UserListManager } from '@/components/lists/UserListManager'; import { RideCreditsManager } from '@/components/profile/RideCreditsManager'; import { useUsernameValidation } from '@/hooks/useUsernameValidation'; import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX, FileText, Image } from 'lucide-react'; -import { Profile as ProfileType, ActivityEntry, ReviewActivity, SubmissionActivity, RankingActivity } from '@/types/database'; +import { Profile as ProfileType } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { getErrorMessage } from '@/lib/errorHandler'; @@ -30,6 +30,95 @@ import { UserBlockButton } from '@/components/profile/UserBlockButton'; import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDisplay'; import { useUserRole } from '@/hooks/useUserRole'; +// Activity type definitions +interface SubmissionActivity { + id: string; + type: 'submission'; + submission_type: 'park' | 'ride' | 'photo' | 'company' | 'ride_model'; + status: string; + created_at: string; + content?: { + action?: 'edit' | 'create'; + name?: string; + slug?: string; + entity_slug?: string; + entity_name?: string; + park_slug?: string; + park_name?: string; + company_type?: string; + manufacturer_slug?: string; + description?: string; + }; + photo_preview?: string; + photo_count?: number; + entity_type?: 'park' | 'ride' | 'company'; + entity_id?: string; +} + +interface RankingActivity { + id: string; + type: 'ranking'; + title: string; + description?: string; + list_type: string; + created_at: string; +} + +interface ReviewActivity { + id: string; + type: 'review'; + rating: number; + title?: string; + content?: string; + created_at: string; + moderation_status?: string; + park_id?: string; + ride_id?: string; + parks?: { + name: string; + slug: string; + } | null; + rides?: { + name: string; + slug: string; + parks?: { + name: string; + slug: string; + } | null; + } | null; +} + +interface CreditActivity { + id: string; + type: 'credit'; + ride_count: number; + first_ride_date?: string; + created_at: string; + rides?: { + name: string; + slug: string; + parks?: { + name: string; + slug: string; + } | null; + } | null; +} + +type ActivityEntry = SubmissionActivity | RankingActivity | ReviewActivity | CreditActivity; + +// Type guards +const isSubmissionActivity = (act: ActivityEntry): act is SubmissionActivity => + act.type === 'submission'; + +const isRankingActivity = (act: ActivityEntry): act is RankingActivity => + act.type === 'ranking'; + +const isReviewActivity = (act: ActivityEntry): act is ReviewActivity => + act.type === 'review'; + +const isCreditActivity = (act: ActivityEntry): act is CreditActivity => + act.type === 'credit'; + export default function Profile() { const { username @@ -764,7 +853,7 @@ export default function Profile() { <>

- {reviewActivity.title || reviewActivity.description || 'Left a review'} + {reviewActivity.title || reviewActivity.content || 'Left a review'}

@@ -789,58 +878,58 @@ export default function Profile() { ); })() - ) : activity.type === 'submission' ? ( + ) : isSubmissionActivity(activity) ? ( <>

- {(activity as any).content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '} - {(activity as any).submission_type === 'photo' ? 'photos for' : (activity as any).submission_type || 'content'} - {(activity as any).content?.name && ` ${(activity as any).content.name}`} + {activity.content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '} + {activity.submission_type === 'photo' ? 'photos for' : activity.submission_type || 'content'} + {activity.content?.name && ` ${activity.content.name}`}

- {(activity as any).status === 'pending' && ( + {activity.status === 'pending' && ( Pending )} - {(activity as any).status === 'approved' && ( + {activity.status === 'approved' && ( Approved )} - {(activity as any).status === 'rejected' && ( + {activity.status === 'rejected' && ( Rejected )} - {(activity as any).status === 'partially_approved' && ( + {activity.status === 'partially_approved' && ( Partially Approved )}
{/* Photo preview for photo submissions */} - {(activity as any).submission_type === 'photo' && (activity as any).photo_preview && ( + {activity.submission_type === 'photo' && activity.photo_preview && (
Photo preview - {(activity as any).photo_count > 1 && ( + {activity.photo_count && activity.photo_count > 1 && ( - +{(activity as any).photo_count - 1} more + +{activity.photo_count - 1} more )}
)} {/* Entity link for photo submissions */} - {(activity as any).submission_type === 'photo' && (activity as any).content?.entity_slug && ( + {activity.submission_type === 'photo' && activity.content?.entity_slug && (
- {(activity as any).entity_type === 'park' ? ( - - {(activity as any).content.entity_name || 'View park'} + {activity.entity_type === 'park' ? ( + + {activity.content.entity_name || 'View park'} - ) : (activity as any).entity_type === 'ride' ? ( + ) : activity.entity_type === 'ride' ? ( <> - - {(activity as any).content.entity_name || 'View ride'} + + {activity.content.entity_name || 'View ride'} - {(activity as any).content.park_name && ( - at {(activity as any).content.park_name} + {activity.content.park_name && ( + at {activity.content.park_name} )} ) : null} @@ -848,42 +937,42 @@ export default function Profile() { )} {/* Links for entity submissions */} - {(activity as any).status === 'approved' && (activity as any).submission_type !== 'photo' && ( + {activity.status === 'approved' && activity.submission_type !== 'photo' && ( <> - {(activity as any).submission_type === 'park' && (activity as any).content?.slug && ( + {activity.submission_type === 'park' && activity.content?.slug && ( View park → )} - {(activity as any).submission_type === 'ride' && (activity as any).content?.slug && (activity as any).content?.park_slug && ( + {activity.submission_type === 'ride' && activity.content?.slug && activity.content?.park_slug && (
View ride → - {(activity as any).content.park_name && ( + {activity.content.park_name && ( - at {(activity as any).content.park_name} + at {activity.content.park_name} )}
)} - {(activity as any).submission_type === 'company' && (activity as any).content?.slug && ( + {activity.submission_type === 'company' && activity.content?.slug && ( - View {(activity as any).content.company_type || 'company'} → + View {activity.content.company_type || 'company'} → )} - {(activity as any).submission_type === 'ride_model' && (activity as any).content?.slug && (activity as any).content?.manufacturer_slug && ( + {activity.submission_type === 'ride_model' && activity.content?.slug && activity.content?.manufacturer_slug && ( View model → @@ -892,54 +981,51 @@ export default function Profile() { )} - {(activity as any).content?.description && (activity as any).submission_type !== 'photo' && ( + {activity.content?.description && activity.submission_type !== 'photo' && (

- {(activity as any).content.description} + {activity.content.description}

)} - ) : activity.type === 'ranking' ? ( + ) : isRankingActivity(activity) ? ( <>
- Created ranking: {(activity as any).title || 'Untitled'} + Created ranking: {activity.title || 'Untitled'} - {((activity as any).list_type || '').replace('_', ' ')} + {(activity.list_type || '').replace('_', ' ')} - {(activity as any).is_public === false && ( - Private - )}
- {(activity as any).description && ( + {activity.description && (

- {(activity as any).description} + {activity.description}

)} - ) : ( + ) : isCreditActivity(activity) ? ( <>

Added ride credit

- {(activity as any).rides && ( + {activity.rides && (
- - {(activity as any).rides.name} + + {activity.rides.name} - {(activity as any).rides.parks && ( - at {(activity as any).rides.parks.name} + {activity.rides.parks && ( + at {activity.rides.parks.name} )}
)} - {(activity as any).ride_count > 1 && ( + {activity.ride_count > 1 && (

- Ridden {(activity as any).ride_count} times + Ridden {activity.ride_count} times

)} - )} + ) : null}
diff --git a/src/pages/RideModelDetail.tsx b/src/pages/RideModelDetail.tsx index bf89c1bc..04ea3def 100644 --- a/src/pages/RideModelDetail.tsx +++ b/src/pages/RideModelDetail.tsx @@ -9,6 +9,7 @@ import { Dialog, DialogContent } from '@/components/ui/dialog'; import { ArrowLeft, FerrisWheel, Building2, Edit } from 'lucide-react'; import { RideModel, Ride, Company } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; +import { useTechnicalSpecifications } from '@/hooks/useTechnicalSpecifications'; import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useAuth } from '@/hooks/useAuth'; @@ -29,6 +30,9 @@ export default function RideModelDetail() { const [loading, setLoading] = useState(true); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [statistics, setStatistics] = useState({ rideCount: 0, photoCount: 0 }); + + // Fetch technical specifications from relational table + const { data: technicalSpecs } = useTechnicalSpecifications('ride_model', model?.id); const fetchData = useCallback(async () => { try { @@ -266,15 +270,19 @@ export default function RideModelDetail() { )} - {model.technical_specs && typeof model.technical_specs === 'object' && Object.keys(model.technical_specs).length > 0 && ( + {technicalSpecs && technicalSpecs.length > 0 && (

Technical Specifications

- {Object.entries(model.technical_specs as Record).map(([key, value]) => ( -
- {key.replace(/_/g, ' ')} - {String(value)} + {technicalSpecs.map((spec) => ( +
+ + {spec.spec_name.replace(/_/g, ' ')} + + + {spec.spec_value} {spec.spec_unit || ''} +
))}
diff --git a/src/types/database.ts b/src/types/database.ts index 6d8d23b1..584c5fb8 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -118,8 +118,8 @@ export interface RideModel { category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation'; ride_type?: string; description?: string; - technical_specs?: Record; - technical_specifications?: RideModelTechnicalSpec[]; + // Note: technical_specs deprecated - use ride_model_technical_specifications table + // Load via useTechnicalSpecifications hook instead banner_image_url?: string; banner_image_id?: string; card_image_url?: string; diff --git a/src/types/versioning.ts b/src/types/versioning.ts index 29750323..de6f4317 100644 --- a/src/types/versioning.ts +++ b/src/types/versioning.ts @@ -127,7 +127,7 @@ export interface RideModelVersion extends BaseVersionWithProfile { manufacturer_id: string | null; category: string; description: string | null; - technical_specs: Record | null; + // Note: technical_specs removed - use ride_model_technical_specifications table } /**