Refactor: Implement complete plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 14:04:57 +00:00
parent a89a740611
commit a2e05c5080
10 changed files with 258 additions and 68 deletions

View File

@@ -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') {

View File

@@ -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);

View 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
});
}

View File

@@ -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',

View 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
});
}

View File

@@ -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();

View File

@@ -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() {
<>
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">
{reviewActivity.title || reviewActivity.description || 'Left a review'}
{reviewActivity.title || reviewActivity.content || 'Left a review'}
</p>
</div>
<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">
<p className="font-medium">
{(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}`}
</p>
{(activity as any).status === 'pending' && (
{activity.status === 'pending' && (
<Badge variant="secondary" className="text-xs">Pending</Badge>
)}
{(activity as any).status === 'approved' && (
{activity.status === 'approved' && (
<Badge variant="default" className="text-xs">Approved</Badge>
)}
{(activity as any).status === 'rejected' && (
{activity.status === 'rejected' && (
<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>
)}
</div>
{/* 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">
<img
src={(activity as any).photo_preview}
src={activity.photo_preview}
alt="Photo preview"
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">
+{(activity as any).photo_count - 1} more
+{activity.photo_count - 1} more
</span>
)}
</div>
)}
{/* 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">
{(activity as any).entity_type === 'park' ? (
<Link to={`/parks/${(activity as any).content.entity_slug}`} className="hover:text-accent transition-colors">
{(activity as any).content.entity_name || 'View park'}
{activity.entity_type === 'park' ? (
<Link to={`/parks/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
{activity.content.entity_name || 'View park'}
</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">
{(activity as any).content.entity_name || 'View ride'}
<Link to={`/parks/${activity.content.park_slug}/rides/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
{activity.content.entity_name || 'View ride'}
</Link>
{(activity as any).content.park_name && (
<span className="text-muted-foreground/70"> at {(activity as any).content.park_name}</span>
{activity.content.park_name && (
<span className="text-muted-foreground/70"> at {activity.content.park_name}</span>
)}
</>
) : 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 && (
<Link
to={`/parks/${(activity as any).content.slug}`}
to={`/parks/${activity.content.slug}`}
className="text-sm text-accent hover:underline"
>
View park
</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">
<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"
>
View ride
</Link>
{(activity as any).content.park_name && (
{activity.content.park_name && (
<span className="text-muted-foreground ml-1">
at {(activity as any).content.park_name}
at {activity.content.park_name}
</span>
)}
</div>
)}
{(activity as any).submission_type === 'company' && (activity as any).content?.slug && (
{activity.submission_type === 'company' && activity.content?.slug && (
<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"
>
View {(activity as any).content.company_type || 'company'}
View {activity.content.company_type || 'company'}
</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
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"
>
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">
{(activity as any).content.description}
{activity.content.description}
</p>
)}
</>
) : activity.type === 'ranking' ? (
) : isRankingActivity(activity) ? (
<>
<div className="flex items-center gap-2 mb-1">
<Link
to={`/profile/${profile?.username}/lists`}
className="font-medium hover:text-accent transition-colors"
>
Created ranking: {(activity as any).title || 'Untitled'}
Created ranking: {activity.title || 'Untitled'}
</Link>
<Badge variant="outline" className="text-xs capitalize">
{((activity as any).list_type || '').replace('_', ' ')}
{(activity.list_type || '').replace('_', ' ')}
</Badge>
{(activity as any).is_public === false && (
<Badge variant="secondary" className="text-xs">Private</Badge>
)}
</div>
{(activity as any).description && (
{activity.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{(activity as any).description}
{activity.description}
</p>
)}
</>
) : (
) : isCreditActivity(activity) ? (
<>
<p className="font-medium mb-1">Added ride credit</p>
{(activity as any).rides && (
{activity.rides && (
<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">
{(activity as any).rides.name}
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
{activity.rides.name}
</Link>
{(activity as any).rides.parks && (
<span className="text-muted-foreground/70"> at {(activity as any).rides.parks.name}</span>
{activity.rides.parks && (
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
)}
</div>
)}
{(activity as any).ride_count > 1 && (
{activity.ride_count > 1 && (
<p className="text-xs text-muted-foreground mt-1">
Ridden {(activity as any).ride_count} times
Ridden {activity.ride_count} times
</p>
)}
</>
)}
) : null}
</div>
<div className="flex-shrink-0 text-xs text-muted-foreground">

View File

@@ -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() {
</Card>
)}
{model.technical_specs && typeof model.technical_specs === 'object' && Object.keys(model.technical_specs).length > 0 && (
{technicalSpecs && technicalSpecs.length > 0 && (
<Card>
<CardContent className="pt-6">
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
<div className="grid md:grid-cols-2 gap-4">
{Object.entries(model.technical_specs as Record<string, any>).map(([key, value]) => (
<div key={key} className="flex justify-between py-2 border-b">
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}</span>
<span className="text-muted-foreground">{String(value)}</span>
{technicalSpecs.map((spec) => (
<div key={spec.id} className="flex justify-between py-2 border-b">
<span className="font-medium capitalize">
{spec.spec_name.replace(/_/g, ' ')}
</span>
<span className="text-muted-foreground">
{spec.spec_value} {spec.spec_unit || ''}
</span>
</div>
))}
</div>

View File

@@ -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<string, any>;
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;

View File

@@ -127,7 +127,7 @@ export interface RideModelVersion extends BaseVersionWithProfile {
manufacturer_id: string | null;
category: string;
description: string | null;
technical_specs: Record<string, any> | null;
// Note: technical_specs removed - use ride_model_technical_specifications table
}
/**