Merge pull request #203 from R0m1k3/antigravity

feat: implement product search page with site selection, real-time re…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-01 01:02:14 +01:00
commit 8b6a3e9229
1 file changed
+213 -201
+213 -201
View File
@@ -51,224 +51,236 @@ export default function Search() {
// Charger les sites au démarrage
useEffect(() => {
} catch (error) {
console.error('Error loading sites:', error);
toast.error('Erreur lors du chargement des sites');
}
};
loadSites();
}, []);
const handleSearch = async (e) => {
e.preventDefault();
if (!query.trim() || !selectedSiteId) {
toast.error('Veuillez entrer une recherche et sélectionner un site');
return;
}
// Clear previous storage when starting new search
sessionStorage.removeItem(STORAGE_KEY);
// Fermer l'EventSource précédent si existant
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
setIsSearching(true);
setResults([]);
setProgress({ status: 'searching', total: 1, completed: 0 });
// Construire l'URL avec les paramètres
const params = new URLSearchParams({
q: query.trim(),
sites: selectedSiteId,
max_results: 20,
});
// Créer l'EventSource pour SSE
const eventSource = new EventSource(`${API_URL}/search?${params}`);
eventSourceRef.current = eventSource;
eventSource.addEventListener('progress', (event) => {
const loadSites = async () => {
try {
const data = JSON.parse(event.data);
setProgress(data);
setResults(data.results || []);
if (data.status === 'completed' || data.status === 'error') {
setIsSearching(false);
eventSource.close();
if (data.status === 'completed') {
toast.success(`${data.results?.length || 0} produits trouvés`);
} else if (data.message) {
toast.error(data.message);
}
const response = await axios.get(`${API_URL}/search-sites`);
const activeSites = response.data.filter(site => site.is_active);
setSites(activeSites);
// Sélectionner le premier site actif par défaut si aucun site n'est sélectionné
if (activeSites.length > 0 && !selectedSiteId) {
setSelectedSiteId(activeSites[0].id.toString());
}
} catch (error) {
console.error('Error parsing SSE data:', error);
console.error('Error loading sites:', error);
toast.error('Erreur lors du chargement des sites');
}
});
eventSource.onerror = () => {
setIsSearching(false);
eventSource.close();
toast.error('Erreur de connexion au serveur');
};
};
const handleAddToMonitoring = async (result) => {
try {
await axios.post(`${API_URL}/items`, {
url: result.url,
name: result.title,
target_price: null,
check_interval_minutes: 60,
const handleSearch = async (e) => {
e.preventDefault();
if (!query.trim() || !selectedSiteId) {
toast.error('Veuillez entrer une recherche et sélectionner un site');
return;
}
// Clear previous storage when starting new search
sessionStorage.removeItem(STORAGE_KEY);
// Fermer l'EventSource précédent si existant
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
setIsSearching(true);
setResults([]);
setProgress({ status: 'searching', total: 1, completed: 0 });
// Construire l'URL avec les paramètres
const params = new URLSearchParams({
q: query.trim(),
sites: selectedSiteId,
max_results: 20,
});
toast.success(`"${result.title}" ajouté au monitoring`);
} catch (error) {
console.error('Error adding item:', error);
toast.error("Erreur lors de l'ajout au monitoring");
}
};
const progressPercent = progress
? Math.round((progress.completed / Math.max(progress.total, 1)) * 100)
: 0;
// Créer l'EventSource pour SSE
const eventSource = new EventSource(`${API_URL}/search?${params}`);
eventSourceRef.current = eventSource;
const selectedSite = sites.find(s => s.id.toString() === selectedSiteId);
eventSource.addEventListener('progress', (event) => {
try {
const data = JSON.parse(event.data);
setProgress(data);
setResults(data.results || []);
return (
<div className="space-y-6 animate-in fade-in duration-500">
{/* Header */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">Recherche de produits</h2>
<p className="text-muted-foreground">
Recherchez des produits sur un site et ajoutez-les au monitoring
</p>
if (data.status === 'completed' || data.status === 'error') {
setIsSearching(false);
eventSource.close();
if (data.status === 'completed') {
toast.success(`${data.results?.length || 0} produits trouvés`);
} else if (data.message) {
toast.error(data.message);
}
}
} catch (error) {
console.error('Error parsing SSE data:', error);
}
});
eventSource.onerror = () => {
setIsSearching(false);
eventSource.close();
toast.error('Erreur de connexion au serveur');
};
};
const handleAddToMonitoring = async (result) => {
try {
await axios.post(`${API_URL}/items`, {
url: result.url,
name: result.title,
target_price: null,
check_interval_minutes: 60,
});
toast.success(`"${result.title}" ajouté au monitoring`);
} catch (error) {
console.error('Error adding item:', error);
toast.error("Erreur lors de l'ajout au monitoring");
}
};
const progressPercent = progress
? Math.round((progress.completed / Math.max(progress.total, 1)) * 100)
: 0;
const selectedSite = sites.find(s => s.id.toString() === selectedSiteId);
return (
<div className="space-y-6 animate-in fade-in duration-500">
{/* Header */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">Recherche de produits</h2>
<p className="text-muted-foreground">
Recherchez des produits sur un site et ajoutez-les au monitoring
</p>
</div>
</div>
</div>
{/* Search Form */}
<Card>
<CardContent className="pt-6">
<form onSubmit={handleSearch} className="space-y-4">
{/* Site Selection */}
<div className="space-y-2">
<p className="text-sm font-medium">Site de recherche :</p>
{sites.length === 0 ? (
<p className="text-sm text-muted-foreground">
Aucun site configuré. Contactez l'administrateur pour activer des sites.
</p>
) : (
<Select
value={selectedSiteId || ''}
onValueChange={setSelectedSiteId}
disabled={isSearching}
>
<SelectTrigger className="w-full md:w-[300px]">
<SelectValue placeholder="Sélectionner un site" />
</SelectTrigger>
<SelectContent>
{sites.map(site => (
<SelectItem key={site.id} value={site.id.toString()}>
<div className="flex items-center gap-2">
<span>{site.name}</span>
{site.category && (
<span className="text-xs text-muted-foreground">({site.category})</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{/* Search Input */}
<div className="flex gap-2">
<div className="relative flex-1">
<SearchIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Rechercher un produit..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9"
disabled={isSearching}
/>
</div>
<Button type="submit" disabled={isSearching || !query.trim() || !selectedSiteId}>
{isSearching ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Recherche...
</>
) : (
<>
<SearchIcon className="h-4 w-4 mr-2" />
Rechercher
</>
)}
</Button>
</div>
{/* Selected site info */}
{selectedSite && (
<p className="text-sm text-muted-foreground">
Recherche sur <span className="font-medium">{selectedSite.name}</span> ({selectedSite.domain})
</p>
)}
</form>
</CardContent>
</Card>
{/* Progress Bar */}
{progress && isSearching && (
{/* Search Form */}
<Card>
<CardContent className="pt-6">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{progress.message || 'Recherche en cours...'}</span>
<span>{progressPercent}%</span>
<form onSubmit={handleSearch} className="space-y-4">
{/* Site Selection */}
<div className="space-y-2">
<p className="text-sm font-medium">Site de recherche :</p>
{sites.length === 0 ? (
<p className="text-sm text-muted-foreground">
Aucun site configuré. Contactez l'administrateur pour activer des sites.
</p>
) : (
<Select
value={selectedSiteId || ''}
onValueChange={setSelectedSiteId}
disabled={isSearching}
>
<SelectTrigger className="w-full md:w-[300px]">
<SelectValue placeholder="Sélectionner un site" />
</SelectTrigger>
<SelectContent>
{sites.map(site => (
<SelectItem key={site.id} value={site.id.toString()}>
<div className="flex items-center gap-2">
<span>{site.name}</span>
{site.category && (
<span className="text-xs text-muted-foreground">({site.category})</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<Progress value={progressPercent} />
{/* Search Input */}
<div className="flex gap-2">
<div className="relative flex-1">
<SearchIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Rechercher un produit..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9"
disabled={isSearching}
/>
</div>
<Button type="submit" disabled={isSearching || !query.trim() || !selectedSiteId}>
{isSearching ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Recherche...
</>
) : (
<>
<SearchIcon className="h-4 w-4 mr-2" />
Rechercher
</>
)}
</Button>
</div>
{/* Selected site info */}
{selectedSite && (
<p className="text-sm text-muted-foreground">
Recherche sur <span className="font-medium">{selectedSite.name}</span> ({selectedSite.domain})
</p>
)}
</form>
</CardContent>
</Card>
{/* Progress Bar */}
{progress && isSearching && (
<Card>
<CardContent className="pt-6">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{progress.message || 'Recherche en cours...'}</span>
<span>{progressPercent}%</span>
</div>
<Progress value={progressPercent} />
</div>
</CardContent>
</Card>
)}
{/* Results */}
{results.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
{results.length} produit{results.length > 1 ? 's' : ''} trouvé{results.length > 1 ? 's' : ''}
</h3>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{results.map((result, index) => (
<SearchResultCard
key={`${result.url}-${index}`}
result={result}
onAddToMonitoring={() => handleAddToMonitoring(result)}
/>
))}
</div>
</CardContent>
</Card>
)}
{/* Results */}
{results.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
{results.length} produit{results.length > 1 ? 's' : ''} trouvé{results.length > 1 ? 's' : ''}
</h3>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{results.map((result, index) => (
<SearchResultCard
key={`${result.url}-${index}`}
result={result}
onAddToMonitoring={() => handleAddToMonitoring(result)}
/>
))}
</div>
</div>
)}
)}
{/* Empty State */}
{!isSearching && results.length === 0 && progress?.status === 'completed' && (
<Card>
<CardContent className="py-12 text-center">
<SearchIcon className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">Aucun produit trouvé</h3>
<p className="text-muted-foreground">
Essayez avec d'autres termes de recherche ou sélectionnez un autre site
</p>
</CardContent>
</Card>
)}
</div>
);
{/* Empty State */}
{!isSearching && results.length === 0 && progress?.status === 'completed' && (
<Card>
<CardContent className="py-12 text-center">
<SearchIcon className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">Aucun produit trouvé</h3>
<p className="text-muted-foreground">
Essayez avec d'autres termes de recherche ou sélectionnez un autre site
</p>
</CardContent>
</Card>
)}
</div>
);
}