feat(frontend): add question search
This commit is contained in:
161
frontend/src/components/questions/question-filters.tsx
Normal file
161
frontend/src/components/questions/question-filters.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user