- Logs détaillés dans GlobalSearchBar pour fetchSuggestions
- Logs pour chaque étape de la recherche (tracks, playlists, users)
- Logs de performance avec durée des requêtes
- Logs dans Search component pour interactions utilisateur
- Logs pour navigation et sélection de résultats
- Utilisation de console.log avec préfixes [🔍] pour faciliter le filtrage dans DevTools
253 lines
9.3 KiB
TypeScript
253 lines
9.3 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Search, type SearchResult } from './Search';
|
|
import { searchTracks } from '@/features/tracks/services/trackListService';
|
|
import { playlistsApi } from '@/services/api/playlists';
|
|
import { searchUsers } from '@/features/search/services/searchService';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { logger } from '@/utils/logger';
|
|
import { isFeatureEnabled } from '@/config/features';
|
|
|
|
/**
|
|
* FE-COMP-008: Global search bar component with autocomplete
|
|
* Can be used in the header or anywhere in the app
|
|
*/
|
|
|
|
export interface GlobalSearchBarProps {
|
|
className?: string;
|
|
placeholder?: string;
|
|
onSearch?: (query: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Global search bar with autocomplete suggestions for tracks, playlists, and users
|
|
*/
|
|
export function GlobalSearchBar({
|
|
className,
|
|
placeholder,
|
|
onSearch,
|
|
}: GlobalSearchBarProps) {
|
|
const navigate = useNavigate();
|
|
const { t } = useTranslation();
|
|
|
|
// Fetch autocomplete suggestions
|
|
const fetchSuggestions = useCallback(
|
|
async (query: string): Promise<SearchResult[]> => {
|
|
console.log('[🔍 GlobalSearchBar] fetchSuggestions called', { query });
|
|
logger.debug('Fetching search suggestions', { query, component: 'GlobalSearchBar' });
|
|
|
|
if (!query.trim()) {
|
|
console.log('[🔍 GlobalSearchBar] Empty query, returning empty results');
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const startTime = performance.now();
|
|
console.log('[🔍 GlobalSearchBar] Starting parallel search requests', {
|
|
query,
|
|
playlistSearchEnabled: isFeatureEnabled('PLAYLIST_SEARCH'),
|
|
});
|
|
|
|
// Fetch suggestions from all sources in parallel
|
|
// FIX: Gérer les feature flags (PLAYLIST_SEARCH peut être désactivé)
|
|
const searchPromises: Array<Promise<any>> = [
|
|
searchTracks(query, { pagination: { page: 1, limit: 3 } }),
|
|
searchUsers({ query, page: 1, limit: 3 }),
|
|
];
|
|
|
|
// Ajouter la recherche de playlists seulement si la feature est activée
|
|
if (isFeatureEnabled('PLAYLIST_SEARCH')) {
|
|
console.log('[🔍 GlobalSearchBar] Playlist search enabled, adding to promises');
|
|
searchPromises.push(
|
|
playlistsApi.search({ q: query, page: 1, limit: 3 }),
|
|
);
|
|
} else {
|
|
console.log('[🔍 GlobalSearchBar] Playlist search disabled, using empty result');
|
|
// Si désactivé, retourner une promesse résolue avec un résultat vide
|
|
searchPromises.push(Promise.resolve({ playlists: [], total: 0 }));
|
|
}
|
|
|
|
const [tracksData, usersData, playlistsData] = await Promise.allSettled(
|
|
searchPromises,
|
|
);
|
|
|
|
const duration = performance.now() - startTime;
|
|
console.log('[🔍 GlobalSearchBar] Search requests completed', {
|
|
duration: `${duration.toFixed(2)}ms`,
|
|
tracksStatus: tracksData.status,
|
|
usersStatus: usersData.status,
|
|
playlistsStatus: playlistsData.status,
|
|
});
|
|
|
|
const results: SearchResult[] = [];
|
|
|
|
// Add track suggestions
|
|
// FIX: searchTracks retourne PaginatedResponse<Track> avec propriété 'data'
|
|
if (tracksData.status === 'fulfilled' && tracksData.value?.data) {
|
|
const tracksCount = tracksData.value.data.length;
|
|
console.log('[🔍 GlobalSearchBar] Adding track suggestions', { count: tracksCount });
|
|
tracksData.value.data.forEach((track: any) => {
|
|
results.push({
|
|
id: track.id,
|
|
type: 'track',
|
|
title: track.title || track.name || 'Untitled',
|
|
subtitle: track.artist || track.artist_name || undefined,
|
|
image: track.cover_url || track.cover || track.thumbnail_url,
|
|
});
|
|
});
|
|
} else {
|
|
console.log('[🔍 GlobalSearchBar] No track suggestions', {
|
|
status: tracksData.status,
|
|
hasData: tracksData.status === 'fulfilled' && !!tracksData.value?.data,
|
|
error: tracksData.status === 'rejected' ? tracksData.reason : undefined,
|
|
});
|
|
}
|
|
|
|
// Add playlist suggestions (seulement si la feature est activée)
|
|
if (
|
|
isFeatureEnabled('PLAYLIST_SEARCH') &&
|
|
playlistsData.status === 'fulfilled' &&
|
|
playlistsData.value?.playlists
|
|
) {
|
|
const playlistsCount = playlistsData.value.playlists.length;
|
|
console.log('[🔍 GlobalSearchBar] Adding playlist suggestions', { count: playlistsCount });
|
|
playlistsData.value.playlists.forEach((playlist: any) => {
|
|
results.push({
|
|
id: playlist.id,
|
|
type: 'playlist',
|
|
title: playlist.title || 'Untitled Playlist',
|
|
subtitle: playlist.is_public ? 'Public' : 'Private',
|
|
image: playlist.cover_url || playlist.thumbnail_url,
|
|
});
|
|
});
|
|
} else {
|
|
console.log('[🔍 GlobalSearchBar] No playlist suggestions', {
|
|
featureEnabled: isFeatureEnabled('PLAYLIST_SEARCH'),
|
|
status: playlistsData.status,
|
|
hasData: playlistsData.status === 'fulfilled' && !!playlistsData.value?.playlists,
|
|
});
|
|
}
|
|
|
|
// Add user suggestions
|
|
if (usersData.status === 'fulfilled' && usersData.value?.users) {
|
|
const usersCount = usersData.value.users.length;
|
|
console.log('[🔍 GlobalSearchBar] Adding user suggestions', { count: usersCount });
|
|
usersData.value.users.forEach((user: any) => {
|
|
results.push({
|
|
id: user.id,
|
|
type: 'user',
|
|
title: user.username,
|
|
subtitle: user.email,
|
|
image: user.avatar_url,
|
|
});
|
|
});
|
|
} else {
|
|
console.log('[🔍 GlobalSearchBar] No user suggestions', {
|
|
status: usersData.status,
|
|
hasData: usersData.status === 'fulfilled' && !!usersData.value?.users,
|
|
error: usersData.status === 'rejected' ? usersData.reason : undefined,
|
|
});
|
|
}
|
|
|
|
const finalResults = results.slice(0, 8); // Limit to 8 total suggestions
|
|
console.log('[🔍 GlobalSearchBar] Final suggestions', {
|
|
total: results.length,
|
|
limited: finalResults.length,
|
|
breakdown: {
|
|
tracks: finalResults.filter((r) => r.type === 'track').length,
|
|
playlists: finalResults.filter((r) => r.type === 'playlist').length,
|
|
users: finalResults.filter((r) => r.type === 'user').length,
|
|
},
|
|
});
|
|
logger.debug('Search suggestions fetched', {
|
|
query,
|
|
totalResults: results.length,
|
|
limitedResults: finalResults.length,
|
|
component: 'GlobalSearchBar',
|
|
});
|
|
|
|
return finalResults;
|
|
} catch (error) {
|
|
console.error('[🔍 GlobalSearchBar] Error fetching suggestions', {
|
|
error,
|
|
query,
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
errorStack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
logger.error('Error fetching search suggestions', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
query,
|
|
component: 'GlobalSearchBar',
|
|
});
|
|
return [];
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
// Handle search action - navigation vers la page de recherche
|
|
const handleSearch = useCallback(
|
|
(query: string) => {
|
|
console.log('[🔍 GlobalSearchBar] handleSearch called', { query });
|
|
if (query.trim()) {
|
|
const searchUrl = `/search?q=${encodeURIComponent(query)}`;
|
|
console.log('[🔍 GlobalSearchBar] Navigating to search page', { searchUrl });
|
|
navigate(searchUrl);
|
|
onSearch?.(query);
|
|
logger.debug('Search submitted', { query, component: 'GlobalSearchBar' });
|
|
} else {
|
|
console.log('[🔍 GlobalSearchBar] Empty query, skipping navigation');
|
|
}
|
|
},
|
|
[navigate, onSearch],
|
|
);
|
|
|
|
// Handle result selection
|
|
const handleResultSelect = useCallback(
|
|
(result: SearchResult) => {
|
|
console.log('[🔍 GlobalSearchBar] handleResultSelect called', {
|
|
type: result.type,
|
|
id: result.id,
|
|
title: result.title,
|
|
});
|
|
logger.debug('Search result selected', {
|
|
type: result.type,
|
|
id: result.id,
|
|
component: 'GlobalSearchBar',
|
|
});
|
|
|
|
switch (result.type) {
|
|
case 'track':
|
|
navigate(`/tracks/${result.id}`);
|
|
break;
|
|
case 'playlist':
|
|
navigate(`/playlists/${result.id}`);
|
|
break;
|
|
case 'user':
|
|
navigate(`/users/${result.id}`);
|
|
break;
|
|
}
|
|
},
|
|
[navigate],
|
|
);
|
|
|
|
return (
|
|
<Search
|
|
onSearch={handleSearch}
|
|
onResultSelect={handleResultSelect}
|
|
fetchSuggestions={fetchSuggestions}
|
|
placeholder={
|
|
placeholder ||
|
|
t('common.search') ||
|
|
'Search tracks, playlists, users...'
|
|
}
|
|
showSuggestions={true}
|
|
showHistory={true}
|
|
className={className}
|
|
// CRITIQUE FIX #21: Augmenter le délai de debouncing à 500ms pour réduire les requêtes API
|
|
// Un délai de 500ms est optimal pour équilibrer réactivité et performance
|
|
debounceDelay={500}
|
|
/>
|
|
);
|
|
}
|