feat(frontend): add question search
This commit is contained in:
@@ -94,6 +94,7 @@ export default async function QuestionsPage({
|
|||||||
patterns={patternsResponse.items}
|
patterns={patternsResponse.items}
|
||||||
currentCategory={params.category}
|
currentCategory={params.category}
|
||||||
currentPattern={params.pattern}
|
currentPattern={params.pattern}
|
||||||
|
currentSearch={params.search}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ export default async function QuestionsPage({
|
|||||||
params.difficulty ? `&difficulty=${params.difficulty}` : ""
|
params.difficulty ? `&difficulty=${params.difficulty}` : ""
|
||||||
}${params.category ? `&category=${params.category}` : ""}${
|
}${params.category ? `&category=${params.category}` : ""}${
|
||||||
params.pattern ? `&pattern=${params.pattern}` : ""
|
params.pattern ? `&pattern=${params.pattern}` : ""
|
||||||
}`}
|
}${params.search ? `&search=${encodeURIComponent(params.search)}` : ""}`}
|
||||||
className={`px-3 py-1 rounded ${
|
className={`px-3 py-1 rounded ${
|
||||||
page === questionsResponse.page
|
page === questionsResponse.page
|
||||||
? "bg-[var(--primary)] text-[var(--primary-foreground)]"
|
? "bg-[var(--primary)] text-[var(--primary-foreground)]"
|
||||||
|
|||||||
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