feat(frontend): add question search

This commit is contained in:
2025-05-30 19:27:31 +01:00
parent f7e491f1e8
commit 37f9102d80
2 changed files with 163 additions and 1 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState, useEffect, useRef } from "react";
import { Search } from "lucide-react";
import type { Category, Pattern } from "@/types";
interface QuestionFiltersProps {
categories: Category[];
patterns: Pattern[];
currentCategory?: string;
currentPattern?: string;
currentSearch?: string;
}
export function QuestionFilters({
categories,
patterns,
currentCategory,
currentPattern,
currentSearch,
}: QuestionFiltersProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [searchValue, setSearchValue] = useState(currentSearch || "");
const debounceRef = useRef<NodeJS.Timeout | null>(null);
// Update local state when URL changes
useEffect(() => {
setSearchValue(currentSearch || "");
}, [currentSearch]);
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchValue(value);
// Debounce the URL update
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (value.trim()) {
params.set("search", value.trim());
} else {
params.delete("search");
}
params.delete("page");
router.push(`/questions?${params.toString()}`);
}, 300);
},
[router, searchParams]
);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
const handleCategoryChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const params = new URLSearchParams(searchParams.toString());
if (e.target.value) {
params.set("category", e.target.value);
} else {
params.delete("category");
}
params.delete("page");
router.push(`/questions?${params.toString()}`);
},
[router, searchParams]
);
const handlePatternChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const params = new URLSearchParams(searchParams.toString());
if (e.target.value) {
params.set("pattern", e.target.value);
} else {
params.delete("pattern");
}
params.delete("page");
router.push(`/questions?${params.toString()}`);
},
[router, searchParams]
);
return (
<>
<div className="space-y-1">
<label
htmlFor="search-filter"
className="text-sm text-[var(--muted-foreground)]"
>
Search
</label>
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--muted-foreground)]" />
<input
id="search-filter"
type="text"
value={searchValue}
onChange={handleSearchChange}
placeholder="Search questions..."
className="pl-8 pr-3 py-1 rounded text-sm bg-[var(--card)] border border-[var(--border)] w-48"
/>
</div>
</div>
<div className="space-y-1">
<label
htmlFor="category-filter"
className="text-sm text-[var(--muted-foreground)]"
>
Category
</label>
<select
id="category-filter"
value={currentCategory || ""}
onChange={handleCategoryChange}
className="px-3 py-1 rounded text-sm bg-[var(--card)] border border-[var(--border)]"
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.slug}>
{cat.name} ({cat.question_count})
</option>
))}
</select>
</div>
<div className="space-y-1">
<label
htmlFor="pattern-filter"
className="text-sm text-[var(--muted-foreground)]"
>
Pattern
</label>
<select
id="pattern-filter"
value={currentPattern || ""}
onChange={handlePatternChange}
className="px-3 py-1 rounded text-sm bg-[var(--card)] border border-[var(--border)]"
>
<option value="">All Patterns</option>
{patterns.map((pat) => (
<option key={pat.id} value={pat.slug}>
{pat.name} ({pat.question_count})
</option>
))}
</select>
</div>
</>
);
}