mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 20:31:13 -05:00
Improve image handling, optimize hooks, and add rate limiting
This commit introduces several improvements: - Enhances `RideModelCard` by safely accessing and displaying ride count and image data, preventing potential errors. - Refactors `useEntityVersions` and `useSearch` hooks to use `useCallback` and improve performance and prevent race conditions. - Introduces a `MAX_MAP_SIZE` and cleanup mechanism for the rate limiting map in `detect-location` Supabase function to prevent memory leaks. - Adds robust error handling and cleanup for image uploads in `uploadPendingImages`. - Modifies `ManufacturerModels` to correctly map and display ride counts. - Includes error handling for topological sort in `process-selective-approval` Supabase function. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 39bb006b-d046-477f-a1f9-b7821836f3a1 Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -42,10 +42,15 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
|
||||
// Track the current channel to prevent duplicate subscriptions
|
||||
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
|
||||
|
||||
// Track if a fetch is in progress to prevent race conditions
|
||||
const fetchInProgressRef = useRef(false);
|
||||
|
||||
const fetchVersions = async () => {
|
||||
const fetchVersions = useCallback(async () => {
|
||||
try {
|
||||
if (!isMountedRef.current) return;
|
||||
if (!isMountedRef.current || fetchInProgressRef.current) return;
|
||||
|
||||
fetchInProgressRef.current = true;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -87,11 +92,12 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
toast.error('Failed to load version history');
|
||||
}
|
||||
} finally {
|
||||
fetchInProgressRef.current = false;
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [entityType, entityId]);
|
||||
|
||||
const fetchFieldHistory = async (versionId: string) => {
|
||||
try {
|
||||
@@ -195,7 +201,7 @@ export function useEntityVersions(entityType: string, entityId: string) {
|
||||
if (entityType && entityId) {
|
||||
fetchVersions();
|
||||
}
|
||||
}, [entityType, entityId]);
|
||||
}, [entityType, entityId, fetchVersions]);
|
||||
|
||||
// Set up realtime subscription for version changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { Park, Ride, Company } from '@/types/database';
|
||||
|
||||
@@ -20,13 +20,23 @@ interface UseSearchOptions {
|
||||
debounceMs?: number;
|
||||
}
|
||||
|
||||
// Hoist default values to prevent recreating on every render
|
||||
const DEFAULT_TYPES: ('park' | 'ride' | 'company')[] = ['park', 'ride', 'company'];
|
||||
const DEFAULT_LIMIT = 10;
|
||||
const DEFAULT_MIN_QUERY = 2;
|
||||
const DEFAULT_DEBOUNCE_MS = 300;
|
||||
|
||||
export function useSearch(options: UseSearchOptions = {}) {
|
||||
const {
|
||||
types = ['park', 'ride', 'company'],
|
||||
limit = 10,
|
||||
minQuery = 2,
|
||||
debounceMs = 300
|
||||
} = options;
|
||||
// Stabilize options using JSON stringify to prevent infinite loops from array recreation
|
||||
const optionsKey = JSON.stringify({
|
||||
types: options.types || DEFAULT_TYPES,
|
||||
limit: options.limit || DEFAULT_LIMIT,
|
||||
minQuery: options.minQuery || DEFAULT_MIN_QUERY,
|
||||
debounceMs: options.debounceMs || DEFAULT_DEBOUNCE_MS
|
||||
});
|
||||
|
||||
const stableOptions = useMemo(() => JSON.parse(optionsKey), [optionsKey]);
|
||||
const { types, limit, minQuery, debounceMs } = stableOptions;
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
@@ -61,7 +71,7 @@ export function useSearch(options: UseSearchOptions = {}) {
|
||||
}, []);
|
||||
|
||||
// Search function
|
||||
const search = async (searchQuery: string) => {
|
||||
const search = useCallback(async (searchQuery: string) => {
|
||||
if (searchQuery.length < minQuery) {
|
||||
setResults([]);
|
||||
return;
|
||||
@@ -162,7 +172,7 @@ export function useSearch(options: UseSearchOptions = {}) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [types, limit, minQuery]);
|
||||
|
||||
// Effect for debounced search
|
||||
useEffect(() => {
|
||||
@@ -171,7 +181,7 @@ export function useSearch(options: UseSearchOptions = {}) {
|
||||
} else {
|
||||
setResults([]);
|
||||
}
|
||||
}, [debouncedQuery]);
|
||||
}, [debouncedQuery, search]);
|
||||
|
||||
// Save search to recent searches
|
||||
const saveSearch = (searchQuery: string) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { usernameSchema } from '@/lib/validation';
|
||||
import { useDebounce } from './useDebounce';
|
||||
@@ -20,6 +20,33 @@ export function useUsernameValidation(username: string, currentUsername?: string
|
||||
|
||||
const debouncedUsername = useDebounce(username, 500);
|
||||
|
||||
const checkUsernameAvailability = useCallback(async (normalizedUsername: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('username')
|
||||
.eq('username', normalizedUsername)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const isAvailable = !data;
|
||||
setState({
|
||||
isValid: isAvailable,
|
||||
isAvailable,
|
||||
isChecking: false,
|
||||
error: isAvailable ? null : 'Username is already taken',
|
||||
});
|
||||
} catch (error) {
|
||||
setState({
|
||||
isValid: false,
|
||||
isAvailable: null,
|
||||
isChecking: false,
|
||||
error: 'Error checking username availability',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedUsername || debouncedUsername === currentUsername) {
|
||||
setState({
|
||||
@@ -47,34 +74,7 @@ export function useUsernameValidation(username: string, currentUsername?: string
|
||||
setState(prev => ({ ...prev, isChecking: true, error: null }));
|
||||
|
||||
checkUsernameAvailability(validation.data);
|
||||
}, [debouncedUsername, currentUsername]);
|
||||
|
||||
const checkUsernameAvailability = async (normalizedUsername: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('username')
|
||||
.eq('username', normalizedUsername)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const isAvailable = !data;
|
||||
setState({
|
||||
isValid: isAvailable,
|
||||
isAvailable,
|
||||
isChecking: false,
|
||||
error: isAvailable ? null : 'Username is already taken',
|
||||
});
|
||||
} catch (error) {
|
||||
setState({
|
||||
isValid: false,
|
||||
isAvailable: null,
|
||||
isChecking: false,
|
||||
error: 'Error checking username availability',
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [debouncedUsername, currentUsername, checkUsernameAvailability]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user