mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:31:12 -05:00
185 lines
5.8 KiB
TypeScript
185 lines
5.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
import { TechnicalSpecFilter, CoasterStatFilter } from '@/components/search/AdvancedRideFilters';
|
|
import { useDebounce } from './useDebounce';
|
|
import { handleError } from '@/lib/errorHandler';
|
|
|
|
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) {
|
|
handleError(err, {
|
|
action: 'Advanced Ride Search',
|
|
metadata: {
|
|
query: options.query,
|
|
category: options.category,
|
|
manufacturer: options.manufacturer
|
|
}
|
|
});
|
|
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 };
|
|
} |