From b81d21ccb5eac83cb2cf3e87047c0f31870d27f1 Mon Sep 17 00:00:00 2001 From: adhi0331 Date: Fri, 22 Nov 2024 18:18:32 -0800 Subject: [PATCH] completed UI --- backend/src/controllers/student.ts | 17 +++ backend/src/routes/student.ts | 1 + frontend/src/api/students.ts | 10 ++ frontend/src/components/Calendar/Datebox.tsx | 4 +- .../CalendarTable/CalendarTable.tsx | 11 +- .../src/components/CalendarTable/Filters.tsx | 127 +++++++++++++++-- .../src/components/CalendarTable/types.ts | 8 +- .../CalendarTable/useColumnSchema.tsx | 38 ++++- frontend/src/pages/calendar.tsx | 130 +++++++++++------- 9 files changed, 270 insertions(+), 76 deletions(-) diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index ef5e965e..90335d99 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -5,6 +5,7 @@ import { RequestHandler } from "express"; import { validationResult } from "express-validator"; +import createHttpError from "http-errors"; import mongoose, { HydratedDocument } from "mongoose"; import EnrollmentModel from "../models/enrollment"; @@ -89,3 +90,19 @@ export const getAllStudents: RequestHandler = async (_, res, next) => { next(error); } }; + +export const getStudent: RequestHandler = async (req, res, next) => { + try { + const id = req.params.id; + + const student = await StudentModel.findById(id); + + if (student === null) { + throw createHttpError(404, "Student not found"); + } + + res.status(200).json(student); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts index a534d31f..713b23a0 100644 --- a/backend/src/routes/student.ts +++ b/backend/src/routes/student.ts @@ -12,5 +12,6 @@ const router = express.Router(); router.post("/create", StudentValidator.createStudent, StudentController.createStudent); router.put("/edit/:id", StudentValidator.editStudent, StudentController.editStudent); router.get("/all", StudentController.getAllStudents); +router.get("/:id", StudentController.getStudent); export default router; diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index 8a1583cc..369b2621 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -40,3 +40,13 @@ export async function getAllStudents(): Promise> { return handleAPIError(error); } } + +export async function getStudent(id: string): Promise> { + try { + const response = await GET(`/student/${id}`); + const json = (await response.json()) as Student; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/components/Calendar/Datebox.tsx b/frontend/src/components/Calendar/Datebox.tsx index bd37c35a..847fd59d 100644 --- a/frontend/src/components/Calendar/Datebox.tsx +++ b/frontend/src/components/Calendar/Datebox.tsx @@ -7,9 +7,9 @@ export type DateboxProps = { }; export function Datebox({ day, hours, saturday }: DateboxProps) { - let boxClass = "border-r border-t p-2 flex flex-col items-center"; + let boxClass = "border-r border-t p-4 flex flex-col items-center"; if (saturday) { - boxClass = "border-t p-2 flex flex-col items-center"; + boxClass = "border-t p-4 flex flex-col items-center"; } return ( diff --git a/frontend/src/components/CalendarTable/CalendarTable.tsx b/frontend/src/components/CalendarTable/CalendarTable.tsx index d24ef1e6..87147306 100644 --- a/frontend/src/components/CalendarTable/CalendarTable.tsx +++ b/frontend/src/components/CalendarTable/CalendarTable.tsx @@ -10,10 +10,11 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import { getAllStudents } from "../../api/students"; import LoadingSpinner from "../LoadingSpinner"; -import { fuzzyFilter, programFilterFn, statusFilterFn } from "../StudentsTable/FilterFns"; import { StudentMap } from "../StudentsTable/types"; import { Table } from "../ui/table"; +import Filter, { fuzzyFilter, programFilterFn, statusFilterFn } from "./Filters"; + // import Filter from "./Filters"; import TBody from "./TBody"; import THead from "./THead"; @@ -67,9 +68,9 @@ export default function CalendarTable() { (program) => ({ id: student._id, - profilePicture: "filler", + profilePicture: "default", student: `${student.student.firstName} ${student.student.lastName}`, - programs: program, + programs: { ...program, studentId: student._id }, }) as CalendarTableRow, ); }); @@ -94,7 +95,7 @@ export default function CalendarTable() { globalFilter, }, onGlobalFilterChange: setGlobalFilter, - // globalFilterFn: fuzzyFilter, + globalFilterFn: fuzzyFilter, getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), @@ -117,6 +118,8 @@ export default function CalendarTable() {

Choose a Student to View Attendance

+ + {isLoading ? ( diff --git a/frontend/src/components/CalendarTable/Filters.tsx b/frontend/src/components/CalendarTable/Filters.tsx index fb76e18c..b6a23767 100644 --- a/frontend/src/components/CalendarTable/Filters.tsx +++ b/frontend/src/components/CalendarTable/Filters.tsx @@ -1,12 +1,101 @@ -import { Table } from "@tanstack/react-table"; +import { RankingInfo, rankItem } from "@tanstack/match-sorter-utils"; +import { FilterFn, Table } from "@tanstack/react-table"; import React from "react"; import SearchIcon from "../../../public/icons/search.svg"; import DebouncedInput from "../DebouncedInput"; +import { ProgramLink } from "../StudentForm/types"; import { ProgramFilter } from "../StudentsTable/FilterFns"; import { CalendarTableRow } from "./types"; +// Extend the FilterFns and FilterMeta interfaces +/* eslint-disable */ +declare module "@tanstack/table-core" { + interface FilterFns { + fuzzy: FilterFn; + programFilter: FilterFn; + statusFilter: FilterFn; + } + interface FilterMeta { + itemRank: RankingInfo; + } +} +/* eslint-enable */ + +export const programFilterFn: FilterFn = (rows, id, filterValue) => { + if (filterValue === "") return true; // no filter case + let containsProgram = false; + const programLinks: ProgramLink[] = rows.getValue(id); + programLinks.forEach((prog) => { + if (prog.programId === filterValue && prog.status === "Joined") { + containsProgram = true; + } + }); + return containsProgram; +}; + +export const statusFilterFn: FilterFn = (rows, id, filterValue) => { + if (filterValue === "") return true; // no filter case + let containsStatus = false; + const programLinks: ProgramLink[] = rows.getValue(id); + programLinks.forEach((prog) => { + if (prog.status === filterValue) { + containsStatus = true; + } + }); + return containsStatus; +}; + +// Filter function from tanstack docs for global filter (search in students ) +export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value as string); + + // Store the itemRank info + addMeta({ + itemRank, + }); + + // Return if the item should be filtered in/out + return itemRank.passed; +}; + +// export default function Filter({ +// globalFilter, +// setGlobalFilter, +// table, +// }: { +// globalFilter: string; +// setGlobalFilter: React.Dispatch>; +// table: Table; +// }) { +// return ( +//
+// {table.getHeaderGroups().map((headerGroup) => ( +// +// } +// value={globalFilter ?? ""} +// onChange={(val) => { +// setGlobalFilter(val); +// }} +// placeholder="Search in Students" +// className="h-full min-w-[300px] p-0 px-2 border border-gray-300 rounded-md bg-white" +// /> +// {headerGroup.headers.map((header) => { +// if (!header.column.getCanFilter()) return null; +// if (header.column.id === "programs") { +// return ; +// } +// return null; +// })} +// +// ))} +//
+// ); +// } + export default function Filter({ globalFilter, setGlobalFilter, @@ -17,25 +106,35 @@ export default function Filter({ table: Table; }) { return ( -
+
+ {/* Global Search Filter */} +
+ + { + setGlobalFilter(val); + }} + placeholder="Search in Students" + className="w-full border-none text-gray-600 outline-none" + /> +
+ + {/* Program Filter */} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { - if (!header.column.getCanFilter()) return null; - if (header.column.id === "Attendance") { - return ; + if (header.column.id === "programs" && header.column.getCanFilter()) { + return ( + + ); } return null; })} - } - value={globalFilter ?? ""} - onChange={(val) => { - setGlobalFilter(val); - }} - placeholder="Search in Students" - className="h-full min-w-[200px] p-0 px-2" - /> ))}
diff --git a/frontend/src/components/CalendarTable/types.ts b/frontend/src/components/CalendarTable/types.ts index 7995b946..aa03505f 100644 --- a/frontend/src/components/CalendarTable/types.ts +++ b/frontend/src/components/CalendarTable/types.ts @@ -2,11 +2,15 @@ import { ColumnDef } from "@tanstack/react-table"; import { ProgramLink } from "../StudentForm/types"; +export type EnrollmentLink = { + studentId: string; +} & ProgramLink; + export type CalendarTableRow = { id: string; profilePicture: string; student: string; - programs: ProgramLink; + programs: EnrollmentLink; }; -export type Columns = ColumnDef[]; +export type Columns = ColumnDef[]; diff --git a/frontend/src/components/CalendarTable/useColumnSchema.tsx b/frontend/src/components/CalendarTable/useColumnSchema.tsx index f1d4b833..913fbdff 100644 --- a/frontend/src/components/CalendarTable/useColumnSchema.tsx +++ b/frontend/src/components/CalendarTable/useColumnSchema.tsx @@ -1,17 +1,28 @@ +import Image from "next/image"; +import Link from "next/link"; import { useMemo } from "react"; -import { ProgramLink } from "../StudentForm/types"; import { ProgramMap } from "../StudentsTable/types"; import { ProgramPill } from "../StudentsTable/useColumnSchema"; -import { Columns } from "./types"; +import { Columns, EnrollmentLink } from "./types"; export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { const columns: Columns = [ { accessorKey: "profilePicture", header: "Profile Picture", - cell: () => filler, + cell: (info) => ( +
+ Profile Picture +
+ ), enableColumnFilter: false, }, { @@ -26,10 +37,23 @@ export function useColumnSchema({ allPrograms }: { allPrograms: ProgramMap }) { accessorKey: "programs", header: "Attendance", cell: (info) => { - const programLink = info.getValue() as unknown as ProgramLink; - const link = programLink.programId; - const program = allPrograms[link]; - return ; + const enrollmentLink = info.getValue() as unknown as EnrollmentLink; + const studentId = enrollmentLink.studentId; + const programId = enrollmentLink.programId; + const program = allPrograms[programId]; + return ( + + + + ); }, }, ]; diff --git a/frontend/src/pages/calendar.tsx b/frontend/src/pages/calendar.tsx index fad3ed6e..48aa85b2 100644 --- a/frontend/src/pages/calendar.tsx +++ b/frontend/src/pages/calendar.tsx @@ -1,56 +1,92 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useMemo, useState } from "react"; + +import Back from "../../public/icons/back.svg"; + +import { Student, getStudent } from "@/api/students"; +import { Calendar } from "@/components/Calendar/Calendar"; import CalendarTable from "@/components/CalendarTable/CalendarTable"; -// import Back from '../../public/icons/back.svg' -// import { Calendar as C } from "@/components/Calendar/Calendar" +import LoadingSpinner from "@/components/LoadingSpinner"; import { useRedirectToLoginIfNotSignedIn } from "@/hooks/redirect"; -// import { useWindowSize } from "@/hooks/useWindowSize"; -// import { useMemo } from "react"; +import { useWindowSize } from "@/hooks/useWindowSize"; -export default function Calendar() { +export default function Component() { useRedirectToLoginIfNotSignedIn(); - // const { windowSize } = useWindowSize(); - // const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); - // const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); - // const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); - - // let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; - // let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; - // let titleClass = "font-[alternate-gothic]"; - // const backButton = "flex space-x-0 text-lg"; - - // if (isTablet) { - // titleClass += " text-2xl leading-none h-6"; - // mainClass += " p-0"; - - // if (isMobile) { - // headerClass += " pt-2 pb-3"; - // } else { - // headerClass += " p-2 py-4"; - // } - // } else { - // headerClass += "pt-10 pb-5"; - - // if (extraLarge) { - // headerClass += " max-w-[1740px]"; - // } else { - // headerClass += " max-w-[1160px]"; - // } - // } + const router = useRouter(); + const { student, program } = router.query; + + const [currStudent, setStudent] = useState(); + // const [currProgram, setProgram ] = useState(); + const [isLoading, setIsLoading] = useState(true); + + const { windowSize } = useWindowSize(); + const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]); + const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]); + const extraLarge = useMemo(() => windowSize.width >= 2000, [windowSize.width]); + + if (!student || !program) return ; + + getStudent(student as string).then( + (result) => { + if (result.success) { + setStudent(result.data); + setIsLoading(false); + } else { + console.log(result.error); + } + }, + (error) => { + console.log(error); + }, + ); + + let mainClass = "h-full overflow-y-scroll no-scrollbar flex flex-col"; + let headerClass = "mb-5 font-[alternate-gothic] text-2xl lg:text-4xl "; + let titleClass = "font-[alternate-gothic]"; + const backButton = "flex space-x-0 text-lg"; + + if (isTablet) { + titleClass += " text-2xl leading-none h-6"; + mainClass += " p-0"; + + if (isMobile) { + headerClass += " pt-2 pb-3"; + } else { + headerClass += " p-2 py-4"; + } + } else { + headerClass += "pt-10 pb-5"; + + if (extraLarge) { + headerClass += " max-w-[1740px]"; + } else { + headerClass += " max-w-[1160px]"; + } + } return ( - //
- //
- //
- // - // {!isTablet &&

Student List

} - //
- //

- // Alice Anderson - UCI # 123456 - //

- //
- - // - //
- +
+ {isLoading ? ( + + ) : ( +
+
+
+ + + + {!isTablet &&

Student List

} +
+

+ {currStudent?.student.firstName + " " + currStudent?.student.lastName} - UCI # + 123456789 +

+
+ +
+ )} +
); }