mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
Refactor: Implement complete plan
This commit is contained in:
@@ -121,7 +121,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
const { data, error } = await query;
|
const { data, error } = await query;
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
let processedData = (data || []) as any[];
|
let processedData = data || [];
|
||||||
|
|
||||||
// Sort by name client-side if needed
|
// Sort by name client-side if needed
|
||||||
if (sortBy === 'name') {
|
if (sortBy === 'name') {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function useEntityCache() {
|
|||||||
|
|
||||||
// Collect all entity IDs from submissions
|
// Collect all entity IDs from submissions
|
||||||
submissions.forEach(submission => {
|
submissions.forEach(submission => {
|
||||||
const content = submission.content as any;
|
const content = submission.content;
|
||||||
if (content && typeof content === 'object') {
|
if (content && typeof content === 'object') {
|
||||||
if (content.ride_id) rideIds.add(content.ride_id);
|
if (content.ride_id) rideIds.add(content.ride_id);
|
||||||
if (content.park_id) parkIds.add(content.park_id);
|
if (content.park_id) parkIds.add(content.park_id);
|
||||||
|
|||||||
44
src/hooks/useCoasterStats.ts
Normal file
44
src/hooks/useCoasterStats.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -41,8 +41,8 @@ export function useEntityVersions(entityType: EntityType, entityId: string) {
|
|||||||
const versionTable = `${entityType}_versions`;
|
const versionTable = `${entityType}_versions`;
|
||||||
const entityIdCol = `${entityType}_id`;
|
const entityIdCol = `${entityType}_id`;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await (supabase as any)
|
||||||
.from(versionTable as any)
|
.from(versionTable)
|
||||||
.select(`
|
.select(`
|
||||||
*,
|
*,
|
||||||
profiles:created_by(username, display_name, avatar_url)
|
profiles:created_by(username, display_name, avatar_url)
|
||||||
@@ -63,7 +63,7 @@ export function useEntityVersions(entityType: EntityType, entityId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionsWithProfiles = (data as any[]).map((v: any) => ({
|
const versionsWithProfiles = (data || []).map((v: any) => ({
|
||||||
...v,
|
...v,
|
||||||
profiles: v.profiles || {
|
profiles: v.profiles || {
|
||||||
username: 'Unknown',
|
username: 'Unknown',
|
||||||
|
|||||||
52
src/hooks/useTechnicalSpecifications.ts
Normal file
52
src/hooks/useTechnicalSpecifications.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -121,7 +121,7 @@ export const rideModelRuntimeSchema = z.object({
|
|||||||
manufacturer_id: z.string().uuid().nullable().optional(),
|
manufacturer_id: z.string().uuid().nullable().optional(),
|
||||||
category: z.string(),
|
category: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
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(),
|
created_at: z.string().optional(),
|
||||||
updated_at: z.string().optional(),
|
updated_at: z.string().optional(),
|
||||||
}).passthrough();
|
}).passthrough();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { UserListManager } from '@/components/lists/UserListManager';
|
|||||||
import { RideCreditsManager } from '@/components/profile/RideCreditsManager';
|
import { RideCreditsManager } from '@/components/profile/RideCreditsManager';
|
||||||
import { useUsernameValidation } from '@/hooks/useUsernameValidation';
|
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 { 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 { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
@@ -30,6 +30,95 @@ import { UserBlockButton } from '@/components/profile/UserBlockButton';
|
|||||||
import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDisplay';
|
import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDisplay';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
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() {
|
export default function Profile() {
|
||||||
const {
|
const {
|
||||||
username
|
username
|
||||||
@@ -764,7 +853,7 @@ export default function Profile() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{reviewActivity.title || reviewActivity.description || 'Left a review'}
|
{reviewActivity.title || reviewActivity.content || 'Left a review'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
@@ -789,58 +878,58 @@ export default function Profile() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
) : activity.type === 'submission' ? (
|
) : isSubmissionActivity(activity) ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{(activity as any).content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '}
|
{activity.content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '}
|
||||||
{(activity as any).submission_type === 'photo' ? 'photos for' : (activity as any).submission_type || 'content'}
|
{activity.submission_type === 'photo' ? 'photos for' : activity.submission_type || 'content'}
|
||||||
{(activity as any).content?.name && ` ${(activity as any).content.name}`}
|
{activity.content?.name && ` ${activity.content.name}`}
|
||||||
</p>
|
</p>
|
||||||
{(activity as any).status === 'pending' && (
|
{activity.status === 'pending' && (
|
||||||
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
||||||
)}
|
)}
|
||||||
{(activity as any).status === 'approved' && (
|
{activity.status === 'approved' && (
|
||||||
<Badge variant="default" className="text-xs">Approved</Badge>
|
<Badge variant="default" className="text-xs">Approved</Badge>
|
||||||
)}
|
)}
|
||||||
{(activity as any).status === 'rejected' && (
|
{activity.status === 'rejected' && (
|
||||||
<Badge variant="destructive" className="text-xs">Rejected</Badge>
|
<Badge variant="destructive" className="text-xs">Rejected</Badge>
|
||||||
)}
|
)}
|
||||||
{(activity as any).status === 'partially_approved' && (
|
{activity.status === 'partially_approved' && (
|
||||||
<Badge variant="outline" className="text-xs">Partially Approved</Badge>
|
<Badge variant="outline" className="text-xs">Partially Approved</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Photo preview for photo submissions */}
|
{/* Photo preview for photo submissions */}
|
||||||
{(activity as any).submission_type === 'photo' && (activity as any).photo_preview && (
|
{activity.submission_type === 'photo' && activity.photo_preview && (
|
||||||
<div className="flex gap-2 items-center mb-2">
|
<div className="flex gap-2 items-center mb-2">
|
||||||
<img
|
<img
|
||||||
src={(activity as any).photo_preview}
|
src={activity.photo_preview}
|
||||||
alt="Photo preview"
|
alt="Photo preview"
|
||||||
className="w-16 h-16 rounded object-cover border"
|
className="w-16 h-16 rounded object-cover border"
|
||||||
/>
|
/>
|
||||||
{(activity as any).photo_count > 1 && (
|
{activity.photo_count && activity.photo_count > 1 && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
+{(activity as any).photo_count - 1} more
|
+{activity.photo_count - 1} more
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Entity link for photo submissions */}
|
{/* 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 && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{(activity as any).entity_type === 'park' ? (
|
{activity.entity_type === 'park' ? (
|
||||||
<Link to={`/parks/${(activity as any).content.entity_slug}`} className="hover:text-accent transition-colors">
|
<Link to={`/parks/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
|
||||||
{(activity as any).content.entity_name || 'View park'}
|
{activity.content.entity_name || 'View park'}
|
||||||
</Link>
|
</Link>
|
||||||
) : (activity as any).entity_type === 'ride' ? (
|
) : activity.entity_type === 'ride' ? (
|
||||||
<>
|
<>
|
||||||
<Link to={`/parks/${(activity as any).content.park_slug}/rides/${(activity as any).content.entity_slug}`} className="hover:text-accent transition-colors">
|
<Link to={`/parks/${activity.content.park_slug}/rides/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
|
||||||
{(activity as any).content.entity_name || 'View ride'}
|
{activity.content.entity_name || 'View ride'}
|
||||||
</Link>
|
</Link>
|
||||||
{(activity as any).content.park_name && (
|
{activity.content.park_name && (
|
||||||
<span className="text-muted-foreground/70"> at {(activity as any).content.park_name}</span>
|
<span className="text-muted-foreground/70"> at {activity.content.park_name}</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -848,42 +937,42 @@ export default function Profile() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Links for entity submissions */}
|
{/* 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 && (
|
||||||
<Link
|
<Link
|
||||||
to={`/parks/${(activity as any).content.slug}`}
|
to={`/parks/${activity.content.slug}`}
|
||||||
className="text-sm text-accent hover:underline"
|
className="text-sm text-accent hover:underline"
|
||||||
>
|
>
|
||||||
View park →
|
View park →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{(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 && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<Link
|
<Link
|
||||||
to={`/parks/${(activity as any).content.park_slug}/rides/${(activity as any).content.slug}`}
|
to={`/parks/${activity.content.park_slug}/rides/${activity.content.slug}`}
|
||||||
className="text-accent hover:underline"
|
className="text-accent hover:underline"
|
||||||
>
|
>
|
||||||
View ride →
|
View ride →
|
||||||
</Link>
|
</Link>
|
||||||
{(activity as any).content.park_name && (
|
{activity.content.park_name && (
|
||||||
<span className="text-muted-foreground ml-1">
|
<span className="text-muted-foreground ml-1">
|
||||||
at {(activity as any).content.park_name}
|
at {activity.content.park_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(activity as any).submission_type === 'company' && (activity as any).content?.slug && (
|
{activity.submission_type === 'company' && activity.content?.slug && (
|
||||||
<Link
|
<Link
|
||||||
to={`/${(activity as any).content.company_type === 'operator' ? 'operators' : (activity as any).content.company_type === 'property_owner' ? 'owners' : (activity as any).content.company_type === 'manufacturer' ? 'manufacturers' : 'designers'}/${(activity as any).content.slug}`}
|
to={`/${activity.content.company_type === 'operator' ? 'operators' : activity.content.company_type === 'property_owner' ? 'owners' : activity.content.company_type === 'manufacturer' ? 'manufacturers' : 'designers'}/${activity.content.slug}`}
|
||||||
className="text-sm text-accent hover:underline"
|
className="text-sm text-accent hover:underline"
|
||||||
>
|
>
|
||||||
View {(activity as any).content.company_type || 'company'} →
|
View {activity.content.company_type || 'company'} →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{(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 && (
|
||||||
<Link
|
<Link
|
||||||
to={`/manufacturers/${(activity as any).content.manufacturer_slug}/models/${(activity as any).content.slug}`}
|
to={`/manufacturers/${activity.content.manufacturer_slug}/models/${activity.content.slug}`}
|
||||||
className="text-sm text-accent hover:underline"
|
className="text-sm text-accent hover:underline"
|
||||||
>
|
>
|
||||||
View model →
|
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' && (
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
{(activity as any).content.description}
|
{activity.content.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : activity.type === 'ranking' ? (
|
) : isRankingActivity(activity) ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Link
|
<Link
|
||||||
to={`/profile/${profile?.username}/lists`}
|
to={`/profile/${profile?.username}/lists`}
|
||||||
className="font-medium hover:text-accent transition-colors"
|
className="font-medium hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
Created ranking: {(activity as any).title || 'Untitled'}
|
Created ranking: {activity.title || 'Untitled'}
|
||||||
</Link>
|
</Link>
|
||||||
<Badge variant="outline" className="text-xs capitalize">
|
<Badge variant="outline" className="text-xs capitalize">
|
||||||
{((activity as any).list_type || '').replace('_', ' ')}
|
{(activity.list_type || '').replace('_', ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
{(activity as any).is_public === false && (
|
|
||||||
<Badge variant="secondary" className="text-xs">Private</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{(activity as any).description && (
|
{activity.description && (
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
{(activity as any).description}
|
{activity.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : isCreditActivity(activity) ? (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium mb-1">Added ride credit</p>
|
<p className="font-medium mb-1">Added ride credit</p>
|
||||||
{(activity as any).rides && (
|
{activity.rides && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
<Link to={`/parks/${(activity as any).rides.parks?.slug}/rides/${(activity as any).rides.slug}`} className="hover:text-accent transition-colors">
|
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
|
||||||
{(activity as any).rides.name}
|
{activity.rides.name}
|
||||||
</Link>
|
</Link>
|
||||||
{(activity as any).rides.parks && (
|
{activity.rides.parks && (
|
||||||
<span className="text-muted-foreground/70"> at {(activity as any).rides.parks.name}</span>
|
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(activity as any).ride_count > 1 && (
|
{activity.ride_count > 1 && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Ridden {(activity as any).ride_count} times
|
Ridden {activity.ride_count} times
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 text-xs text-muted-foreground">
|
<div className="flex-shrink-0 text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Dialog, DialogContent } from '@/components/ui/dialog';
|
|||||||
import { ArrowLeft, FerrisWheel, Building2, Edit } from 'lucide-react';
|
import { ArrowLeft, FerrisWheel, Building2, Edit } from 'lucide-react';
|
||||||
import { RideModel, Ride, Company } from '@/types/database';
|
import { RideModel, Ride, Company } from '@/types/database';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { useTechnicalSpecifications } from '@/hooks/useTechnicalSpecifications';
|
||||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
@@ -29,6 +30,9 @@ export default function RideModelDetail() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
const [statistics, setStatistics] = useState({ rideCount: 0, photoCount: 0 });
|
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 () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -266,15 +270,19 @@ export default function RideModelDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{model.technical_specs && typeof model.technical_specs === 'object' && Object.keys(model.technical_specs).length > 0 && (
|
{technicalSpecs && technicalSpecs.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
|
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
{Object.entries(model.technical_specs as Record<string, any>).map(([key, value]) => (
|
{technicalSpecs.map((spec) => (
|
||||||
<div key={key} className="flex justify-between py-2 border-b">
|
<div key={spec.id} className="flex justify-between py-2 border-b">
|
||||||
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}</span>
|
<span className="font-medium capitalize">
|
||||||
<span className="text-muted-foreground">{String(value)}</span>
|
{spec.spec_name.replace(/_/g, ' ')}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{spec.spec_value} {spec.spec_unit || ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ export interface RideModel {
|
|||||||
category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation';
|
category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation';
|
||||||
ride_type?: string;
|
ride_type?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
technical_specs?: Record<string, any>;
|
// Note: technical_specs deprecated - use ride_model_technical_specifications table
|
||||||
technical_specifications?: RideModelTechnicalSpec[];
|
// Load via useTechnicalSpecifications hook instead
|
||||||
banner_image_url?: string;
|
banner_image_url?: string;
|
||||||
banner_image_id?: string;
|
banner_image_id?: string;
|
||||||
card_image_url?: string;
|
card_image_url?: string;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export interface RideModelVersion extends BaseVersionWithProfile {
|
|||||||
manufacturer_id: string | null;
|
manufacturer_id: string | null;
|
||||||
category: string;
|
category: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
technical_specs: Record<string, any> | null;
|
// Note: technical_specs removed - use ride_model_technical_specifications table
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user