From d71852ab48c66a6a144eaff07ce15060e894afaf Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Tue, 10 Nov 2026 14:45:22 -0800 Subject: [PATCH 1/2] add search context and action --- client/app/context/action.ts | 22 +++++++++- client/app/context/loginContext.tsx | 19 +++++---- client/app/context/searchContext.tsx | 63 ++++++++++++++++++++++++++++ client/app/layouts/private-route.tsx | 1 - client/app/root.tsx | 7 +++- client/app/routes/explore.tsx | 19 ++++----- client/app/routes/search.tsx | 17 ++++---- 7 files changed, 119 insertions(+), 29 deletions(-) create mode 100644 client/app/context/searchContext.tsx diff --git a/client/app/context/action.ts b/client/app/context/action.ts index 55b6b2d3..4ddadd3c 100644 --- a/client/app/context/action.ts +++ b/client/app/context/action.ts @@ -6,7 +6,7 @@ export type LoginState = { error?: string | null; }; -export const submitAction = async ( +export const submitLoginAction = async ( prevState: LoginState, formData: FormData ): Promise => { @@ -55,3 +55,23 @@ export const submitAction = async ( }; } }; + +export const submitSearchAction = async ({ categories, query }: { categories: string[], query: string }) => { + const params = new URLSearchParams({ query }); + categories.forEach((category) => params.append("category", category)); + + try { + const response = await tekdbApi.get("/api/search/", { + params, + }); + + if (response.status === 200) { + return response.data; + } + + throw new Error("Search failed"); + } catch (error) { + console.error("Search error", error); + throw error; + } +}; \ No newline at end of file diff --git a/client/app/context/loginContext.tsx b/client/app/context/loginContext.tsx index 3d1ebd13..23998dcd 100644 --- a/client/app/context/loginContext.tsx +++ b/client/app/context/loginContext.tsx @@ -1,10 +1,11 @@ import React, { useEffect, createContext, - useContext, + use, useCallback, } from "react"; -import { submitAction } from "./action"; +import type { ReactNode } from "react"; +import { submitLoginAction } from "./action"; import { getAuthSession } from "../api/tekdbApi"; type UseLoginFormReturn = { @@ -24,7 +25,11 @@ type useLogin = { const LoginContext = createContext(null); -export const LoginProvider: React.FC> = ({ +interface LoginProviderProps { + children: ReactNode; +} + +export const LoginProvider: React.FC = ({ children, }) => { const [showLoginModal, setShowLoginModal] = React.useState(false); @@ -66,7 +71,7 @@ export const LoginProvider: React.FC> = ({ const formAction = useCallback(async (formData: FormData) => { setIsPending(true); try { - const loginState = await submitAction( + const loginState = await submitLoginAction( { success: false, error: null, @@ -77,7 +82,7 @@ export const LoginProvider: React.FC> = ({ if (loginState.success) { setIsAuthenticated(true); setError(null); - setShowLoginModal(false); + setShowLoginModal(false); return; } @@ -104,12 +109,12 @@ export const LoginProvider: React.FC> = ({ }; return ( - {children} + {children} ); }; export const useLogin = () => { - const context = useContext(LoginContext); + const context = use(LoginContext); if (!context) { throw new Error("useLogin must be used within a LoginProvider"); } diff --git a/client/app/context/searchContext.tsx b/client/app/context/searchContext.tsx new file mode 100644 index 00000000..133455e3 --- /dev/null +++ b/client/app/context/searchContext.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, use } from "react" +import type { ReactNode } from "react" +import { submitSearchAction } from "./action" + +type useSearch = { + categories: string[], + results: any[], + query: string, + handleCategoryTypeChange: (type: string) => void, + handleSearch: (formData: FormData) => Promise + handleQueryChange: (query: string) => void +} + +const SearchContext = React.createContext(null); + +interface SearchProviderProps { + children: ReactNode; +} + +export const SearchProvider: React.FC = ({ children }) => { + const [categories, setCategories] = React.useState([]); + const [results, setResults] = React.useState([]); + const [query, setQuery] = React.useState(""); + + const handleCategoryTypeChange = useCallback((type: string) => { + if (categories.includes(type)) { + setCategories(categories.filter((category: string) => category !== type)); + } else { + setCategories([...categories, type]); + } + }, [categories]); + + const handleQueryChange = useCallback((query: string) => { + setQuery(query); + }, []); + + const handleSearch = useCallback(async () => { + + try { + const results = await submitSearchAction({ categories, query }); + setResults(results); + } catch (error) { + console.error("Search failed", error); + } + }, [categories, query]); + + const value = { categories, results, query, handleCategoryTypeChange, handleSearch, handleQueryChange } + console.log("Search context value:", value.categories); + console.log("Search context query:", value.query); + return ( + + {children} + + ); +}; + +export const useSearch = () => { + const context = use(SearchContext); + if (!context) { + throw new Error("useSearch must be used within a SearchProvider"); + } + return context; +}; \ No newline at end of file diff --git a/client/app/layouts/private-route.tsx b/client/app/layouts/private-route.tsx index a8ec258f..dd387228 100644 --- a/client/app/layouts/private-route.tsx +++ b/client/app/layouts/private-route.tsx @@ -5,7 +5,6 @@ import { useLogin } from "../context/loginContext"; /** Return a 404 if the user is not authenticated */ const PrivateRoute: React.FC = () => { const { loginForm } = useLogin(); - console.log("PrivateRoute", loginForm); if (loginForm.isCheckingAuth) { return <>; } diff --git a/client/app/root.tsx b/client/app/root.tsx index cd512e7a..ca40ed00 100644 --- a/client/app/root.tsx +++ b/client/app/root.tsx @@ -7,6 +7,7 @@ import { ScrollRestoration, useLoaderData, } from "react-router"; +import type { ReactNode } from "react"; import type { Route } from "./+types/root"; import "./App.css"; @@ -16,8 +17,10 @@ import { type PageContentAndSiteInfo, } from "./api/pageContent"; import { LoginProvider } from "./context/loginContext"; +import { SearchProvider } from "./context/searchContext"; -export function Layout({ children }: { children: React.ReactNode }) { + +export function Layout({ children }: { children: ReactNode }) { return ( @@ -77,9 +80,11 @@ export default function Root() { }} /> + + ); diff --git a/client/app/routes/explore.tsx b/client/app/routes/explore.tsx index 0af58c8b..9bbad6e1 100644 --- a/client/app/routes/explore.tsx +++ b/client/app/routes/explore.tsx @@ -1,17 +1,15 @@ -import React, { useState } from "react"; +import React from "react"; +import { useSearch } from "../context/searchContext"; import "./explore.css"; const ExploreRoute: React.FC = () => { - const [categories, setCategories] = useState([]); + const { handleCategoryTypeChange, handleSearch, handleQueryChange } = useSearch(); - const handleCategoryTypeChange = (type: string) => { - if (categories.includes(type)) { - setCategories(categories.filter((category) => category !== type)); - } else { - setCategories([...categories, type]); - } + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSearch(new FormData(e.currentTarget)); }; - console.log("Selected categories:", categories); + return (
@@ -19,7 +17,7 @@ const ExploreRoute: React.FC = () => {
-
+

Search & Browse

You can search by term alone and/or add category filters @@ -65,6 +63,7 @@ const ExploreRoute: React.FC = () => { name="query" placeholder="Search" aria-describedby="search-icon-label" + onChange={(e) => handleQueryChange(e.target.value)} />

diff --git a/client/app/routes/search.tsx b/client/app/routes/search.tsx index 2d1e023e..f41ee8f3 100644 --- a/client/app/routes/search.tsx +++ b/client/app/routes/search.tsx @@ -1,19 +1,18 @@ -import React, { useState } from "react"; +import React from "react"; +import { useSearch } from "../context/searchContext"; const SearchRoute: React.FC = () => { -const [categories, setCategories] = useState([]); + const { handleCategoryTypeChange, handleSearch } = useSearch(); - const handleCategoryTypeChange = (type: string) => { - if (categories.includes(type)) { - setCategories(categories.filter((category) => category !== type)); - } else { - setCategories([...categories, type]); - } + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSearch(new FormData(e.currentTarget)); }; + return (
- +
From a3d71f505564657a39ca611675aa6c5d4dd4a6aa Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Tue, 10 Nov 2026 16:27:58 -0800 Subject: [PATCH 2/2] route to search page when submitting form --- client/app/components/checkbox.tsx | 23 ++++ client/app/components/header.tsx | 4 +- client/app/context/searchContext.tsx | 109 ++++++++++--------- client/app/routes/explore.tsx | 59 ++++------ client/app/routes/search.tsx | 155 +++++++++++---------------- client/app/utils/constants.ts | 1 + 6 files changed, 169 insertions(+), 182 deletions(-) create mode 100644 client/app/components/checkbox.tsx create mode 100644 client/app/utils/constants.ts diff --git a/client/app/components/checkbox.tsx b/client/app/components/checkbox.tsx new file mode 100644 index 00000000..ca3873dc --- /dev/null +++ b/client/app/components/checkbox.tsx @@ -0,0 +1,23 @@ +const Checkbox = ({ + label, + onChange, + checked, +}: { + label: string; + onChange: () => void; + checked: boolean; +}) => { + return ( +
+ onChange()} + checked={checked} + id={`check-${label.toLowerCase()}`} + /> + {label} +
+ ); +}; + +export default Checkbox; diff --git a/client/app/components/header.tsx b/client/app/components/header.tsx index c5b90346..97109d4d 100644 --- a/client/app/components/header.tsx +++ b/client/app/components/header.tsx @@ -90,7 +90,9 @@ const Header: React.FC = ({ id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" - >{loginForm?.username} + > + {loginForm?.username} +