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;
|
userId: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess: () => void;
|
onSuccess: (newCreditId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: AddRideCreditDialogProps) {
|
export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: AddRideCreditDialogProps) {
|
||||||
@@ -51,20 +51,22 @@ export function AddRideCreditDialog({ userId, open, onOpenChange, onSuccess }: A
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('user_ride_credits')
|
.from('user_ride_credits')
|
||||||
.insert({
|
.insert({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
ride_id: selectedRideId,
|
ride_id: selectedRideId,
|
||||||
first_ride_date: firstRideDate ? format(firstRideDate, 'yyyy-MM-dd') : null,
|
first_ride_date: firstRideDate ? format(firstRideDate, 'yyyy-MM-dd') : null,
|
||||||
ride_count: rideCount
|
ride_count: rideCount
|
||||||
});
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
toast.success('Ride credit added!');
|
toast.success('Ride credit added!');
|
||||||
handleReset();
|
handleReset();
|
||||||
onSuccess();
|
onSuccess(data.id); // Pass the new ID
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding credit:', error);
|
console.error('Error adding credit:', error);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ interface RideCreditCardProps {
|
|||||||
maxPosition?: number;
|
maxPosition?: number;
|
||||||
viewMode: 'grid' | 'list';
|
viewMode: 'grid' | 'list';
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean;
|
||||||
onUpdate: () => void;
|
onUpdate: (creditId: string, updates: Partial<UserRideCredit>) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onReorder?: (creditId: string, newPosition: number) => Promise<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');
|
toast.success('Ride count updated');
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
onUpdate();
|
// Optimistic update - pass specific changes
|
||||||
|
onUpdate(credit.id, { ride_count: editCount });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating count:', error);
|
console.error('Error updating count:', error);
|
||||||
toast.error(getErrorMessage(error));
|
toast.error(getErrorMessage(error));
|
||||||
@@ -78,6 +79,10 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
|
|||||||
const handleQuickIncrement = async () => {
|
const handleQuickIncrement = async () => {
|
||||||
try {
|
try {
|
||||||
const newCount = credit.ride_count + 1;
|
const newCount = credit.ride_count + 1;
|
||||||
|
|
||||||
|
// Optimistic update first
|
||||||
|
onUpdate(credit.id, { ride_count: newCount });
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('user_ride_credits')
|
.from('user_ride_credits')
|
||||||
.update({ ride_count: newCount })
|
.update({ ride_count: newCount })
|
||||||
@@ -86,10 +91,11 @@ export function RideCreditCard({ credit, position, maxPosition, viewMode, isEdit
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
toast.success('Ride count increased');
|
toast.success('Ride count increased');
|
||||||
onUpdate();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error incrementing count:', error);
|
console.error('Error incrementing count:', error);
|
||||||
toast.error(getErrorMessage(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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
@@ -139,16 +139,76 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreditAdded = () => {
|
const handleCreditAdded = async (newCreditId: string) => {
|
||||||
fetchCredits();
|
try {
|
||||||
setIsAddDialogOpen(false);
|
// 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 = () => {
|
const handleCreditUpdated = (creditId: string, updates: Partial<UserRideCredit>) => {
|
||||||
fetchCredits();
|
// Optimistic update - update only the affected credit
|
||||||
|
setCredits(prev => prev.map(c =>
|
||||||
|
c.id === creditId ? { ...c, ...updates } : c
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreditDeleted = async (creditId: string) => {
|
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 {
|
try {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('user_ride_credits')
|
.from('user_ride_credits')
|
||||||
@@ -159,10 +219,14 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
toast.success('Ride credit removed');
|
toast.success('Ride credit removed');
|
||||||
fetchCredits();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting credit:', error);
|
console.error('Error deleting credit:', error);
|
||||||
toast.error(getErrorMessage(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;
|
if (error) throw error;
|
||||||
|
|
||||||
// Refetch to get accurate sort_order values
|
// No refetch - optimistic update is already applied
|
||||||
await fetchCredits();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reordering credit:', error);
|
console.error('Error reordering credit:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -193,6 +256,9 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
|
|
||||||
if (oldIndex === -1 || newIndex === -1) return;
|
if (oldIndex === -1 || newIndex === -1) return;
|
||||||
|
|
||||||
|
// Store old order for rollback
|
||||||
|
const oldCredits = credits;
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
const newCredits = arrayMove(credits, oldIndex, newIndex);
|
const newCredits = arrayMove(credits, oldIndex, newIndex);
|
||||||
setCredits(newCredits);
|
setCredits(newCredits);
|
||||||
@@ -202,17 +268,18 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
toast.success('Order updated');
|
toast.success('Order updated');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(getErrorMessage(error));
|
toast.error(getErrorMessage(error));
|
||||||
// Revert on error
|
// Rollback to old order
|
||||||
fetchCredits();
|
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),
|
totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0),
|
||||||
uniqueCredits: credits.length,
|
uniqueCredits: credits.length,
|
||||||
parksVisited: new Set(credits.map(c => c.rides?.parks?.id).filter(Boolean)).size,
|
parksVisited: new Set(credits.map(c => c.rides?.parks?.id).filter(Boolean)).size,
|
||||||
coasters: credits.filter(c => c.rides?.category === 'roller_coaster').length
|
coasters: credits.filter(c => c.rides?.category === 'roller_coaster').length
|
||||||
};
|
}), [credits]);
|
||||||
|
|
||||||
// Use filtered credits for display
|
// Use filtered credits for display
|
||||||
const displayCredits = filteredCredits;
|
const displayCredits = filteredCredits;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface SortableRideCreditCardProps {
|
|||||||
position: number;
|
position: number;
|
||||||
maxPosition: number;
|
maxPosition: number;
|
||||||
viewMode: 'grid' | 'list';
|
viewMode: 'grid' | 'list';
|
||||||
onUpdate: () => void;
|
onUpdate: (creditId: string, updates: Partial<UserRideCredit>) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onReorder: (creditId: string, newPosition: number) => Promise<void>;
|
onReorder: (creditId: string, newPosition: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user