fix: Corriger fonctionnalité de recherche globale

- Supprimer appel automatique de onSearch à chaque changement de query
- onSearch est maintenant appelé seulement lors de la soumission (Enter ou clic)
- Gérer feature flag PLAYLIST_SEARCH désactivé pour éviter erreurs
- Améliorer gestion des erreurs dans fetchSuggestions
- Corriger style du composant Search dans le Header
- Supprimer icône Search dupliquée dans Header
This commit is contained in:
senke 2026-01-18 12:36:44 +01:00
parent 0adcd2cefb
commit 106e80ec6d
3 changed files with 35 additions and 24 deletions

View file

@ -118,10 +118,8 @@ export function Header(_props: HeaderProps) {
{/* Global Search integrated */}
<div className="flex-1 max-w-lg mx-8 relative hidden md:block">
<GlobalSearchBar className="w-full bg-transparent border-none" />
<div className="absolute left-3 top-1/2 -translate-y-1/2 flex items-center gap-2 pointer-events-none opacity-50 group-hover:opacity-100 transition-opacity">
<Search className="w-4 h-4 text-kodo-steel" />
</div>
{/* FIX: L'icône Search est déjà dans le composant Search, pas besoin de la dupliquer */}
<GlobalSearchBar className="w-full" />
</div>
<div className="flex items-center gap-4">

View file

@ -6,6 +6,7 @@ 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
@ -38,31 +39,45 @@ export function GlobalSearchBar({
try {
// Fetch suggestions from all sources in parallel
const [tracksData, playlistsData, usersData] = await Promise.allSettled(
[
searchTracks(query, { pagination: { page: 1, limit: 3 } }),
// FIX: Gérer les feature flags (PLAYLIST_SEARCH peut être désactivé)
const searchPromises: 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')) {
searchPromises.push(
playlistsApi.search({ q: query, page: 1, limit: 3 }),
searchUsers({ query, page: 1, limit: 3 }),
],
);
} else {
// 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 results: SearchResult[] = [];
// Add track suggestions
// FIX: searchTracks retourne PaginatedResponse<Track> avec propriété 'data'
if (tracksData.status === 'fulfilled' && tracksData.value?.data) {
tracksData.value.data.forEach((track: any) => {
results.push({
id: track.id,
type: 'track',
title: track.title,
subtitle: track.artist ? `by ${track.artist}` : undefined,
image: track.cover_url,
title: track.title || track.name || 'Untitled',
subtitle: track.artist || track.artist_name || undefined,
image: track.cover_url || track.cover || track.thumbnail_url,
});
});
}
// Add playlist suggestions
// Add playlist suggestions (seulement si la feature est activée)
if (
isFeatureEnabled('PLAYLIST_SEARCH') &&
playlistsData.status === 'fulfilled' &&
playlistsData.value?.playlists
) {
@ -70,9 +85,9 @@ export function GlobalSearchBar({
results.push({
id: playlist.id,
type: 'playlist',
title: playlist.title,
title: playlist.title || 'Untitled Playlist',
subtitle: playlist.is_public ? 'Public' : 'Private',
image: playlist.cover_url,
image: playlist.cover_url || playlist.thumbnail_url,
});
});
}
@ -102,7 +117,7 @@ export function GlobalSearchBar({
[],
);
// Handle search action
// Handle search action - navigation vers la page de recherche
const handleSearch = useCallback(
(query: string) => {
if (query.trim()) {

View file

@ -95,12 +95,10 @@ export function Search({
loadSuggestions();
}, [debouncedQuery, fetchSuggestions, showSuggestions]);
// Appeler onSearch quand la query débounced change
useEffect(() => {
if (debouncedQuery.trim()) {
onSearch(debouncedQuery);
}
}, [debouncedQuery, onSearch]);
// FIX: Ne pas appeler onSearch automatiquement à chaque changement de query
// onSearch sera appelé seulement quand l'utilisateur soumet la recherche (Enter ou clic)
// Cela évite de naviguer vers /search à chaque frappe
// L'effet précédent qui appelait onSearch à chaque debouncedQuery a été supprimé
// Gérer le clic en dehors pour fermer le dropdown
useEffect(() => {
@ -273,7 +271,7 @@ export function Search({
return (
<div ref={searchRef} className={cn('relative w-full', className)}>
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground pointer-events-none z-10" />
<input
ref={inputRef}
type="text"
@ -283,7 +281,7 @@ export function Search({
onKeyDown={handleInputKeyDown}
placeholder={placeholder}
className={cn(
'w-full rounded-md border border-input bg-background px-8 py-2 text-sm',
'w-full rounded-md border border-input bg-background pl-10 pr-10 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',