mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
feat: Add track material column and filtering
This commit is contained in:
@@ -618,6 +618,26 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Track Material</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => setValue('track_material', value === '' ? undefined : value as 'wood' | 'steel' | 'hybrid' | 'aluminum' | 'other')}
|
||||||
|
defaultValue={initialData?.track_material || ''}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select track material" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="">None</SelectItem>
|
||||||
|
<SelectItem value="wood">Wood</SelectItem>
|
||||||
|
<SelectItem value="steel">Steel</SelectItem>
|
||||||
|
<SelectItem value="hybrid">Hybrid (Wood/Steel)</SelectItem>
|
||||||
|
<SelectItem value="aluminum">Aluminum</SelectItem>
|
||||||
|
<SelectItem value="other">Other</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export function RideCreditFilters({
|
|||||||
const cities = new Set<string>();
|
const cities = new Set<string>();
|
||||||
const parks = new Map<string, string>();
|
const parks = new Map<string, string>();
|
||||||
const manufacturers = new Map<string, string>();
|
const manufacturers = new Map<string, string>();
|
||||||
|
const coasterTypes = new Set<string>();
|
||||||
|
const seatingTypes = new Set<string>();
|
||||||
|
const intensityLevels = new Set<string>();
|
||||||
|
const trackMaterials = new Set<string>();
|
||||||
|
|
||||||
credits.forEach(credit => {
|
credits.forEach(credit => {
|
||||||
const location = credit.rides?.parks?.locations;
|
const location = credit.rides?.parks?.locations;
|
||||||
@@ -56,6 +60,22 @@ export function RideCreditFilters({
|
|||||||
if (credit.rides?.manufacturer?.id && credit.rides?.manufacturer?.name) {
|
if (credit.rides?.manufacturer?.id && credit.rides?.manufacturer?.name) {
|
||||||
manufacturers.set(credit.rides.manufacturer.id, credit.rides.manufacturer.name);
|
manufacturers.set(credit.rides.manufacturer.id, credit.rides.manufacturer.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credit.rides?.coaster_type) {
|
||||||
|
coasterTypes.add(credit.rides.coaster_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credit.rides?.seating_type) {
|
||||||
|
seatingTypes.add(credit.rides.seating_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credit.rides?.intensity_level) {
|
||||||
|
intensityLevels.add(credit.rides.intensity_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credit.rides?.track_material) {
|
||||||
|
trackMaterials.add(credit.rides.track_material);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -64,6 +84,10 @@ export function RideCreditFilters({
|
|||||||
cities: Array.from(cities).sort(),
|
cities: Array.from(cities).sort(),
|
||||||
parks: Array.from(parks.entries()).map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name)),
|
parks: Array.from(parks.entries()).map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
manufacturers: Array.from(manufacturers.entries()).map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name)),
|
manufacturers: Array.from(manufacturers.entries()).map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
coasterTypes: Array.from(coasterTypes).sort(),
|
||||||
|
seatingTypes: Array.from(seatingTypes).sort(),
|
||||||
|
intensityLevels: Array.from(intensityLevels).sort(),
|
||||||
|
trackMaterials: Array.from(trackMaterials).sort(),
|
||||||
};
|
};
|
||||||
}, [credits]);
|
}, [credits]);
|
||||||
|
|
||||||
@@ -295,6 +319,86 @@ export function RideCreditFilters({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{filterOptions.coasterTypes.length > 0 && (
|
||||||
|
<div className={sectionSpacing}>
|
||||||
|
<Label className="text-sm font-medium">Coaster Type</Label>
|
||||||
|
<MultiSelectCombobox
|
||||||
|
options={filterOptions.coasterTypes.map(type => ({
|
||||||
|
label: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
value: type
|
||||||
|
}))}
|
||||||
|
value={filters.coasterTypes || []}
|
||||||
|
onValueChange={(values) =>
|
||||||
|
onFilterChange('coasterTypes', values.length > 0 ? values : undefined)
|
||||||
|
}
|
||||||
|
placeholder="Select coaster types..."
|
||||||
|
searchPlaceholder="Search types..."
|
||||||
|
emptyText="No coaster types found"
|
||||||
|
className={compact ? "h-9 text-sm" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterOptions.seatingTypes.length > 0 && (
|
||||||
|
<div className={sectionSpacing}>
|
||||||
|
<Label className="text-sm font-medium">Seating Type</Label>
|
||||||
|
<MultiSelectCombobox
|
||||||
|
options={filterOptions.seatingTypes.map(type => ({
|
||||||
|
label: type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||||
|
value: type
|
||||||
|
}))}
|
||||||
|
value={filters.seatingTypes || []}
|
||||||
|
onValueChange={(values) =>
|
||||||
|
onFilterChange('seatingTypes', values.length > 0 ? values : undefined)
|
||||||
|
}
|
||||||
|
placeholder="Select seating types..."
|
||||||
|
searchPlaceholder="Search types..."
|
||||||
|
emptyText="No seating types found"
|
||||||
|
className={compact ? "h-9 text-sm" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterOptions.intensityLevels.length > 0 && (
|
||||||
|
<div className={sectionSpacing}>
|
||||||
|
<Label className="text-sm font-medium">Intensity Level</Label>
|
||||||
|
<MultiSelectCombobox
|
||||||
|
options={filterOptions.intensityLevels.map(level => ({
|
||||||
|
label: level.charAt(0).toUpperCase() + level.slice(1),
|
||||||
|
value: level
|
||||||
|
}))}
|
||||||
|
value={filters.intensityLevels || []}
|
||||||
|
onValueChange={(values) =>
|
||||||
|
onFilterChange('intensityLevels', values.length > 0 ? values : undefined)
|
||||||
|
}
|
||||||
|
placeholder="Select intensity levels..."
|
||||||
|
searchPlaceholder="Search intensity..."
|
||||||
|
emptyText="No intensity levels found"
|
||||||
|
className={compact ? "h-9 text-sm" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterOptions.trackMaterials.length > 0 && (
|
||||||
|
<div className={sectionSpacing}>
|
||||||
|
<Label className="text-sm font-medium">Track Material</Label>
|
||||||
|
<MultiSelectCombobox
|
||||||
|
options={filterOptions.trackMaterials.map(tm => ({
|
||||||
|
label: tm.charAt(0).toUpperCase() + tm.slice(1),
|
||||||
|
value: tm
|
||||||
|
}))}
|
||||||
|
value={filters.trackMaterial || []}
|
||||||
|
onValueChange={(values) =>
|
||||||
|
onFilterChange('trackMaterial', values.length > 0 ? values : undefined)
|
||||||
|
}
|
||||||
|
placeholder="Select track materials..."
|
||||||
|
searchPlaceholder="Search materials..."
|
||||||
|
emptyText="No track materials found"
|
||||||
|
className={compact ? "h-9 text-sm" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Inversions</Label>
|
<Label>Inversions</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -160,6 +160,38 @@ export function useRideCreditFilters(credits: UserRideCredit[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coaster types filter
|
||||||
|
if (filters.coasterTypes && filters.coasterTypes.length > 0) {
|
||||||
|
result = result.filter(credit =>
|
||||||
|
credit.rides?.coaster_type &&
|
||||||
|
filters.coasterTypes!.includes(credit.rides.coaster_type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seating types filter
|
||||||
|
if (filters.seatingTypes && filters.seatingTypes.length > 0) {
|
||||||
|
result = result.filter(credit =>
|
||||||
|
credit.rides?.seating_type &&
|
||||||
|
filters.seatingTypes!.includes(credit.rides.seating_type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intensity levels filter
|
||||||
|
if (filters.intensityLevels && filters.intensityLevels.length > 0) {
|
||||||
|
result = result.filter(credit =>
|
||||||
|
credit.rides?.intensity_level &&
|
||||||
|
filters.intensityLevels!.includes(credit.rides.intensity_level)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track material filter
|
||||||
|
if (filters.trackMaterial && filters.trackMaterial.length > 0) {
|
||||||
|
result = result.filter(credit =>
|
||||||
|
credit.rides?.track_material &&
|
||||||
|
filters.trackMaterial!.includes(credit.rides.track_material)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// User rating
|
// User rating
|
||||||
if (filters.hasRating === true) {
|
if (filters.hasRating === true) {
|
||||||
result = result.filter(credit => credit.personal_rating !== null);
|
result = result.filter(credit => credit.personal_rating !== null);
|
||||||
|
|||||||
@@ -2487,6 +2487,7 @@ export type Database = {
|
|||||||
slug: string
|
slug: string
|
||||||
status: string
|
status: string
|
||||||
submission_id: string
|
submission_id: string
|
||||||
|
track_material: string | null
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -2525,6 +2526,7 @@ export type Database = {
|
|||||||
slug: string
|
slug: string
|
||||||
status?: string
|
status?: string
|
||||||
submission_id: string
|
submission_id: string
|
||||||
|
track_material?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -2563,6 +2565,7 @@ export type Database = {
|
|||||||
slug?: string
|
slug?: string
|
||||||
status?: string
|
status?: string
|
||||||
submission_id?: string
|
submission_id?: string
|
||||||
|
track_material?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -2656,6 +2659,7 @@ export type Database = {
|
|||||||
slug: string
|
slug: string
|
||||||
status: string
|
status: string
|
||||||
submission_id: string | null
|
submission_id: string | null
|
||||||
|
track_material: string | null
|
||||||
version_id: string
|
version_id: string
|
||||||
version_number: number
|
version_number: number
|
||||||
}
|
}
|
||||||
@@ -2695,6 +2699,7 @@ export type Database = {
|
|||||||
slug: string
|
slug: string
|
||||||
status: string
|
status: string
|
||||||
submission_id?: string | null
|
submission_id?: string | null
|
||||||
|
track_material?: string | null
|
||||||
version_id?: string
|
version_id?: string
|
||||||
version_number: number
|
version_number: number
|
||||||
}
|
}
|
||||||
@@ -2734,6 +2739,7 @@ export type Database = {
|
|||||||
slug?: string
|
slug?: string
|
||||||
status?: string
|
status?: string
|
||||||
submission_id?: string | null
|
submission_id?: string | null
|
||||||
|
track_material?: string | null
|
||||||
version_id?: string
|
version_id?: string
|
||||||
version_number?: number
|
version_number?: number
|
||||||
}
|
}
|
||||||
@@ -2827,6 +2833,7 @@ export type Database = {
|
|||||||
seating_type: string | null
|
seating_type: string | null
|
||||||
slug: string
|
slug: string
|
||||||
status: string
|
status: string
|
||||||
|
track_material: string | null
|
||||||
updated_at: string
|
updated_at: string
|
||||||
view_count_30d: number | null
|
view_count_30d: number | null
|
||||||
view_count_7d: number | null
|
view_count_7d: number | null
|
||||||
@@ -2869,6 +2876,7 @@ export type Database = {
|
|||||||
seating_type?: string | null
|
seating_type?: string | null
|
||||||
slug: string
|
slug: string
|
||||||
status?: string
|
status?: string
|
||||||
|
track_material?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
view_count_30d?: number | null
|
view_count_30d?: number | null
|
||||||
view_count_7d?: number | null
|
view_count_7d?: number | null
|
||||||
@@ -2911,6 +2919,7 @@ export type Database = {
|
|||||||
seating_type?: string | null
|
seating_type?: string | null
|
||||||
slug?: string
|
slug?: string
|
||||||
status?: string
|
status?: string
|
||||||
|
track_material?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
view_count_30d?: number | null
|
view_count_30d?: number | null
|
||||||
view_count_7d?: number | null
|
view_count_7d?: number | null
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export const rideValidationSchema = z.object({
|
|||||||
coaster_type: z.string().optional(),
|
coaster_type: z.string().optional(),
|
||||||
seating_type: z.string().optional(),
|
seating_type: z.string().optional(),
|
||||||
intensity_level: z.string().optional(),
|
intensity_level: z.string().optional(),
|
||||||
|
track_material: z.enum(['wood', 'steel', 'hybrid', 'aluminum', 'other']).optional(),
|
||||||
banner_image_id: z.string().optional(),
|
banner_image_id: z.string().optional(),
|
||||||
banner_image_url: z.string().optional(),
|
banner_image_url: z.string().optional(),
|
||||||
card_image_id: z.string().optional(),
|
card_image_id: z.string().optional(),
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export interface Ride {
|
|||||||
coaster_type?: string;
|
coaster_type?: string;
|
||||||
seating_type?: string;
|
seating_type?: string;
|
||||||
intensity_level?: string;
|
intensity_level?: string;
|
||||||
|
track_material?: string;
|
||||||
drop_height_meters?: number;
|
drop_height_meters?: number;
|
||||||
max_g_force?: number;
|
max_g_force?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const RIDE_FIELDS = [
|
|||||||
'capacity_per_hour', 'duration_seconds', 'max_speed_kmh',
|
'capacity_per_hour', 'duration_seconds', 'max_speed_kmh',
|
||||||
'max_height_meters', 'length_meters', 'inversions',
|
'max_height_meters', 'length_meters', 'inversions',
|
||||||
'ride_sub_type', 'coaster_type', 'seating_type', 'intensity_level',
|
'ride_sub_type', 'coaster_type', 'seating_type', 'intensity_level',
|
||||||
'drop_height_meters', 'max_g_force', 'image_url',
|
'track_material', 'drop_height_meters', 'max_g_force', 'image_url',
|
||||||
'banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'
|
'banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Add track_material column to rides table
|
||||||
|
ALTER TABLE rides
|
||||||
|
ADD COLUMN track_material text;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rides.track_material IS 'Material used for the ride track (wood, steel, hybrid, aluminum, etc.)';
|
||||||
|
|
||||||
|
-- Add track_material to ride_versions table for version tracking
|
||||||
|
ALTER TABLE ride_versions
|
||||||
|
ADD COLUMN track_material text;
|
||||||
|
|
||||||
|
-- Add track_material to ride_submissions table
|
||||||
|
ALTER TABLE ride_submissions
|
||||||
|
ADD COLUMN track_material text;
|
||||||
|
|
||||||
|
-- Create an index for filtering
|
||||||
|
CREATE INDEX idx_rides_track_material ON rides(track_material)
|
||||||
|
WHERE track_material IS NOT NULL;
|
||||||
Reference in New Issue
Block a user