mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 10:11:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
539
src-old/components/profile/RideCreditsManager.tsx
Normal file
539
src-old/components/profile/RideCreditsManager.tsx
Normal file
@@ -0,0 +1,539 @@
|
||||
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 '@/lib/supabaseClient';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } 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 { useIsMobile } from '@/hooks/use-mobile';
|
||||
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<UserRideCredit[]>([]);
|
||||
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);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// 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,
|
||||
average_rating,
|
||||
review_count,
|
||||
parks:park_id (
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
park_type,
|
||||
locations:location_id (
|
||||
country,
|
||||
state_province,
|
||||
city
|
||||
)
|
||||
),
|
||||
manufacturer:companies!rides_manufacturer_id_fkey (
|
||||
id,
|
||||
name
|
||||
),
|
||||
designer:companies!rides_designer_id_fkey (
|
||||
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 || [];
|
||||
|
||||
// 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 as UserRideCredit[]);
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Fetch Ride Credits',
|
||||
userId,
|
||||
metadata: { sortBy }
|
||||
});
|
||||
} 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;
|
||||
|
||||
// Type assertion for complex joined data
|
||||
const newCredit = data as unknown as UserRideCredit;
|
||||
|
||||
// Add to existing array
|
||||
setCredits(prev => [...prev, newCredit]);
|
||||
setIsAddDialogOpen(false);
|
||||
} catch (error: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Add Ride Credit',
|
||||
userId,
|
||||
metadata: { creditId: newCreditId }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreditUpdated = (creditId: string, updates: Partial<UserRideCredit>) => {
|
||||
// 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: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Delete Ride Credit',
|
||||
userId,
|
||||
metadata: { creditId }
|
||||
});
|
||||
|
||||
// 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: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Reorder Ride Credit',
|
||||
userId,
|
||||
metadata: { creditId, newPosition }
|
||||
});
|
||||
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: unknown) {
|
||||
handleError(error, {
|
||||
action: 'Drag Reorder Ride Credit',
|
||||
userId,
|
||||
metadata: {
|
||||
activeId: String(active.id),
|
||||
overId: String(over.id),
|
||||
oldIndex,
|
||||
newIndex
|
||||
}
|
||||
});
|
||||
// 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 (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 6 }, (_, i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardContent className="p-6">
|
||||
<div className="h-32 bg-muted rounded"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{stats.totalRides}</div>
|
||||
<div className="text-sm text-muted-foreground">Total Rides</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{stats.uniqueCredits}</div>
|
||||
<div className="text-sm text-muted-foreground">Unique Credits</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{stats.parksVisited}</div>
|
||||
<div className="text-sm text-muted-foreground">Parks Visited</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{stats.coasters}</div>
|
||||
<div className="text-sm text-muted-foreground">Coasters</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area with Sidebar */}
|
||||
<div className="flex gap-6 items-start">
|
||||
{/* Left Sidebar - Desktop Only */}
|
||||
{!isMobile && (
|
||||
<aside className="w-80 flex-shrink-0">
|
||||
<div className="sticky top-4">
|
||||
<div className="rounded-lg border bg-card">
|
||||
<RideCreditFilters
|
||||
filters={filters}
|
||||
onFilterChange={updateFilter}
|
||||
onClearFilters={clearFilters}
|
||||
credits={credits}
|
||||
activeFilterCount={activeFilterCount}
|
||||
resultCount={displayCredits.length}
|
||||
totalCount={credits.length}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* Right Content Area */}
|
||||
<div className="flex-1 min-w-0 space-y-4">
|
||||
{/* Mobile Filters - Collapsible Card */}
|
||||
{isMobile && (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<RideCreditFilters
|
||||
filters={filters}
|
||||
onFilterChange={updateFilter}
|
||||
onClearFilters={clearFilters}
|
||||
credits={credits}
|
||||
activeFilterCount={activeFilterCount}
|
||||
resultCount={displayCredits.length}
|
||||
totalCount={credits.length}
|
||||
compact={false}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-wrap gap-4 items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setIsAddDialogOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Credit
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={isEditMode ? 'default' : 'outline'}
|
||||
onClick={() => setIsEditMode(!isEditMode)}
|
||||
>
|
||||
<GripVertical className="w-4 h-4 mr-2" />
|
||||
{isEditMode ? 'Done Editing' : 'Edit Order'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Select value={sortBy} onValueChange={(value) => setSortBy(value as typeof sortBy)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="custom">Custom Order</SelectItem>
|
||||
<SelectItem value="date">Most Recent</SelectItem>
|
||||
<SelectItem value="count">Most Ridden</SelectItem>
|
||||
<SelectItem value="name">Ride Name</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex border rounded-md">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('grid')}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credits Display */}
|
||||
{displayCredits.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12">
|
||||
<div className="text-center">
|
||||
{credits.length === 0 ? (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold mb-2">No Ride Credits Yet</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Start tracking your ride experiences
|
||||
</p>
|
||||
<Button onClick={() => setIsAddDialogOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Your First Credit
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold mb-2">No Results Found</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Try adjusting your filters
|
||||
</p>
|
||||
<Button onClick={clearFilters} variant="outline">
|
||||
Clear Filters
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isEditMode ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={displayCredits.map(c => c.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className={viewMode === 'grid'
|
||||
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{displayCredits.map((credit, index) => (
|
||||
<SortableRideCreditCard
|
||||
key={credit.id}
|
||||
credit={credit}
|
||||
position={index + 1}
|
||||
maxPosition={displayCredits.length}
|
||||
viewMode={viewMode}
|
||||
onUpdate={handleCreditUpdated}
|
||||
onDelete={() => handleCreditDeleted(credit.id)}
|
||||
onReorder={handleReorder}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
<div className={viewMode === 'grid'
|
||||
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{displayCredits.map((credit, index) => (
|
||||
<RideCreditCard
|
||||
key={credit.id}
|
||||
credit={credit}
|
||||
position={index + 1}
|
||||
viewMode={viewMode}
|
||||
onUpdate={handleCreditUpdated}
|
||||
onDelete={() => handleCreditDeleted(credit.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Credit Dialog */}
|
||||
<AddRideCreditDialog
|
||||
userId={userId}
|
||||
open={isAddDialogOpen}
|
||||
onOpenChange={setIsAddDialogOpen}
|
||||
onSuccess={handleCreditAdded}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user