diff --git a/app/routes/forms.tsx b/app/routes/forms.tsx index d5aef18..1790883 100644 --- a/app/routes/forms.tsx +++ b/app/routes/forms.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react" +import { useState, useEffect, useRef, useCallback } from "react" import { useNavigate, Link } from "react-router" -import { Plus, Search } from "lucide-react" +import { Plus, Search, Filter, ArrowUpDown, Loader2 } from "lucide-react" import { Navbar } from "@/components/shared/navbar" import { Footer } from "@/components/shared/footer" import { FormInput } from "@/components/shared/form-input" @@ -10,13 +10,25 @@ import { useAuth } from "@/app/context/auth-context" import { getForms } from "@/lib/api" import type { FormSummary } from "@/lib/types" +type StatusFilter = "all" | "has_responses" | "no_responses" +type SortBy = "created_at" | "updated_at" +type SortDir = "newest" | "oldest" + +const DEBOUNCE_MS = 400 + export default function FormsPage() { const { user, loading } = useAuth() const navigate = useNavigate() const [search, setSearch] = useState("") + const [statusFilter, setStatusFilter] = useState("all") + const [sortBy, setSortBy] = useState("created_at") + const [sortDir, setSortDir] = useState("newest") const [forms, setForms] = useState([]) + const [totalForms, setTotalForms] = useState(0) const [loadingForms, setLoadingForms] = useState(true) const [error, setError] = useState(null) + const debounceRef = useRef | null>(null) + const isInitialLoad = useRef(true) useEffect(() => { if (!loading && !user) { @@ -24,21 +36,62 @@ export default function FormsPage() { } }, [user, loading, navigate]) + const fetchForms = useCallback(async (params: { + search: string + statusFilter: StatusFilter + sortBy: SortBy + sortDir: SortDir + }) => { + setError(null) + setLoadingForms(true) + try { + const data = await getForms({ + search: params.search || undefined, + status: params.statusFilter === "all" ? undefined : params.statusFilter, + sort_by: params.sortBy, + sort_dir: params.sortDir, + }) + setForms(data) + if (isInitialLoad.current) { + setTotalForms(data.length) + isInitialLoad.current = false + } + } catch { + setError("Failed to load forms. Please try again.") + } finally { + setLoadingForms(false) + } + }, []) + + // Initial fetch useEffect(() => { if (!user) return + fetchForms({ search, statusFilter, sortBy, sortDir }) + }, [user]) // eslint-disable-line react-hooks/exhaustive-deps - async function fetchForms() { - try { - const data = await getForms() - setForms(data) - } catch { - setError("Failed to load forms. Please try again.") - } finally { - setLoadingForms(false) - } + // Debounced fetch when filters change (skip initial) + useEffect(() => { + if (!user || isInitialLoad.current) return + + if (debounceRef.current) clearTimeout(debounceRef.current) + + debounceRef.current = setTimeout(() => { + fetchForms({ search, statusFilter, sortBy, sortDir }) + }, DEBOUNCE_MS) + + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current) } - fetchForms() - }, [user]) + }, [search, statusFilter, sortBy, sortDir, user, fetchForms]) + + const hasActiveFilters = search !== "" || statusFilter !== "all" + + function clearAllFilters() { + setSearch("") + setStatusFilter("all") + setSortBy("created_at") + setSortDir("newest") + } if (loading || loadingForms) { return ( @@ -50,13 +103,6 @@ export default function FormsPage() { if (!user) return null - const filtered = forms.filter((form) => { - return ( - form.title.toLowerCase().includes(search.toLowerCase()) || - form.description.toLowerCase().includes(search.toLowerCase()) - ) - }) - return (
@@ -78,21 +124,76 @@ export default function FormsPage() {
-
-
+
+
setSearch(e.target.value)} className="pl-9" />
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
-

- Showing {filtered.length} of {forms.length} forms -

+
+

+ {loadingForms ? ( + + + Searching... + + ) : ( + <>Showing {forms.length}{totalForms > 0 ? ` of ${totalForms}` : ""} forms + )} +

+ {hasActiveFilters && ( + + )} +
{error ? (
@@ -106,9 +207,9 @@ export default function FormsPage() { Retry
- ) : filtered.length > 0 ? ( + ) : forms.length > 0 ? (
- {filtered.map((form) => ( + {forms.map((form) => ( ))}
@@ -121,7 +222,7 @@ export default function FormsPage() { variant="ghost" size="sm" className="mt-3" - onClick={() => setSearch("")} + onClick={clearAllFilters} > Clear filters diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 398e47c..700d14a 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,13 +1,5 @@ -import type { Route } from "./+types/home"; -import { Welcome } from "../welcome/welcome"; +import { redirect } from "react-router"; -export function meta({}: Route.MetaArgs) { - return [ - { title: "New React Router App" }, - { name: "description", content: "Welcome to React Router!" }, - ]; -} - -export default function Home() { - return ; +export function loader() { + throw redirect("/forms"); } diff --git a/lib/api.ts b/lib/api.ts index e9e3ae9..c7915f9 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -120,8 +120,6 @@ export async function register(email: string, password: string) { const data = await res.json().catch(() => null) throw new Error(data?.message ?? "Registration failed") } - - return res.json() } export async function logout() { @@ -145,8 +143,22 @@ export async function logout() { clearTokens() } -export async function getForms(): Promise { - const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/forms`) +export interface GetFormsParams { + search?: string + status?: "has_responses" | "no_responses" + sort_by?: "created_at" | "updated_at" + sort_dir?: "newest" | "oldest" +} + +export async function getForms(params: GetFormsParams = {}): Promise { + const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/api/forms`) + + if (params.search) url.searchParams.set("search", params.search) + if (params.status) url.searchParams.set("status", params.status) + if (params.sort_by) url.searchParams.set("sort_by", params.sort_by) + if (params.sort_dir) url.searchParams.set("sort_dir", params.sort_dir) + + const res = await fetchWithAuth(url.toString()) if (!res.ok) { throw new Error("Failed to fetch forms")