From b95b06a7f2d071042368bb608cb1649885117957 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:43:52 +0000 Subject: [PATCH] Refactor: Implement optimistic updates --- .../profile/AddRideCreditDialog.tsx | 10 +- src/components/profile/RideCreditCard.tsx | 12 ++- src/components/profile/RideCreditsManager.tsx | 93 ++++++++++++++++--- .../profile/SortableRideCreditCard.tsx | 2 +- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/components/profile/AddRideCreditDialog.tsx b/src/components/profile/AddRideCreditDialog.tsx index 49472bb8..ce898b6a 100644 --- a/src/components/profile/AddRideCreditDialog.tsx +++ b/src/components/profile/AddRideCreditDialog.tsx @@ -17,7 +17,7 @@ interface AddRideCreditDialogProps { userId: string; open: boolean; onOpenChange: (open: boolean) => void; - onSuccess: () => void; + onSuccess: (newCreditId: string) => void; } export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: AddRideCreditDialogProps) { @@ -51,20 +51,22 @@ export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: A return; } - const { error } = await supabase + const { data, error } = await supabase .from('user_ride_credits') .insert({ user_id: userId, ride_id: selectedRideId, first_ride_date: firstRideDate ? format(firstRideDate, 'yyyy-MM-dd') : null, ride_count: rideCount - }); + }) + .select('id') + .single(); if (error) throw error; toast.success('Ride credit added!'); handleReset(); - onSuccess(); + onSuccess(data.id); // Pass the new ID onOpenChange(false); } catch (error) { console.error('Error adding credit:', error); diff --git a/src/components/profile/RideCreditCard.tsx b/src/components/profile/RideCreditCard.tsx index fe480b62..8f8508ca 100644 --- a/src/components/profile/RideCreditCard.tsx +++ b/src/components/profile/RideCreditCard.tsx @@ -28,7 +28,7 @@ interface RideCreditCardProps { maxPosition?: number; viewMode: 'grid' | 'list'; isEditMode?: boolean; - onUpdate: () => void; + onUpdate: (creditId: string, updates: Partial) => void; onDelete: () => void; onReorder?: (creditId: string, newPosition: number) => Promise; } @@ -66,7 +66,8 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit toast.success('Ride count updated'); setIsEditing(false); - onUpdate(); + // Optimistic update - pass specific changes + onUpdate(credit.id, { ride_count: editCount }); } catch (error) { console.error('Error updating count:', error); toast.error(getErrorMessage(error)); @@ -78,6 +79,10 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit const handleQuickIncrement = async () => { try { const newCount = credit.ride_count + 1; + + // Optimistic update first + onUpdate(credit.id, { ride_count: newCount }); + const { error } = await supabase .from('user_ride_credits') .update({ ride_count: newCount }) @@ -86,10 +91,11 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit if (error) throw error; toast.success('Ride count increased'); - onUpdate(); } catch (error) { console.error('Error incrementing count:', error); toast.error(getErrorMessage(error)); + // Rollback on error + onUpdate(credit.id, { ride_count: credit.ride_count }); } }; diff --git a/src/components/profile/RideCreditsManager.tsx b/src/components/profile/RideCreditsManager.tsx index 8bb84b1f..cbad1b15 100644 --- a/src/components/profile/RideCreditsManager.tsx +++ b/src/components/profile/RideCreditsManager.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +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'; @@ -139,16 +139,76 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { } }; - const handleCreditAdded = () => { - fetchCredits(); - setIsAddDialogOpen(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 = () => { - fetchCredits(); + 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') @@ -159,10 +219,14 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { if (error) throw error; toast.success('Ride credit removed'); - fetchCredits(); } catch (error) { console.error('Error deleting credit:', error); toast.error(getErrorMessage(error)); + + // Rollback on error + if (deletedCredit) { + setCredits(prev => [...prev, deletedCredit]); + } } }; @@ -175,8 +239,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { if (error) throw error; - // Refetch to get accurate sort_order values - await fetchCredits(); + // No refetch - optimistic update is already applied } catch (error) { console.error('Error reordering credit:', error); throw error; @@ -193,6 +256,9 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { if (oldIndex === -1 || newIndex === -1) return; + // Store old order for rollback + const oldCredits = credits; + // Optimistic update const newCredits = arrayMove(credits, oldIndex, newIndex); setCredits(newCredits); @@ -202,17 +268,18 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) { toast.success('Order updated'); } catch (error) { toast.error(getErrorMessage(error)); - // Revert on error - fetchCredits(); + // Rollback to old order + setCredits(oldCredits); } }; - const stats = { + // 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; diff --git a/src/components/profile/SortableRideCreditCard.tsx b/src/components/profile/SortableRideCreditCard.tsx index b22ebf50..8f9678b9 100644 --- a/src/components/profile/SortableRideCreditCard.tsx +++ b/src/components/profile/SortableRideCreditCard.tsx @@ -9,7 +9,7 @@ interface SortableRideCreditCardProps { position: number; maxPosition: number; viewMode: 'grid' | 'list'; - onUpdate: () => void; + onUpdate: (creditId: string, updates: Partial) => void; onDelete: () => void; onReorder: (creditId: string, newPosition: number) => Promise; }