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 @@
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
......
......@@ -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');
}
......@@ -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;
......@@ -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
......
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