Skip to content
Extraits de code Groupes Projets
Valider 98959ab5 rédigé par oussama aftys's avatar oussama aftys
Parcourir les fichiers

fixed live search bug

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