import { useState, useEffect, useMemo } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Plus, LayoutGrid, List, GripVertical } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { toast } from 'sonner'; import { getErrorMessage } from '@/lib/errorHandler'; import { AddRideCreditDialog } from './AddRideCreditDialog'; import { RideCreditCard } from './RideCreditCard'; import { SortableRideCreditCard } from './SortableRideCreditCard'; import { RideCreditFilters } from './RideCreditFilters'; import { UserRideCredit } from '@/types/database'; import { useRideCreditFilters } from '@/hooks/useRideCreditFilters'; import { DndContext, DragEndEvent, closestCenter, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, arrayMove, } from '@dnd-kit/sortable'; interface RideCreditsManagerProps { userId: string; } export function RideCreditsManager({ userId }: RideCreditsManagerProps) { const [credits, setCredits] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [sortBy, setSortBy] = useState<'date' | 'count' | 'name' | 'custom'>('custom'); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); // Use the filter hook const { filters, updateFilter, clearFilters, filteredCredits, activeFilterCount, } = useRideCreditFilters(credits); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ); useEffect(() => { fetchCredits(); }, [userId, sortBy]); const fetchCredits = async () => { try { setLoading(true); // Build main query with enhanced data let query = supabase .from('user_ride_credits') .select(` *, rides:ride_id ( id, name, slug, category, status, coaster_type, seating_type, intensity_level, max_speed_kmh, max_height_meters, length_meters, inversions, card_image_url, parks:park_id ( id, name, slug, park_type, locations:location_id ( country, state_province, city ) ), manufacturer:manufacturer_id ( id, name ), designer:designer_id ( id, name ) ) `) .eq('user_id', userId); // Apply sorting - default to sort_order when available if (sortBy === 'date') { query = query.order('first_ride_date', { ascending: false, nullsFirst: false }); } else if (sortBy === 'count') { query = query.order('ride_count', { ascending: false }); } else if (sortBy === 'name') { query = query.order('created_at', { ascending: false }); } else { query = query.order('sort_order', { ascending: true, nullsFirst: false }); } const { data, error } = await query; if (error) throw error; let processedData = (data || []) as any[]; // Sort by name client-side if needed if (sortBy === 'name') { processedData.sort((a, b) => { const nameA = a.rides?.name || ''; const nameB = b.rides?.name || ''; return nameA.localeCompare(nameB); }); } setCredits(processedData); } catch (error) { console.error('Error fetching ride credits:', error); toast.error(getErrorMessage(error)); } finally { setLoading(false); } }; const handleCreditAdded = async (newCreditId: string) => { try { // Fetch just the new credit with all necessary joins const { data, error } = await supabase .from('user_ride_credits') .select(` *, rides:ride_id ( id, name, slug, category, status, coaster_type, seating_type, intensity_level, max_speed_kmh, max_height_meters, length_meters, inversions, card_image_url, parks:park_id ( id, name, slug, park_type, locations:location_id ( country, state_province, city ) ), manufacturer:manufacturer_id ( id, name ), designer:designer_id ( id, name ) ) `) .eq('id', newCreditId) .single(); if (error) throw error; // Add to existing array setCredits(prev => [...prev, data as any]); setIsAddDialogOpen(false); } catch (error) { console.error('Error fetching new credit:', error); toast.error(getErrorMessage(error)); } }; const handleCreditUpdated = (creditId: string, updates: Partial) => { // Optimistic update - update only the affected credit setCredits(prev => prev.map(c => c.id === creditId ? { ...c, ...updates } : c )); }; const handleCreditDeleted = async (creditId: string) => { // Store for rollback const deletedCredit = credits.find(c => c.id === creditId); // Optimistic removal setCredits(prev => prev.filter(c => c.id !== creditId)); try { const { error } = await supabase .from('user_ride_credits') .delete() .eq('id', creditId) .eq('user_id', userId); if (error) throw error; toast.success('Ride credit removed'); } catch (error) { console.error('Error deleting credit:', error); toast.error(getErrorMessage(error)); // Rollback on error if (deletedCredit) { setCredits(prev => [...prev, deletedCredit]); } } }; const handleReorder = async (creditId: string, newPosition: number) => { try { const { error } = await supabase.rpc('reorder_ride_credit', { p_credit_id: creditId, p_new_position: newPosition }); if (error) throw error; // No refetch - optimistic update is already applied } catch (error) { console.error('Error reordering credit:', error); throw error; } }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = credits.findIndex(c => c.id === String(active.id)); const newIndex = credits.findIndex(c => c.id === String(over.id)); if (oldIndex === -1 || newIndex === -1) return; // Store old order for rollback const oldCredits = credits; // Optimistic update const newCredits = arrayMove(credits, oldIndex, newIndex); setCredits(newCredits); try { await handleReorder(String(active.id), newIndex + 1); toast.success('Order updated'); } catch (error) { toast.error(getErrorMessage(error)); // Rollback to old order setCredits(oldCredits); } }; // Memoize statistics to only recalculate when credits change const stats = useMemo(() => ({ totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0), uniqueCredits: credits.length, parksVisited: new Set(credits.map(c => c.rides?.parks?.id).filter(Boolean)).size, coasters: credits.filter(c => c.rides?.category === 'roller_coaster').length }), [credits]); // Use filtered credits for display const displayCredits = filteredCredits; if (loading) { return (
{Array.from({ length: 6 }, (_, i) => (
))}
); } return (
{/* Statistics */}
{stats.totalRides}
Total Rides
{stats.uniqueCredits}
Unique Credits
{stats.parksVisited}
Parks Visited
{stats.coasters}
Coasters
{/* Filters */} {/* Controls */}
{/* Credits Display */} {displayCredits.length === 0 ? (
{credits.length === 0 ? ( <>

No Ride Credits Yet

Start tracking your ride experiences

) : ( <>

No Results Found

Try adjusting your filters

)}
) : isEditMode ? ( c.id)} strategy={verticalListSortingStrategy} >
{displayCredits.map((credit, index) => ( handleCreditDeleted(credit.id)} onReorder={handleReorder} /> ))}
) : (
{displayCredits.map((credit, index) => ( handleCreditDeleted(credit.id)} /> ))}
)} {/* Add Credit Dialog */}
); }