From 98959ab501ea4669dfa494a568cb50e77db36831 Mon Sep 17 00:00:00 2001
From: "oussama.aftys" <oussama.aftys@marketingconfort.com>
Date: Mon, 21 Apr 2025 11:09:57 +0100
Subject: [PATCH] fixed live search bug
---
.env | 4 +-
src/shared/api/live.ts | 36 +++++++----
.../supervision/details-products-search.tsx | 48 +++++++++-----
.../supervision/details-products-section.tsx | 62 ++++++++++++-------
4 files changed, 101 insertions(+), 49 deletions(-)
diff --git a/.env b/.env
index 42df6545..47aa5c73 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 4bf3b8bb..952ac6b7 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 116c77c2..14439ec4 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 6bea6f86..e65983ab 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
--
GitLab