mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
Refactor: Implement optimistic updates
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -28,7 +28,7 @@ interface RideCreditCardProps {
|
||||
maxPosition?: number;
|
||||
viewMode: 'grid' | 'list';
|
||||
isEditMode?: boolean;
|
||||
onUpdate: () => void;
|
||||
onUpdate: (creditId: string, updates: Partial<UserRideCredit>) => void;
|
||||
onDelete: () => void;
|
||||
onReorder?: (creditId: string, newPosition: number) => Promise<void>;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
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<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')
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,7 @@ interface SortableRideCreditCardProps {
|
||||
position: number;
|
||||
maxPosition: number;
|
||||
viewMode: 'grid' | 'list';
|
||||
onUpdate: () => void;
|
||||
onUpdate: (creditId: string, updates: Partial<UserRideCredit>) => void;
|
||||
onDelete: () => void;
|
||||
onReorder: (creditId: string, newPosition: number) => Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user