feat(search): add search history in localStorage (G5)

This commit is contained in:
senke 2026-02-20 16:56:30 +01:00
parent 72bc1a811d
commit 26397bbceb
4 changed files with 91 additions and 3 deletions

View file

@ -41,7 +41,7 @@ export function SearchPage() {
{isLoading ? (
<SearchPageSkeleton />
) : !query ? (
<SearchPageDiscovery />
<SearchPageDiscovery onQuerySelect={setQuery} />
) : !hasResults ? (
<SearchPageEmpty />
) : results ? (

View file

@ -1,8 +1,53 @@
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Music, User, Sparkles } from 'lucide-react';
import { Music, User, Sparkles, History } from 'lucide-react';
import { useSearchHistory } from '../../hooks/useSearchHistory';
interface SearchPageDiscoveryProps {
onQuerySelect?: (query: string) => void;
}
export function SearchPageDiscovery({ onQuerySelect }: SearchPageDiscoveryProps) {
const { getHistory, clearHistory } = useSearchHistory();
const [refreshKey, setRefreshKey] = useState(0);
const history = getHistory();
const handleClear = () => {
clearHistory();
setRefreshKey((k) => k + 1);
};
export function SearchPageDiscovery() {
return (
<div className="space-y-8">
{history.length > 0 && (
<section>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold flex items-center gap-2">
<History className="w-5 h-5 text-muted-foreground" />
Recherches récentes
</h3>
<button
type="button"
onClick={handleClear}
className="text-sm text-muted-foreground hover:text-foreground"
>
Effacer
</button>
</div>
<div className="flex flex-wrap gap-2">
{history.map((q) => (
<button
key={q}
type="button"
onClick={() => onQuerySelect?.(q)}
className="px-4 py-2 rounded-full bg-muted/50 hover:bg-muted text-sm"
>
{q}
</button>
))}
</div>
</section>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 opacity-80">
<Card
variant="glass"
@ -35,5 +80,6 @@ export function SearchPageDiscovery() {
<p className="text-xs text-muted-foreground">Trending creators this week</p>
</Card>
</div>
</div>
);
}

View file

@ -3,6 +3,7 @@ import { useSearchParams } from 'react-router-dom';
import { searchApi } from '@/services/api/search';
import { SearchResults } from '@/types/search';
import { useDebounce } from '@/hooks/useDebounce';
import { useSearchHistory } from '../../hooks/useSearchHistory';
export type SearchActiveTab = 'all' | 'track' | 'user' | 'playlist';
@ -12,6 +13,7 @@ function tabToTypes(tab: SearchActiveTab): string[] {
}
export function useSearchPage() {
const { addToHistory } = useSearchHistory();
const [searchParams, setSearchParams] = useSearchParams();
const queryParam = searchParams.get('q') ?? '';
const typeParam = searchParams.get('type') ?? 'all';
@ -48,6 +50,7 @@ export function useSearchPage() {
const types = tabToTypes(activeTab);
const data = await searchApi.search(debouncedQuery, types);
setResults(data);
addToHistory(debouncedQuery);
} catch (err) {
setError(err instanceof Error ? err : new Error('Search signal interrupted.'));
} finally {

View file

@ -0,0 +1,39 @@
const STORAGE_KEY = 'veza_search_history';
const MAX_ITEMS = 10;
export function useSearchHistory() {
const getHistory = (): string[] => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter((x): x is string => typeof x === 'string').slice(0, MAX_ITEMS);
} catch {
return [];
}
};
const addToHistory = (q: string) => {
const trimmed = q.trim();
if (!trimmed) return;
const current = getHistory();
const filtered = current.filter((x) => x.toLowerCase() !== trimmed.toLowerCase());
const updated = [trimmed, ...filtered].slice(0, MAX_ITEMS);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
} catch {
// ignore quota or other storage errors
}
};
const clearHistory = () => {
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
// ignore
}
};
return { getHistory, addToHistory, clearHistory };
}