feat(search): add search history in localStorage (G5)
This commit is contained in:
parent
72bc1a811d
commit
26397bbceb
4 changed files with 91 additions and 3 deletions
|
|
@ -41,7 +41,7 @@ export function SearchPage() {
|
|||
{isLoading ? (
|
||||
<SearchPageSkeleton />
|
||||
) : !query ? (
|
||||
<SearchPageDiscovery />
|
||||
<SearchPageDiscovery onQuerySelect={setQuery} />
|
||||
) : !hasResults ? (
|
||||
<SearchPageEmpty />
|
||||
) : results ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
39
apps/web/src/features/search/hooks/useSearchHistory.ts
Normal file
39
apps/web/src/features/search/hooks/useSearchHistory.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue