mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:31:13 -05:00
Refactor: Update user list item interfaces
This commit is contained in:
277
src/components/search/AdvancedRideFilters.tsx
Normal file
277
src/components/search/AdvancedRideFilters.tsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
import { ChevronDown, Plus, X } from 'lucide-react';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
|
export interface TechnicalSpecFilter {
|
||||||
|
spec_name: string;
|
||||||
|
spec_value?: string;
|
||||||
|
min_value?: number;
|
||||||
|
max_value?: number;
|
||||||
|
operator: 'equals' | 'contains' | 'range' | 'has_spec';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoasterStatFilter {
|
||||||
|
stat_name: string;
|
||||||
|
min_value?: number;
|
||||||
|
max_value?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdvancedRideFiltersProps {
|
||||||
|
technicalSpecFilters: TechnicalSpecFilter[];
|
||||||
|
coasterStatFilters: CoasterStatFilter[];
|
||||||
|
onTechnicalSpecFiltersChange: (filters: TechnicalSpecFilter[]) => void;
|
||||||
|
onCoasterStatFiltersChange: (filters: CoasterStatFilter[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMON_TECH_SPECS = [
|
||||||
|
'Track Material',
|
||||||
|
'Manufacturer Model',
|
||||||
|
'Train Configuration',
|
||||||
|
'Launch System',
|
||||||
|
'Braking System',
|
||||||
|
'Control System',
|
||||||
|
'Safety System',
|
||||||
|
];
|
||||||
|
|
||||||
|
const COMMON_COASTER_STATS = [
|
||||||
|
'Max Speed',
|
||||||
|
'Height',
|
||||||
|
'Drop Height',
|
||||||
|
'Length',
|
||||||
|
'Duration',
|
||||||
|
'Inversions',
|
||||||
|
'G-Force',
|
||||||
|
'Capacity',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AdvancedRideFilters({
|
||||||
|
technicalSpecFilters,
|
||||||
|
coasterStatFilters,
|
||||||
|
onTechnicalSpecFiltersChange,
|
||||||
|
onCoasterStatFiltersChange,
|
||||||
|
}: AdvancedRideFiltersProps) {
|
||||||
|
const [isSpecsOpen, setIsSpecsOpen] = useState(false);
|
||||||
|
const [isStatsOpen, setIsStatsOpen] = useState(false);
|
||||||
|
|
||||||
|
const addTechnicalSpecFilter = () => {
|
||||||
|
onTechnicalSpecFiltersChange([
|
||||||
|
...technicalSpecFilters,
|
||||||
|
{ spec_name: '', operator: 'has_spec' },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTechnicalSpecFilter = (index: number, updates: Partial<TechnicalSpecFilter>) => {
|
||||||
|
const updated = [...technicalSpecFilters];
|
||||||
|
updated[index] = { ...updated[index], ...updates };
|
||||||
|
onTechnicalSpecFiltersChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTechnicalSpecFilter = (index: number) => {
|
||||||
|
onTechnicalSpecFiltersChange(technicalSpecFilters.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCoasterStatFilter = () => {
|
||||||
|
onCoasterStatFiltersChange([
|
||||||
|
...coasterStatFilters,
|
||||||
|
{ stat_name: '' },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCoasterStatFilter = (index: number, updates: Partial<CoasterStatFilter>) => {
|
||||||
|
const updated = [...coasterStatFilters];
|
||||||
|
updated[index] = { ...updated[index], ...updates };
|
||||||
|
onCoasterStatFiltersChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCoasterStatFilter = (index: number) => {
|
||||||
|
onCoasterStatFiltersChange(coasterStatFilters.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Technical Specifications Filters */}
|
||||||
|
<Collapsible open={isSpecsOpen} onOpenChange={setIsSpecsOpen}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-semibold">Technical Specifications</h4>
|
||||||
|
{technicalSpecFilters.length > 0 && (
|
||||||
|
<Badge variant="secondary">{technicalSpecFilters.length}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronDown className={`w-4 h-4 transition-transform ${isSpecsOpen ? 'rotate-180' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent className="space-y-3 pt-3">
|
||||||
|
{technicalSpecFilters.map((filter, index) => (
|
||||||
|
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Specification Name</Label>
|
||||||
|
<Select
|
||||||
|
value={filter.spec_name}
|
||||||
|
onValueChange={(value) => updateTechnicalSpecFilter(index, { spec_name: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select specification" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{COMMON_TECH_SPECS.map((spec) => (
|
||||||
|
<SelectItem key={spec} value={spec}>
|
||||||
|
{spec}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Filter Type</Label>
|
||||||
|
<Select
|
||||||
|
value={filter.operator}
|
||||||
|
onValueChange={(value: any) => updateTechnicalSpecFilter(index, { operator: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="has_spec">Has Specification</SelectItem>
|
||||||
|
<SelectItem value="equals">Equals Value</SelectItem>
|
||||||
|
<SelectItem value="contains">Contains Text</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeTechnicalSpecFilter(index)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(filter.operator === 'equals' || filter.operator === 'contains') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Value</Label>
|
||||||
|
<Input
|
||||||
|
value={filter.spec_value || ''}
|
||||||
|
onChange={(e) => updateTechnicalSpecFilter(index, { spec_value: e.target.value })}
|
||||||
|
placeholder="Enter value to match"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addTechnicalSpecFilter}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Add Technical Spec Filter
|
||||||
|
</Button>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
{/* Coaster Statistics Filters */}
|
||||||
|
<Collapsible open={isStatsOpen} onOpenChange={setIsStatsOpen}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-semibold">Coaster Statistics</h4>
|
||||||
|
{coasterStatFilters.length > 0 && (
|
||||||
|
<Badge variant="secondary">{coasterStatFilters.length}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronDown className={`w-4 h-4 transition-transform ${isStatsOpen ? 'rotate-180' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent className="space-y-3 pt-3">
|
||||||
|
{coasterStatFilters.map((filter, index) => (
|
||||||
|
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Statistic Name</Label>
|
||||||
|
<Select
|
||||||
|
value={filter.stat_name}
|
||||||
|
onValueChange={(value) => updateCoasterStatFilter(index, { stat_name: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select statistic" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{COMMON_COASTER_STATS.map((stat) => (
|
||||||
|
<SelectItem key={stat} value={stat}>
|
||||||
|
{stat}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Min Value</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={filter.min_value || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCoasterStatFilter(index, {
|
||||||
|
min_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Min"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Max Value</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={filter.max_value || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCoasterStatFilter(index, {
|
||||||
|
max_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Max"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeCoasterStatFilter(index)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addCoasterStatFilter}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Add Coaster Stat Filter
|
||||||
|
</Button>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { ChevronDown, Filter, X } from 'lucide-react';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||||
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker';
|
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker';
|
||||||
|
import { AdvancedRideFilters, TechnicalSpecFilter, CoasterStatFilter } from './AdvancedRideFilters';
|
||||||
|
|
||||||
export interface SearchFilters {
|
export interface SearchFilters {
|
||||||
// Park filters
|
// Park filters
|
||||||
@@ -36,6 +37,8 @@ export interface SearchFilters {
|
|||||||
speedMax?: number;
|
speedMax?: number;
|
||||||
intensityLevel?: string;
|
intensityLevel?: string;
|
||||||
rideDateRange?: DateRange;
|
rideDateRange?: DateRange;
|
||||||
|
technicalSpecFilters?: TechnicalSpecFilter[];
|
||||||
|
coasterStatFilters?: CoasterStatFilter[];
|
||||||
|
|
||||||
// Company filters
|
// Company filters
|
||||||
companyType?: string;
|
companyType?: string;
|
||||||
@@ -391,6 +394,16 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Ride Filters */}
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<AdvancedRideFilters
|
||||||
|
technicalSpecFilters={filters.technicalSpecFilters || []}
|
||||||
|
coasterStatFilters={filters.coasterStatFilters || []}
|
||||||
|
onTechnicalSpecFiltersChange={(filters) => updateFilter('technicalSpecFilters', filters)}
|
||||||
|
onCoasterStatFiltersChange={(filters) => updateFilter('coasterStatFilters', filters)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
177
src/hooks/useAdvancedRideSearch.ts
Normal file
177
src/hooks/useAdvancedRideSearch.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { TechnicalSpecFilter, CoasterStatFilter } from '@/components/search/AdvancedRideFilters';
|
||||||
|
import { useDebounce } from './useDebounce';
|
||||||
|
|
||||||
|
interface AdvancedSearchOptions {
|
||||||
|
query?: string;
|
||||||
|
category?: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
technicalSpecFilters?: TechnicalSpecFilter[];
|
||||||
|
coasterStatFilters?: CoasterStatFilter[];
|
||||||
|
speedMin?: number;
|
||||||
|
speedMax?: number;
|
||||||
|
heightMin?: number;
|
||||||
|
heightMax?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdvancedRideSearch(options: AdvancedSearchOptions) {
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const debouncedQuery = useDebounce(options.query || '', 300);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const performSearch = async () => {
|
||||||
|
if (!debouncedQuery && !options.category && !options.manufacturer &&
|
||||||
|
(!options.technicalSpecFilters || options.technicalSpecFilters.length === 0) &&
|
||||||
|
(!options.coasterStatFilters || options.coasterStatFilters.length === 0)) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let query = supabase
|
||||||
|
.from('rides')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
parks!inner(id, name, slug),
|
||||||
|
companies!rides_manufacturer_id_fkey(id, name, slug)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Basic text search
|
||||||
|
if (debouncedQuery) {
|
||||||
|
query = query.or(`name.ilike.%${debouncedQuery}%,description.ilike.%${debouncedQuery}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter
|
||||||
|
if (options.category) {
|
||||||
|
query = query.eq('category', options.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manufacturer filter
|
||||||
|
if (options.manufacturer) {
|
||||||
|
query = query.eq('manufacturer_id', options.manufacturer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed range filter
|
||||||
|
if (options.speedMin !== undefined) {
|
||||||
|
query = query.gte('max_speed_kmh', options.speedMin);
|
||||||
|
}
|
||||||
|
if (options.speedMax !== undefined) {
|
||||||
|
query = query.lte('max_speed_kmh', options.speedMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height range filter
|
||||||
|
if (options.heightMin !== undefined) {
|
||||||
|
query = query.gte('max_height_meters', options.heightMin);
|
||||||
|
}
|
||||||
|
if (options.heightMax !== undefined) {
|
||||||
|
query = query.lte('max_height_meters', options.heightMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.limit(options.limit || 50);
|
||||||
|
|
||||||
|
const { data: initialResults, error: queryError } = await query;
|
||||||
|
|
||||||
|
if (queryError) throw queryError;
|
||||||
|
|
||||||
|
let filteredResults = initialResults || [];
|
||||||
|
|
||||||
|
// Apply technical specification filters
|
||||||
|
if (options.technicalSpecFilters && options.technicalSpecFilters.length > 0) {
|
||||||
|
for (const filter of options.technicalSpecFilters) {
|
||||||
|
if (!filter.spec_name) continue;
|
||||||
|
|
||||||
|
const { data: specsData, error: specsError } = await supabase
|
||||||
|
.from('ride_technical_specifications')
|
||||||
|
.select('ride_id, spec_name, spec_value')
|
||||||
|
.eq('spec_name', filter.spec_name);
|
||||||
|
|
||||||
|
if (specsError) throw specsError;
|
||||||
|
|
||||||
|
const matchingRideIds = new Set<string>();
|
||||||
|
|
||||||
|
specsData?.forEach((spec) => {
|
||||||
|
let matches = false;
|
||||||
|
|
||||||
|
switch (filter.operator) {
|
||||||
|
case 'has_spec':
|
||||||
|
matches = true;
|
||||||
|
break;
|
||||||
|
case 'equals':
|
||||||
|
matches = spec.spec_value === filter.spec_value;
|
||||||
|
break;
|
||||||
|
case 'contains':
|
||||||
|
matches = filter.spec_value ?
|
||||||
|
spec.spec_value.toLowerCase().includes(filter.spec_value.toLowerCase()) :
|
||||||
|
false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
matchingRideIds.add(spec.ride_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredResults = filteredResults.filter((ride) => matchingRideIds.has(ride.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply coaster statistics filters
|
||||||
|
if (options.coasterStatFilters && options.coasterStatFilters.length > 0) {
|
||||||
|
for (const filter of options.coasterStatFilters) {
|
||||||
|
if (!filter.stat_name) continue;
|
||||||
|
|
||||||
|
let statsQuery = supabase
|
||||||
|
.from('ride_coaster_statistics')
|
||||||
|
.select('ride_id, stat_name, stat_value')
|
||||||
|
.eq('stat_name', filter.stat_name);
|
||||||
|
|
||||||
|
if (filter.min_value !== undefined) {
|
||||||
|
statsQuery = statsQuery.gte('stat_value', filter.min_value);
|
||||||
|
}
|
||||||
|
if (filter.max_value !== undefined) {
|
||||||
|
statsQuery = statsQuery.lte('stat_value', filter.max_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: statsData, error: statsError } = await statsQuery;
|
||||||
|
|
||||||
|
if (statsError) throw statsError;
|
||||||
|
|
||||||
|
const matchingRideIds = new Set(statsData?.map((stat) => stat.ride_id) || []);
|
||||||
|
filteredResults = filteredResults.filter((ride) => matchingRideIds.has(ride.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setResults(filteredResults);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Advanced search error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Search failed');
|
||||||
|
setResults([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performSearch();
|
||||||
|
}, [
|
||||||
|
debouncedQuery,
|
||||||
|
options.category,
|
||||||
|
options.manufacturer,
|
||||||
|
options.speedMin,
|
||||||
|
options.speedMax,
|
||||||
|
options.heightMin,
|
||||||
|
options.heightMax,
|
||||||
|
options.limit,
|
||||||
|
JSON.stringify(options.technicalSpecFilters),
|
||||||
|
JSON.stringify(options.coasterStatFilters),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { results, loading, error };
|
||||||
|
}
|
||||||
@@ -36,43 +36,10 @@ export function transformParkData(submissionData: any): ParkInsert {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform ride submission data to database insert format
|
* Transform ride submission data to database insert format
|
||||||
|
* Note: Relational data (technical_specs, coaster_stats, former_names) are now
|
||||||
|
* stored in separate tables and should not be included in the main ride insert.
|
||||||
*/
|
*/
|
||||||
export function transformRideData(submissionData: any): RideInsert {
|
export function transformRideData(submissionData: any): RideInsert {
|
||||||
// Parse JSON fields if they're strings
|
|
||||||
let coasterStats = null;
|
|
||||||
let technicalSpecs = null;
|
|
||||||
let formerNames = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (submissionData.coaster_stats) {
|
|
||||||
coasterStats = typeof submissionData.coaster_stats === 'string'
|
|
||||||
? JSON.parse(submissionData.coaster_stats)
|
|
||||||
: submissionData.coaster_stats;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse coaster_stats:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (submissionData.technical_specs) {
|
|
||||||
technicalSpecs = typeof submissionData.technical_specs === 'string'
|
|
||||||
? JSON.parse(submissionData.technical_specs)
|
|
||||||
: submissionData.technical_specs;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse technical_specs:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (submissionData.former_names) {
|
|
||||||
formerNames = typeof submissionData.former_names === 'string'
|
|
||||||
? JSON.parse(submissionData.former_names)
|
|
||||||
: submissionData.former_names;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse former_names:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: submissionData.name,
|
name: submissionData.name,
|
||||||
slug: submissionData.slug,
|
slug: submissionData.slug,
|
||||||
@@ -99,9 +66,6 @@ export function transformRideData(submissionData: any): RideInsert {
|
|||||||
coaster_type: submissionData.coaster_type || null,
|
coaster_type: submissionData.coaster_type || null,
|
||||||
seating_type: submissionData.seating_type || null,
|
seating_type: submissionData.seating_type || null,
|
||||||
intensity_level: submissionData.intensity_level || null,
|
intensity_level: submissionData.intensity_level || null,
|
||||||
coaster_stats: coasterStats,
|
|
||||||
technical_specs: technicalSpecs,
|
|
||||||
former_names: formerNames || [],
|
|
||||||
banner_image_url: submissionData.banner_image_url || null,
|
banner_image_url: submissionData.banner_image_url || null,
|
||||||
banner_image_id: submissionData.banner_image_id || null,
|
banner_image_id: submissionData.banner_image_id || null,
|
||||||
card_image_url: submissionData.card_image_url || null,
|
card_image_url: submissionData.card_image_url || null,
|
||||||
@@ -136,20 +100,10 @@ export function transformCompanyData(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform ride model submission data to database insert format
|
* Transform ride model submission data to database insert format
|
||||||
|
* Note: Technical specifications are now stored in the ride_model_technical_specifications
|
||||||
|
* table and should not be included in the main ride model insert.
|
||||||
*/
|
*/
|
||||||
export function transformRideModelData(submissionData: any): RideModelInsert {
|
export function transformRideModelData(submissionData: any): RideModelInsert {
|
||||||
let technicalSpecs = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (submissionData.technical_specs) {
|
|
||||||
technicalSpecs = typeof submissionData.technical_specs === 'string'
|
|
||||||
? JSON.parse(submissionData.technical_specs)
|
|
||||||
: submissionData.technical_specs;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse technical_specs:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: submissionData.name,
|
name: submissionData.name,
|
||||||
slug: submissionData.slug,
|
slug: submissionData.slug,
|
||||||
@@ -157,7 +111,6 @@ export function transformRideModelData(submissionData: any): RideModelInsert {
|
|||||||
category: submissionData.category,
|
category: submissionData.category,
|
||||||
ride_type: submissionData.ride_type || null,
|
ride_type: submissionData.ride_type || null,
|
||||||
description: submissionData.description || null,
|
description: submissionData.description || null,
|
||||||
technical_specs: technicalSpecs,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Phase 5: Advanced Search Performance Indexes
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Search indexes for technical specifications
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_search
|
||||||
|
ON ride_technical_specifications(ride_id, spec_name, spec_value);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_category
|
||||||
|
ON ride_technical_specifications(category, spec_name);
|
||||||
|
|
||||||
|
-- GIN index for flexible text search on spec values
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_tech_specs_value_gin
|
||||||
|
ON ride_technical_specifications USING gin(to_tsvector('english', spec_value));
|
||||||
|
|
||||||
|
-- Search indexes for coaster statistics
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_coaster_stats_search
|
||||||
|
ON ride_coaster_statistics(ride_id, stat_name, stat_value);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_coaster_stats_value_range
|
||||||
|
ON ride_coaster_statistics(stat_name, stat_value);
|
||||||
|
|
||||||
|
-- Composite index for rides filtering
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rides_search_composite
|
||||||
|
ON rides(category, status, manufacturer_id, park_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rides_speed_height
|
||||||
|
ON rides(max_speed_kmh, max_height_meters)
|
||||||
|
WHERE max_speed_kmh IS NOT NULL OR max_height_meters IS NOT NULL;
|
||||||
|
|
||||||
|
-- Ride model technical specifications indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ride_model_tech_specs_search
|
||||||
|
ON ride_model_technical_specifications(ride_model_id, spec_name, spec_value);
|
||||||
|
|
||||||
|
-- Park search optimization
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_parks_search_composite
|
||||||
|
ON parks(park_type, status, operator_id, property_owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_parks_location
|
||||||
|
ON parks(location_id) WHERE location_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Company search optimization
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_companies_type_search
|
||||||
|
ON companies(company_type, name);
|
||||||
|
|
||||||
|
-- Text search indexes for fuzzy matching
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rides_name_gin
|
||||||
|
ON rides USING gin(to_tsvector('english', name));
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_parks_name_gin
|
||||||
|
ON parks USING gin(to_tsvector('english', name));
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_companies_name_gin
|
||||||
|
ON companies USING gin(to_tsvector('english', name));
|
||||||
|
|
||||||
|
COMMENT ON INDEX idx_ride_tech_specs_search IS 'Optimizes technical specification searches by ride and spec name';
|
||||||
|
COMMENT ON INDEX idx_ride_coaster_stats_search IS 'Optimizes coaster statistics searches';
|
||||||
|
COMMENT ON INDEX idx_rides_search_composite IS 'Composite index for common ride search filters';
|
||||||
|
COMMENT ON INDEX idx_rides_name_gin IS 'Full-text search index for ride names';
|
||||||
Reference in New Issue
Block a user