Refactor: Implement optimistic updates

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 15:43:52 +00:00
parent 0d9926a5ae
commit b95b06a7f2
4 changed files with 96 additions and 21 deletions

View File

@@ -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);

View File

@@ -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 });
}
};

View File

@@ -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;

View File

@@ -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>;
}