diff --git a/.env b/.env index 42df6545f19bc489b5bfbd3f0849626e43259c74..47aa5c7364bd9a2b7bf9e1cdd1899023a2c49313 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ NEXT_PUBLIC_HOST_API=https://api-dev-minimal-v510.vercel.app -NEXT_PUBLIC_WEB_SOCKET_URL=https://stream.mydressin-server.com/ws +NEXT_PUBLIC_WEB_SOCKET_URL=https://stream-service.mydressin-server.com/ws # ASSETS NEXT_PUBLIC_ASSETS_API=https://api-dev-minimal-v510.vercel.app @@ -40,7 +40,7 @@ NEXT_PUBLIC_DEFAULT_IMAGE_URL=https://mydressin-rec.s3.eu-west-3.amazonaws.com/I #GATEWAY API URL -NEXT_PUBLIC_MYDRESSIN_GATEWAY_API_URL=https://api.mydressin-server.com +NEXT_PUBLIC_MYDRESSIN_GATEWAY_API_URL=https://api-gateway.mydressin-server.com NEXT_PUBLIC_MYDRESSIN_CLIENT_URL=https://app.mydressin-server.com diff --git a/src/shared/api/live.ts b/src/shared/api/live.ts index 4bf3b8bbd69d96d1e823874e3cb4e7c37d26c477..952ac6b74f8abebcb44056517097d983a967046d 100644 --- a/src/shared/api/live.ts +++ b/src/shared/api/live.ts @@ -525,7 +525,7 @@ export function useSearchProductsLive(page: number, size: number, keyword: strin return memoizedValue; } -export async function searchProduct(keyword: string) { +export async function searchProduct(keyword: string, signal?: AbortSignal): Promise<IProductListItem[]> { // Normalize the keyword (trim and lowercase) const normalizedKeyword = keyword.trim().toLowerCase(); if (!normalizedKeyword) { @@ -540,10 +540,15 @@ export async function searchProduct(keyword: string) { // Try with retry logic while (retries <= maxRetries) { try { + // Check if request was aborted before making a new attempt + if (signal?.aborted) { + throw new DOMException('Request aborted', 'AbortError'); + } + const URL = `${endpoints.Addons.PromoCode.getProductsPublishedWebsite(0, 10, normalizedKeyword)}`; - // Make the request without cache headers - const response = await axiosInstance.get(URL); + // Make the request with the abort signal + const response = await axiosInstance.get(URL, { signal }); // Validate response format before returning const data = response.data?.content; @@ -554,6 +559,12 @@ export async function searchProduct(keyword: string) { return data as IProductListItem[]; } catch (error: unknown) { + // Propagate abort errors immediately without retry + if (axios.isAxiosError(error) && error.name === 'CanceledError' || + error instanceof DOMException && error.name === 'AbortError') { + throw error; + } + lastError = error; // Only retry on network errors or 5xx server errors const isNetworkError = error instanceof Error && error.message?.includes('Network Error'); @@ -571,14 +582,17 @@ export async function searchProduct(keyword: string) { } } - // Log the error - console.error('Product search failed after retries:', lastError); - - // Throw the error to be handled by the caller - if (lastError instanceof Error) { - throw lastError; - } else { - throw new Error('Failed to search products'); + // Handle specific error cases better + if (axios.isAxiosError(lastError) && lastError.response) { + const status = lastError.response.status; + if (status === 404) { + return []; // Return empty results for not found + } else if (status === 401 || status === 403) { + throw new Error('Unauthorized access to product search'); + } } + + // Generic error for all other cases + throw lastError instanceof Error ? lastError : new Error('Failed to search products'); } diff --git a/src/shared/sections/lives/supervision/details-products-search.tsx b/src/shared/sections/lives/supervision/details-products-search.tsx index 116c77c2691ba8c53458274ce93154c5c1297c50..14439ec48abf67d0e3a7a197a3ee259a4551c0e9 100644 --- a/src/shared/sections/lives/supervision/details-products-search.tsx +++ b/src/shared/sections/lives/supervision/details-products-search.tsx @@ -35,7 +35,7 @@ type Props = { // Maximum number of search history items to store const MAX_SEARCH_HISTORY = 5; -const ProductsSearch: React.FC<Props> = ({ +const ProductsSearch = React.memo<Props>(({ query, results, onSearch, @@ -44,16 +44,17 @@ const ProductsSearch: React.FC<Props> = ({ minSearchChars = 3, }) => { const { addProductToLiveFunction, products } = useLiveProducts(); - const [searchHistory, setSearchHistory] = useState<string[]>([]); - - // Log when props change - useEffect(() => { - console.log('LiveSearch component received results:', results); - console.log('Current query:', query); - console.log('Loading state:', loading); - }, [results, query, loading]); + const [searchHistory, setSearchHistory] = useState<string[]>(() => { + // Initialize from localStorage if available + try { + const savedHistory = localStorage.getItem('productSearchHistory'); + return savedHistory ? JSON.parse(savedHistory) : []; + } catch { + return []; + } + }); - // Save search to history (in-memory only) + // Save search to history (now with localStorage persistence) const saveSearchToHistory = useCallback((searchTerm: string) => { if (!searchTerm || searchTerm.trim().length < minSearchChars) return; @@ -65,6 +66,13 @@ const ProductsSearch: React.FC<Props> = ({ ...prevHistory.filter(term => term !== normalizedTerm) ].slice(0, MAX_SEARCH_HISTORY); + // Save to localStorage + try { + localStorage.setItem('productSearchHistory', JSON.stringify(newHistory)); + } catch (error) { + // Ignore localStorage errors + } + return newHistory; }); }, [minSearchChars]); @@ -74,7 +82,7 @@ const ProductsSearch: React.FC<Props> = ({ if (results.length > 0 && query.trim().length >= minSearchChars) { saveSearchToHistory(query); } - }, [results, query, minSearchChars, saveSearchToHistory]); + }, [results.length, query, minSearchChars, saveSearchToHistory]); const isProductInList = useCallback( (product: IProductListItem) => products.some((p) => p.sku === product.ugs), @@ -105,11 +113,15 @@ const ProductsSearch: React.FC<Props> = ({ const handleClearHistory = useCallback(() => { setSearchHistory([]); + try { + localStorage.removeItem('productSearchHistory'); + } catch { + // Ignore localStorage errors + } }, []); const renderOption = useCallback( (props: React.HTMLAttributes<HTMLLIElement>, product: IProductListItem) => { - console.log('Rendering option for product:', product); const isAdded = isProductInList(product); return ( <Box @@ -153,6 +165,7 @@ const ProductsSearch: React.FC<Props> = ({ }, [isProductInList, handleAdd] ); + // Placeholder message based on character count const placeholderText = useMemo(() => { return `Rechercher produits par UGS (min. ${minSearchChars} caractères)...`; @@ -163,6 +176,11 @@ const ProductsSearch: React.FC<Props> = ({ [query] ); + // Memoize the input change handler + const handleInputChange = useCallback((_: React.SyntheticEvent, newValue: string) => { + onSearch(newValue); + }, [onSearch]); + return ( <Box sx={{ width: '100%', ...sx }}> <Autocomplete @@ -172,7 +190,7 @@ const ProductsSearch: React.FC<Props> = ({ popupIcon={null} options={results} getOptionKey={(option) => option.id} - onInputChange={(event, newValue) => onSearch(newValue)} + onInputChange={handleInputChange} getOptionLabel={(option) => option.name} noOptionsText={loading ? 'Chargement...' : noOptions} isOptionEqualToValue={(option, value) => option.id === value.id} @@ -246,6 +264,6 @@ const ProductsSearch: React.FC<Props> = ({ )} </Box> ); -}; +}); -export default React.memo(ProductsSearch); +export default ProductsSearch; diff --git a/src/shared/sections/lives/supervision/details-products-section.tsx b/src/shared/sections/lives/supervision/details-products-section.tsx index 6bea6f861a4828368bba1aae6db34d7de70b5d12..e65983ab21058d23a3c926ec928529ce002ca774 100644 --- a/src/shared/sections/lives/supervision/details-products-section.tsx +++ b/src/shared/sections/lives/supervision/details-products-section.tsx @@ -11,19 +11,16 @@ import useLiveProducts from "@/contexts/live-products/use-live-products"; import { SplashScreen } from "@/shared/components/loading-screen"; import { useLiveData } from "@/contexts/live-details"; -// Custom hook for debouncing +// Custom hook for debouncing with improved performance function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { - console.log('Debounce hook received new value, setting timeout'); const timer = setTimeout(() => { - console.log('Debounce timeout completed, updating value'); setDebouncedValue(value); }, delay); return () => { - console.log('Cleaning up previous debounce timeout'); clearTimeout(timer); }; }, [value, delay]); @@ -44,6 +41,9 @@ export default function ProductsSection() { const [searchResults, setSearchResults] = useState<IProductListItem[]>([]); const [searchError, setSearchError] = useState<string | null>(null); + // Reference to track current search request + const activeSearchRef = useRef<AbortController | null>(null); + // Minimum characters before search is triggered const MIN_SEARCH_CHARS = 3; @@ -59,51 +59,71 @@ export default function ProductsSection() { } function handleSearch(inputValue: string) { - console.log('Search input received:', inputValue); setQuery(inputValue); setSearchError(null); } - // Improved search function + // Improved search function with abort controller for cancelling previous requests const performSearch = useCallback((searchQuery: string) => { - console.log('Performing search with query:', searchQuery); - // Reset results if query is too short if (searchQuery.trim().length < MIN_SEARCH_CHARS) { - console.log('Query too short, minimum required:', MIN_SEARCH_CHARS); setSearchResults([]); return; } // Normalize the query const normalizedQuery = searchQuery.trim().toLowerCase(); - console.log('Normalized query:', normalizedQuery); + + // Cancel any in-flight request + if (activeSearchRef.current) { + activeSearchRef.current.abort(); + } + + // Create new abort controller for this request + const abortController = new AbortController(); + activeSearchRef.current = abortController; setLoading(true); setSearchError(null); - console.log('Calling search API with query:', normalizedQuery); - searchProduct(normalizedQuery) + // Use the abort signal for the search request + searchProduct(normalizedQuery, abortController.signal) .then((data) => { - console.log('Search API response:', data); - console.log('Number of results:', data.length); - setSearchResults(data); + // Only update if this is still the active request + if (activeSearchRef.current === abortController) { + setSearchResults(data); + } }) .catch((error) => { - console.error('Search API error:', error); - setSearchError('Erreur lors de la recherche de produits'); - setSearchResults([]); + // Only update error if this is still the active request + // and it's not an abort error + if (activeSearchRef.current === abortController && + error.name !== 'AbortError' && + error.name !== 'CanceledError') { + setSearchError('Erreur lors de la recherche de produits'); + setSearchResults([]); + } }) .finally(() => { - console.log('Search request completed'); - setLoading(false); + // Only update loading state if this is still the active request + if (activeSearchRef.current === abortController) { + setLoading(false); + activeSearchRef.current = null; + } }); }, []); // Effect to trigger search when debounced query changes useEffect(() => { - console.log('Debounced query changed:', debouncedQuery); performSearch(debouncedQuery); + + // Cleanup function to abort any in-flight request when component unmounts + return () => { + if (activeSearchRef.current) { + activeSearchRef.current.abort(); + activeSearchRef.current = null; + } + }; }, [debouncedQuery, performSearch]); // Get the appropriate message for search status