Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search bar component #208

Merged
merged 7 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions frontend/src/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { queryOptions, useQuery } from "@tanstack/react-query";
import { ErrorComponent, Route, notFound } from "@tanstack/react-router";
import axios, { AxiosError } from "axios";
import { z } from "zod";
import { timetableWithSectionsType } from "../../lib/src";
import authenticatedRoute from "./AuthenticatedRoute";
import { ToastAction } from "./components/ui/toast";
import { useToast } from "./components/ui/use-toast";
import { router } from "./main";

const fetchSearchDetails = async (
query: string,
): Promise<z.infer<typeof timetableWithSectionsType>[]> => {
const response = await axios.get<z.infer<typeof timetableWithSectionsType>[]>(
`/api/timetable/search?query=${query}`,
{
headers: {
"Content-Type": "application/json",
},
},
);
return response.data;
};

const searchQueryOptions = (query: string) =>
queryOptions({
queryKey: ["search_timetables", query],
queryFn: () => fetchSearchDetails(query),
});

const searchRoute = new Route({
getParentRoute: () => authenticatedRoute,
path: "search/$query",
component: SearchResults,
loader: ({ context: { queryClient }, params }) =>
queryClient
.ensureQueryData(searchQueryOptions(params.query))
.catch((error: Error) => {
if (
error instanceof AxiosError &&
error.response &&
error.response.status === 401
) {
router.navigate({
to: "/login",
});
}

throw error;
}),
errorComponent: ({ error }) => {
const { toast } = useToast();

if (error instanceof AxiosError) {
if (error.response) {
switch (error.response.status) {
case 404:
toast({
title: "Error",
description:
"message" in error.response.data
? error.response.data.message
: "API returned 404",
variant: "destructive",
action: (
<ToastAction altText="Report issue: https://github.com/crux-bphc/chronofactorem-rewrite/issues">
<a href="https://github.com/crux-bphc/chronofactorem-rewrite/issues">
Report
</a>
</ToastAction>
),
});
break;
case 500:
toast({
title: "Server Error",
description:
"message" in error.response.data
? error.response.data.message
: "API returned 500",
variant: "destructive",
action: (
<ToastAction altText="Report issue: https://github.com/crux-bphc/chronofactorem-rewrite/issues">
<a href="https://github.com/crux-bphc/chronofactorem-rewrite/issues">
Report
</a>
</ToastAction>
),
});
break;

default:
toast({
title: "Unknown Error",
description:
"message" in error.response.data
? error.response.data.message
: `API returned ${error.response.status}`,
variant: "destructive",
action: (
<ToastAction altText="Report issue: https://github.com/crux-bphc/chronofactorem-rewrite/issues">
<a href="https://github.com/crux-bphc/chronofactorem-rewrite/issues">
Report
</a>
</ToastAction>
),
});
}
} else {
return <ErrorComponent error={error} />;
}
}
},
});

function SearchResults() {
const { query } = searchRoute.useParams();
// @ts-ignore Suppress unused variable warning, needs to be removed when the page is finished
const searchQueryResult = useQuery(searchQueryOptions(query));
return (
<main className="text-foreground py-6 md:py-12 px-10 md:px-16">
<h1 className="text-xl font-bold text-center sm:text-left md:text-4xl">
Search Results
</h1>
</main>
);
}

export default searchRoute;
71 changes: 71 additions & 0 deletions frontend/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { router } from "@/main";
import { ListFilter, Search } from "lucide-react";
import { useRef } from "react";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Input } from "./ui/input";
import { useToast } from "./ui/use-toast";

const SearchBar = () => {
const { toast } = useToast();
const searchRef = useRef<HTMLInputElement>(null);
const handleSearch = async (query: string | undefined) => {
if (query === undefined || query.length < 2) {
toast({
title: "Error",
variant: "destructive",
description: "Search query has to be atleast 2 characters long",
});
return;
}
router.navigate({
to: "/search/$query",
params: { query },
});
};
return (
<div className="flex items-center gap-2 m-1">
<div className="relative ml-auto flex-1 md:grow-0">
<Input
type="search"
placeholder="Search Timetables..."
className="w-full rounded-lg bg-background pl-4 md:w-48 lg:w-80"
ref={searchRef}
/>
<Search
className="absolute right-4 top-2.5 h-4 w-4 text-muted-foreground cursor-pointer"
onClick={() => handleSearch(searchRef.current?.value)}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 gap-1 hidden">
<ListFilter className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
Filter
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border border-slate-900 p-1 bg-slate-950 mt-2 rounded-sm"
>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Course</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Name</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

export default SearchBar;
2 changes: 1 addition & 1 deletion frontend/src/components/announcements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function Announcements() {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="mr-9" variant="outline" size="icon">
<Button className="mr-1 md:mr-9" variant="outline" size="icon">
<Megaphone className="h-5 w-5" />
</Button>
</DialogTrigger>
Expand Down
28 changes: 23 additions & 5 deletions frontend/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof userWithTimetablesType>
Expand Down Expand Up @@ -162,7 +164,7 @@ export function NavBar() {
</Link>
{!isEditPage && (
<Button
className="text-green-200 w-fit text-xl p-4 ml-4 bg-green-900 hover:bg-green-800"
className="text-green-200 w-fit text-xl px-2 md:px-4 py-4 md:ml-4 bg-green-900 hover:bg-green-800"
onClick={
userQueryResultData ? () => createMutation.mutate() : undefined
}
Expand All @@ -175,7 +177,7 @@ export function NavBar() {
)}
<Link
to={userQueryResultData ? "/about" : undefined}
className="text-primary py-2 px-2 ml-2 text-lg rounded-full hover:bg-muted transition h-fit duration-200 ease-in-out"
className="text-primary py-2 px-2 md:ml-2 text-lg rounded-full hover:bg-muted transition h-fit duration-200 ease-in-out"
>
<div className="hidden md:flex">About</div>
<div className="flex md:hidden">
Expand All @@ -185,17 +187,33 @@ export function NavBar() {
{!isCMSPage && (
<Link
to={userQueryResultData ? "/CMSExport" : undefined}
className="text-primary py-2 px-2 ml-2 text-lg rounded-full hover:bg-muted transition h-fit whitespace-nowrap duration-200 ease-in-out"
className="text-primary py-2 px-2 md:ml-2 text-lg rounded-full hover:bg-muted transition h-fit whitespace-nowrap duration-200 ease-in-out"
>
<div className="hidden md:flex">CMS Auto-Enroll</div>
<div className="flex md:hidden">
<BookUp className="h-6 w-6" />
</div>
</Link>
)}
<div className="hidden md:flex md:ml-4">
<SearchBar />
</div>
<div className="text-primary py-2 px-2 md:ml-2 text-lg rounded-full hover:bg-muted transition h-fit duration-200 ease-in-out">
<div className="flex md:hidden">
<Popover>
<PopoverTrigger asChild className="cursor-pointer">
<Search />
</PopoverTrigger>
<PopoverContent className="w-80 mt-4 mr-8">
<SearchBar />
</PopoverContent>
</Popover>
</div>
</div>
</div>

<div className="flex flex-row">
<div className="pt-3">
<div className="flex pt-3">
<Announcements />
<ModeToggle />
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import getDegreesRoute from "./GetDegrees";
import homeRoute from "./Home";
import loginRoute from "./Login";
import RootComponent from "./RootComponent";
import searchRoute from "./SearchResults";
import viewTimetableRoute from "./ViewTimetable";
import NotFound from "./components/NotFound";
import "./index.css";
Expand Down Expand Up @@ -51,6 +52,7 @@ const routeTree = rootRoute.addChildren([
viewTimetableRoute,
CMSExportRoute,
CMSRoute,
searchRoute,
]),
]);

Expand Down
Loading