Skip to content

Commit

Permalink
completed UI
Browse files Browse the repository at this point in the history
  • Loading branch information
adhi0331 committed Nov 23, 2024
1 parent c61aebc commit b81d21c
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 76 deletions.
17 changes: 17 additions & 0 deletions backend/src/controllers/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
};
1 change: 1 addition & 0 deletions backend/src/routes/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
10 changes: 10 additions & 0 deletions frontend/src/api/students.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ export async function getAllStudents(): Promise<APIResult<[Student]>> {
return handleAPIError(error);
}
}

export async function getStudent(id: string): Promise<APIResult<Student>> {
try {
const response = await GET(`/student/${id}`);
const json = (await response.json()) as Student;
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
4 changes: 2 additions & 2 deletions frontend/src/components/Calendar/Datebox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/CalendarTable/CalendarTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Check warning on line 16 in frontend/src/components/CalendarTable/CalendarTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend lint and style check

There should be no empty line within import group

// import Filter from "./Filters";
import TBody from "./TBody";
import THead from "./THead";
Expand Down Expand Up @@ -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,
);
});
Expand All @@ -94,7 +95,7 @@ export default function CalendarTable() {
globalFilter,
},
onGlobalFilterChange: setGlobalFilter,
// globalFilterFn: fuzzyFilter,
globalFilterFn: fuzzyFilter,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
Expand All @@ -117,6 +118,8 @@ export default function CalendarTable() {
</h1>

<p>Choose a Student to View Attendance</p>

<Filter globalFilter={globalFilter} setGlobalFilter={setGlobalFilter} table={table} />
</div>
{isLoading ? (
<LoadingSpinner />
Expand Down
127 changes: 113 additions & 14 deletions frontend/src/components/CalendarTable/Filters.tsx
Original file line number Diff line number Diff line change
@@ -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<unknown>;
programFilter: FilterFn<unknown>;
statusFilter: FilterFn<unknown>;
}
interface FilterMeta {
itemRank: RankingInfo;
}
}
/* eslint-enable */

export const programFilterFn: FilterFn<unknown> = (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<unknown> = (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<CalendarTableRow> = (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<React.SetStateAction<string>>;
// table: Table<CalendarTableRow>;
// }) {
// return (
// <div className="flex items-center space-x-4 mb-4">
// {table.getHeaderGroups().map((headerGroup) => (
// <React.Fragment key={headerGroup.id}>
// <DebouncedInput
// icon={<SearchIcon width="20" height="20" />}
// 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 <ProgramFilter key={header.id} setValue={header.column.setFilterValue} />;
// }
// return null;
// })}
// </React.Fragment>
// ))}
// </div>
// );
// }

export default function Filter({
globalFilter,
setGlobalFilter,
Expand All @@ -17,25 +106,35 @@ export default function Filter({
table: Table<CalendarTableRow>;
}) {
return (
<div>
<div className="mb-6 flex items-center space-x-4">
{/* Global Search Filter */}
<div className="flex h-12 items-center rounded-lg border border-gray-300 bg-white px-3 py-2">
<SearchIcon width="20" height="20" className="mr-2 text-gray-400" />
<DebouncedInput
value={globalFilter ?? ""}
onChange={(val) => {
setGlobalFilter(val);
}}
placeholder="Search in Students"
className="w-full border-none text-gray-600 outline-none"
/>
</div>

{/* Program Filter */}
{table.getHeaderGroups().map((headerGroup) => (
<React.Fragment key={headerGroup.id}>
{headerGroup.headers.map((header) => {
if (!header.column.getCanFilter()) return null;
if (header.column.id === "Attendance") {
return <ProgramFilter key={header.id} setValue={header.column.setFilterValue} />;
if (header.column.id === "programs" && header.column.getCanFilter()) {
return (
<ProgramFilter
key={header.id}
setValue={header.column.setFilterValue}
className="h-12 rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-700"
/>
);
}
return null;
})}
<DebouncedInput
icon={<SearchIcon width="20" height="20" />}
value={globalFilter ?? ""}
onChange={(val) => {
setGlobalFilter(val);
}}
placeholder="Search in Students"
className="h-full min-w-[200px] p-0 px-2"
/>
</React.Fragment>
))}
</div>
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/CalendarTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CalendarTableRow, ProgramLink | string>[];
export type Columns = ColumnDef<CalendarTableRow, EnrollmentLink | string>[];
38 changes: 31 additions & 7 deletions frontend/src/components/CalendarTable/useColumnSchema.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <span className="truncate pl-10 pr-2.5 hover:text-clip">filler</span>,
cell: (info) => (
<div className="flex h-full w-full items-center justify-center">
<Image
alt="Profile Picture"
src={(info.getValue() as string) === "default" ? "../defaultProfilePic.svg" : ""}
className="rounded-full object-cover"
width={50}
height={50}
/>
</div>
),
enableColumnFilter: false,
},
{
Expand All @@ -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 <ProgramPill name={program.abbreviation} color={program.color} />;
const enrollmentLink = info.getValue() as unknown as EnrollmentLink;
const studentId = enrollmentLink.studentId;
const programId = enrollmentLink.programId;
const program = allPrograms[programId];
return (
<Link
href={{
pathname: "./calendar",
query: {
student: studentId,
program: programId,
},
}}
>
<ProgramPill name={program.abbreviation} color={program.color} />
</Link>
);
},
},
];
Expand Down
Loading

0 comments on commit b81d21c

Please sign in to comment.