Refactor profile stats calculation

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 21:15:23 +00:00
parent 2f295b4879
commit ec664a1669
5 changed files with 250 additions and 2 deletions

View File

@@ -28,7 +28,16 @@ const rideSchema = z.object({
max_speed_kmh: z.number().optional(), max_speed_kmh: z.number().optional(),
max_height_meters: z.number().optional(), max_height_meters: z.number().optional(),
length_meters: z.number().optional(), length_meters: z.number().optional(),
inversions: z.number().optional() inversions: z.number().optional(),
// New roller coaster specific fields
coaster_type: z.string().optional(),
seating_type: z.string().optional(),
intensity_level: z.string().optional(),
drop_height_meters: z.number().optional(),
max_g_force: z.number().optional(),
former_names: z.string().optional(),
coaster_stats: z.string().optional(),
technical_specs: z.string().optional()
}); });
type RideFormData = z.infer<typeof rideSchema>; type RideFormData = z.infer<typeof rideSchema>;
@@ -59,6 +68,30 @@ const statusOptions = [
'SBNO' // Standing But Not Operating 'SBNO' // Standing But Not Operating
]; ];
const coasterTypes = [
'steel',
'wood',
'hybrid'
];
const seatingTypes = [
'sit_down',
'stand_up',
'flying',
'inverted',
'floorless',
'suspended',
'wing',
'dive',
'spinning'
];
const intensityLevels = [
'family',
'thrill',
'extreme'
];
export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) { export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: RideFormProps) {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [rideImage, setRideImage] = useState<string>(initialData?.image_url || ''); const [rideImage, setRideImage] = useState<string>(initialData?.image_url || '');
@@ -87,10 +120,20 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
max_speed_kmh: initialData?.max_speed_kmh || undefined, max_speed_kmh: initialData?.max_speed_kmh || undefined,
max_height_meters: initialData?.max_height_meters || undefined, max_height_meters: initialData?.max_height_meters || undefined,
length_meters: initialData?.length_meters || undefined, length_meters: initialData?.length_meters || undefined,
inversions: initialData?.inversions || undefined inversions: initialData?.inversions || undefined,
coaster_type: initialData?.coaster_type || undefined,
seating_type: initialData?.seating_type || undefined,
intensity_level: initialData?.intensity_level || undefined,
drop_height_meters: initialData?.drop_height_meters || undefined,
max_g_force: initialData?.max_g_force || undefined,
former_names: initialData?.former_names || '',
coaster_stats: initialData?.coaster_stats || '',
technical_specs: initialData?.technical_specs || ''
} }
}); });
const selectedCategory = watch('category');
const generateSlug = (name: string) => { const generateSlug = (name: string) => {
return name return name
.toLowerCase() .toLowerCase()
@@ -279,6 +322,89 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
</div> </div>
</div> </div>
{/* Roller Coaster Specific Fields */}
{selectedCategory === 'roller_coaster' && (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Roller Coaster Details</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label>Coaster Type</Label>
<Select onValueChange={(value) => setValue('coaster_type', value)} defaultValue={initialData?.coaster_type}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{coasterTypes.map((type) => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Seating Type</Label>
<Select onValueChange={(value) => setValue('seating_type', value)} defaultValue={initialData?.seating_type}>
<SelectTrigger>
<SelectValue placeholder="Select seating" />
</SelectTrigger>
<SelectContent>
{seatingTypes.map((type) => (
<SelectItem key={type} value={type}>
{type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Intensity Level</Label>
<Select onValueChange={(value) => setValue('intensity_level', value)} defaultValue={initialData?.intensity_level}>
<SelectTrigger>
<SelectValue placeholder="Select intensity" />
</SelectTrigger>
<SelectContent>
{intensityLevels.map((level) => (
<SelectItem key={level} value={level}>
{level.charAt(0).toUpperCase() + level.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="drop_height_meters">Drop Height (meters)</Label>
<Input
id="drop_height_meters"
type="number"
min="0"
step="0.1"
{...register('drop_height_meters', { valueAsNumber: true })}
placeholder="e.g. 45.5"
/>
</div>
<div className="space-y-2">
<Label htmlFor="max_g_force">Max G-Force</Label>
<Input
id="max_g_force"
type="number"
min="0"
step="0.1"
{...register('max_g_force', { valueAsNumber: true })}
placeholder="e.g. 4.2"
/>
</div>
</div>
</div>
)}
{/* Technical Specifications */} {/* Technical Specifications */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Technical Specifications</h3> <h3 className="text-lg font-semibold">Technical Specifications</h3>

View File

@@ -618,16 +618,21 @@ export type Database = {
category: string category: string
closing_date: string | null closing_date: string | null
coaster_stats: Json | null coaster_stats: Json | null
coaster_type: string | null
created_at: string created_at: string
description: string | null description: string | null
designer_id: string | null designer_id: string | null
drop_height_meters: number | null
duration_seconds: number | null duration_seconds: number | null
former_names: Json | null
height_requirement: number | null height_requirement: number | null
id: string id: string
image_url: string | null image_url: string | null
intensity_level: string | null
inversions: number | null inversions: number | null
length_meters: number | null length_meters: number | null
manufacturer_id: string | null manufacturer_id: string | null
max_g_force: number | null
max_height_meters: number | null max_height_meters: number | null
max_speed_kmh: number | null max_speed_kmh: number | null
name: string name: string
@@ -636,6 +641,7 @@ export type Database = {
review_count: number | null review_count: number | null
ride_model_id: string | null ride_model_id: string | null
ride_sub_type: string | null ride_sub_type: string | null
seating_type: string | null
slug: string slug: string
status: string status: string
technical_specs: Json | null technical_specs: Json | null
@@ -648,16 +654,21 @@ export type Database = {
category: string category: string
closing_date?: string | null closing_date?: string | null
coaster_stats?: Json | null coaster_stats?: Json | null
coaster_type?: string | null
created_at?: string created_at?: string
description?: string | null description?: string | null
designer_id?: string | null designer_id?: string | null
drop_height_meters?: number | null
duration_seconds?: number | null duration_seconds?: number | null
former_names?: Json | null
height_requirement?: number | null height_requirement?: number | null
id?: string id?: string
image_url?: string | null image_url?: string | null
intensity_level?: string | null
inversions?: number | null inversions?: number | null
length_meters?: number | null length_meters?: number | null
manufacturer_id?: string | null manufacturer_id?: string | null
max_g_force?: number | null
max_height_meters?: number | null max_height_meters?: number | null
max_speed_kmh?: number | null max_speed_kmh?: number | null
name: string name: string
@@ -666,6 +677,7 @@ export type Database = {
review_count?: number | null review_count?: number | null
ride_model_id?: string | null ride_model_id?: string | null
ride_sub_type?: string | null ride_sub_type?: string | null
seating_type?: string | null
slug: string slug: string
status?: string status?: string
technical_specs?: Json | null technical_specs?: Json | null
@@ -678,16 +690,21 @@ export type Database = {
category?: string category?: string
closing_date?: string | null closing_date?: string | null
coaster_stats?: Json | null coaster_stats?: Json | null
coaster_type?: string | null
created_at?: string created_at?: string
description?: string | null description?: string | null
designer_id?: string | null designer_id?: string | null
drop_height_meters?: number | null
duration_seconds?: number | null duration_seconds?: number | null
former_names?: Json | null
height_requirement?: number | null height_requirement?: number | null
id?: string id?: string
image_url?: string | null image_url?: string | null
intensity_level?: string | null
inversions?: number | null inversions?: number | null
length_meters?: number | null length_meters?: number | null
manufacturer_id?: string | null manufacturer_id?: string | null
max_g_force?: number | null
max_height_meters?: number | null max_height_meters?: number | null
max_speed_kmh?: number | null max_speed_kmh?: number | null
name?: string name?: string
@@ -696,6 +713,7 @@ export type Database = {
review_count?: number | null review_count?: number | null
ride_model_id?: string | null ride_model_id?: string | null
ride_sub_type?: string | null ride_sub_type?: string | null
seating_type?: string | null
slug?: string slug?: string
status?: string status?: string
technical_specs?: Json | null technical_specs?: Json | null

View File

@@ -261,6 +261,27 @@ export default function RideDetail() {
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* New roller coaster specific stats */}
{ride.drop_height_meters && (
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl mb-2"></div>
<div className="text-2xl font-bold text-destructive">{ride.drop_height_meters}m</div>
<div className="text-sm text-muted-foreground">drop</div>
</CardContent>
</Card>
)}
{ride.max_g_force && (
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl mb-2"></div>
<div className="text-2xl font-bold text-warning">{ride.max_g_force}g</div>
<div className="text-sm text-muted-foreground">max G-force</div>
</CardContent>
</Card>
)}
</div> </div>
{/* Requirements & Warnings */} {/* Requirements & Warnings */}
@@ -367,6 +388,49 @@ export default function RideDetail() {
</div> </div>
)} )}
{/* Roller Coaster Specific Info */}
{ride.category === 'roller_coaster' && (ride.coaster_type || ride.seating_type || ride.intensity_level) && (
<>
<Separator />
<div className="space-y-3">
<div className="font-medium">Coaster Details</div>
{ride.coaster_type && (
<div className="flex items-center gap-3">
<div className="text-lg">🎢</div>
<div>
<div className="font-medium">Type</div>
<div className="text-sm text-muted-foreground">
{ride.coaster_type.charAt(0).toUpperCase() + ride.coaster_type.slice(1)}
</div>
</div>
</div>
)}
{ride.seating_type && (
<div className="flex items-center gap-3">
<div className="text-lg">💺</div>
<div>
<div className="font-medium">Seating</div>
<div className="text-sm text-muted-foreground">
{ride.seating_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</div>
</div>
</div>
)}
{ride.intensity_level && (
<div className="flex items-center gap-3">
<div className="text-lg">🔥</div>
<div>
<div className="font-medium">Intensity</div>
<div className="text-sm text-muted-foreground">
{ride.intensity_level.charAt(0).toUpperCase() + ride.intensity_level.slice(1)}
</div>
</div>
</div>
)}
</div>
</>
)}
<Separator /> <Separator />
<div className="space-y-2"> <div className="space-y-2">
@@ -424,6 +488,18 @@ export default function RideDetail() {
<span className="font-medium">{ride.inversions}</span> <span className="font-medium">{ride.inversions}</span>
</div> </div>
)} )}
{ride.drop_height_meters && (
<div className="flex justify-between">
<span>Drop Height</span>
<span className="font-medium">{ride.drop_height_meters}m</span>
</div>
)}
{ride.max_g_force && (
<div className="flex justify-between">
<span>Maximum G-Force</span>
<span className="font-medium">{ride.max_g_force}g</span>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -88,6 +88,13 @@ export interface Ride {
average_rating: number; average_rating: number;
review_count: number; review_count: number;
image_url?: string; image_url?: string;
// New roller coaster specific fields
coaster_type?: string;
seating_type?: string;
intensity_level?: string;
drop_height_meters?: number;
max_g_force?: number;
former_names?: any;
} }
export interface Profile { export interface Profile {

View File

@@ -0,0 +1,21 @@
-- Add roller coaster specific columns to the rides table
ALTER TABLE public.rides
ADD COLUMN coaster_type TEXT CHECK (coaster_type IN ('steel', 'wood', 'hybrid')),
ADD COLUMN seating_type TEXT,
ADD COLUMN intensity_level TEXT CHECK (intensity_level IN ('family', 'thrill', 'extreme')),
ADD COLUMN former_names JSONB DEFAULT '[]'::jsonb,
ADD COLUMN drop_height_meters NUMERIC(6,2),
ADD COLUMN max_g_force NUMERIC(4,2);
-- Add indexes for better query performance on commonly filtered fields
CREATE INDEX idx_rides_coaster_type ON public.rides(coaster_type) WHERE coaster_type IS NOT NULL;
CREATE INDEX idx_rides_seating_type ON public.rides(seating_type) WHERE seating_type IS NOT NULL;
CREATE INDEX idx_rides_intensity_level ON public.rides(intensity_level) WHERE intensity_level IS NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN public.rides.coaster_type IS 'Type of roller coaster construction: steel, wood, or hybrid';
COMMENT ON COLUMN public.rides.seating_type IS 'How riders are seated: sit_down, stand_up, flying, inverted, etc.';
COMMENT ON COLUMN public.rides.intensity_level IS 'Target audience intensity: family, thrill, or extreme';
COMMENT ON COLUMN public.rides.former_names IS 'Array of previous names this ride has had';
COMMENT ON COLUMN public.rides.drop_height_meters IS 'Maximum drop height in meters';
COMMENT ON COLUMN public.rides.max_g_force IS 'Maximum G-force experienced on the ride';