From 2f972591324b9ec47776f7a244717cc6a83dbb31 Mon Sep 17 00:00:00 2001 From: kishan1735 Date: Tue, 23 Jul 2024 21:49:42 +0530 Subject: [PATCH 1/6] feat: add search bar component --- .../controllers/timetable/searchTimetable.ts | 43 +++++++ backend/src/routers/timetableRouter.ts | 10 ++ frontend/src/components/SearchBar.tsx | 108 ++++++++++++++++++ frontend/src/components/announcements.tsx | 2 +- frontend/src/components/navbar.tsx | 37 +++++- 5 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/timetable/searchTimetable.ts create mode 100644 frontend/src/components/SearchBar.tsx diff --git a/backend/src/controllers/timetable/searchTimetable.ts b/backend/src/controllers/timetable/searchTimetable.ts new file mode 100644 index 00000000..7cb4c6b7 --- /dev/null +++ b/backend/src/controllers/timetable/searchTimetable.ts @@ -0,0 +1,43 @@ +import { Request, Response } from "express"; +import { z } from "zod"; +import { namedNonEmptyStringType } from "../../../../lib/src/zodFieldTypes.js"; +import { env } from "../../config/server.js"; +import { Timetable } from "../../entity/entities.js"; +import { validate } from "../../middleware/zodValidateRequest.js"; + +const searchTimetableSchema = z.object({ + query: z.object({ + query: namedNonEmptyStringType("query"), + }), +}); + +export const searchTimetableValidator = validate(searchTimetableSchema); + +export const searchTimetable = async (req: Request, res: Response) => { + try { + const { query } = req.query; + + const searchServiceURL = `${env.SEARCH_SERVICE_URL}/timetable/search?query=${query}`; + + const response = await fetch(searchServiceURL, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + let timetables = await response.json(); + + if (!response.ok) { + console.log("Error while searching timetable: ", timetables.error); + return res.status(500).json({ message: "Internal Server Error" }); + } + + timetables = timetables.map( + (el: { timetable: Timetable; score: string }) => el.timetable, + ); + + return res.json(timetables); + } catch (err) { + console.log(err); + return res.status(500).json({ message: "Internal Server Error" }); + } +}; diff --git a/backend/src/routers/timetableRouter.ts b/backend/src/routers/timetableRouter.ts index f1df5252..e3df6361 100644 --- a/backend/src/routers/timetableRouter.ts +++ b/backend/src/routers/timetableRouter.ts @@ -30,6 +30,10 @@ import { removeSection, removeSectionValidator, } from "../controllers/timetable/removeSection.js"; +import { + searchTimetable, + searchTimetableValidator, +} from "../controllers/timetable/searchTimetable.js"; import { authenticate } from "../middleware/auth.js"; const timetableRouter = express.Router(); @@ -41,6 +45,12 @@ timetableRouter.get( getPublicTimetablesValidator, getPublicTimetables, ); +timetableRouter.get( + "/search", + authenticate, + searchTimetableValidator, + searchTimetable, +); timetableRouter.get("/:id", getTimetableByIdValidator, getTimetableById); timetableRouter.post( "/:id/delete", diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx new file mode 100644 index 00000000..fe401126 --- /dev/null +++ b/frontend/src/components/SearchBar.tsx @@ -0,0 +1,108 @@ +import { useQueryClient } from "@tanstack/react-query"; +import axios, { AxiosError } from "axios"; +import { ListFilter, Search } from "lucide-react"; +import { useRef } from "react"; +import { z } from "zod"; +import { timetableWithSectionsType } from "../../../lib/src"; +import { Button } from "./ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { Input } from "./ui/input"; +import { ToastAction } from "./ui/toast"; +import { useToast } from "./ui/use-toast"; + +const fetchSearchDetails = async ( + query: string, +): Promise[]> => { + const response = await axios.get[]>( + `/api/timetable/search?query=${query}`, + { + headers: { + "Content-Type": "application/json ", + }, + }, + ); + return response.data; +}; + +const SearchBar = () => { + const { toast } = useToast(); + const searchRef = useRef(null); + const queryClient = useQueryClient(); + const handleSearch = async (query: string | undefined) => { + if (query === undefined || query.length < 2) { + toast({ + title: "Error", + description: "Search query has to be atleast 2 characters long", + }); + return; + } + try { + const timetables = await queryClient.fetchQuery({ + queryKey: ["search_timetable"], + queryFn: () => fetchSearchDetails(query), + }); + localStorage.setItem("timetable_search", JSON.stringify(timetables)); + } catch (error) { + if (error instanceof AxiosError && error.response) { + toast({ + title: "Server Error", + description: `${error.response.data.message}`, + variant: "destructive", + action: ( + + + Report + + + ), + }); + } + } + }; + return ( +
+
+ handleSearch(searchRef.current?.value)} + /> + + +
+ + + + + + Filter by + + Course + Name + Archived + + +
+ ); +}; + +export default SearchBar; diff --git a/frontend/src/components/announcements.tsx b/frontend/src/components/announcements.tsx index 86b4d0f5..a071d57c 100644 --- a/frontend/src/components/announcements.tsx +++ b/frontend/src/components/announcements.tsx @@ -66,7 +66,7 @@ function Announcements() { return ( - diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 0927b68f..159220fc 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -16,13 +16,15 @@ import { } from "@tanstack/react-query"; import { Link, useRouter } from "@tanstack/react-router"; import axios, { AxiosError } from "axios"; -import { BookUp, Info, LogOut, Pencil, Plus } from "lucide-react"; +import { BookUp, Info, LogOut, Pencil, Plus, Search } from "lucide-react"; import { useCookies } from "react-cookie"; import { z } from "zod"; import { userWithTimetablesType } from "../../../lib/src/index"; import { router } from "../main"; +import SearchBar from "./SearchBar"; import Announcements from "./announcements"; import { ModeToggle } from "./mode-toggle"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; const fetchUserDetails = async (): Promise< z.infer @@ -162,7 +164,7 @@ export function NavBar() { {!isEditPage && (