) {
+ defaultValue = defaultValue?.map((item) => item.toLowerCase());
+
return (
{options.map((item, index) => {
diff --git a/frontend/src/components/DebouncedInput.tsx b/frontend/src/components/DebouncedInput.tsx
new file mode 100644
index 00000000..1ddced1c
--- /dev/null
+++ b/frontend/src/components/DebouncedInput.tsx
@@ -0,0 +1,46 @@
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { Textfield } from "./Textfield";
+
+const DebouncedInput = ({
+ value: initialValue,
+ onChange,
+ debounce = 500,
+ placeholder = "",
+}: {
+ value: string;
+ onChange: (val: string) => void;
+ debounce?: number;
+ placeholder?: string;
+} & Omit, "onChange">) => {
+ const [value, setValue] = useState(initialValue);
+ const { register } = useForm();
+
+ useEffect(() => {
+ setValue(initialValue);
+ }, [initialValue]);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ onChange(value);
+ }, debounce);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [value]);
+
+ return (
+ {
+ setValue(e.target.value);
+ }}
+ />
+ );
+};
+
+export default DebouncedInput;
diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx
new file mode 100644
index 00000000..0d97f000
--- /dev/null
+++ b/frontend/src/components/Dropdown.tsx
@@ -0,0 +1,96 @@
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import { FieldValues, Path, PathValue, UseFormSetValue } from "react-hook-form";
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "../components/ui/dropdown";
+
+import { cn } from "@/lib/utils";
+
+type BaseProps = {
+ name: Path;
+ label?: string;
+ placeholder: string;
+ defaultValue?: string;
+ className?: string;
+};
+
+type DropdownProps = BaseProps & {
+ options: string[];
+ setDropdownValue?: UseFormSetValue;
+ onChange?: (val: string) => void;
+};
+
+export function Dropdown({
+ setDropdownValue,
+ label,
+ name,
+ options,
+ onChange = () => void 0,
+ defaultValue = "",
+ className,
+}: DropdownProps) {
+ const [selectedOption, setSelectedOption] = useState(defaultValue);
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ if (selectedOption && setDropdownValue) {
+ setDropdownValue(name, selectedOption as PathValue>);
+ }
+ }, [selectedOption]);
+
+ return (
+ {
+ setOpen(!open);
+ }}
+ >
+
+ {label + ": "}
+ {selectedOption ?? ""}
+
+
+
+ {
+ onChange("");
+ setSelectedOption(defaultValue);
+ }}
+ >
+ {defaultValue}
+
+ {options.map((option) => (
+ {
+ onChange(option);
+ setSelectedOption(option);
+ }}
+ >
+ {option}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx
index 842c9f0a..72a2400a 100644
--- a/frontend/src/components/Landing.tsx
+++ b/frontend/src/components/Landing.tsx
@@ -6,7 +6,7 @@
*/
import { Poppins } from "next/font/google";
import Image from "next/image";
-import React, { useMemo } from "react";
+import React from "react";
import { useWindowSize } from "../hooks/useWindowSize";
import { cn } from "../lib/utils";
@@ -32,8 +32,7 @@ const Logo = () => {
// Navigation component that wraps the content of the page
function Landing({ children }: { children: React.ReactNode }) {
- const { width } = useWindowSize();
- const isMobile = useMemo(() => width <= 640, [width]);
+ const { isMobile } = useWindowSize();
return (
width <= 640, [width]);
+ const { isMobile } = useWindowSize();
useEffect(() => {
const ordering = navigation.map((item) => item.href);
diff --git a/frontend/src/components/StudentForm/ContactInfo.tsx b/frontend/src/components/StudentForm/ContactInfo.tsx
index 30defb6d..a7f73f18 100644
--- a/frontend/src/components/StudentForm/ContactInfo.tsx
+++ b/frontend/src/components/StudentForm/ContactInfo.tsx
@@ -1,9 +1,10 @@
import { UseFormRegister } from "react-hook-form";
+import { Student } from "../../api/students";
import { cn } from "../../lib/utils";
import { Textfield } from "../Textfield";
-import { StudentData, StudentFormData } from "./types";
+import { StudentFormData } from "./types";
type ContactRole = "student" | "emergency" | "serviceCoordinator";
@@ -13,7 +14,7 @@ type ContactInfoProps = {
register: UseFormRegister;
classname?: string;
type: "add" | "edit";
- data: StudentData | null;
+ data: Student | null;
};
type FieldProps = {
@@ -63,7 +64,8 @@ export default function ContactInfo({ register, type, data, classname }: Contact
const toTitleCase = (word: string) => {
return word
- .split("_")
+ .replace(/[A-Z]/g, " $&")
+ .split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
.join(" ");
};
diff --git a/frontend/src/components/StudentForm/StudentBackground.tsx b/frontend/src/components/StudentForm/StudentBackground.tsx
index 59d1bcea..6bd8be40 100644
--- a/frontend/src/components/StudentForm/StudentBackground.tsx
+++ b/frontend/src/components/StudentForm/StudentBackground.tsx
@@ -1,20 +1,31 @@
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
+import { Student } from "../../api/students";
import { cn } from "../../lib/utils";
import { Checkbox } from "../Checkbox";
import { Textfield } from "../Textfield";
-import { StudentData, StudentFormData } from "./types";
+import { StudentFormData } from "./types";
type StudentBackgroundProps = {
register: UseFormRegister;
classname?: string;
setCalendarValue: UseFormSetValue;
- data: StudentData | null;
+ data: Student | null;
};
const dietaryList = ["Nuts", "Eggs", "Seafood", "Pollen", "Dairy", "Other"];
+export const convertDateToString = (date: Date | undefined) => {
+ return date
+ ? new Date(date).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ })
+ : "";
+};
+
export default function StudentBackground({
data,
register,
@@ -41,7 +52,7 @@ export default function StudentBackground({
placeholder="00/00/0000"
calendar={true}
setCalendarValue={setCalendarValue}
- defaultValue={data?.birthday}
+ defaultValue={convertDateToString(data?.birthday)}
/>
@@ -60,7 +71,7 @@ export default function StudentBackground({
register={register}
name="dietary"
options={dietaryList}
- defaultValue={data?.dietary.map((item) => item.toLowerCase())}
+ defaultValue={data?.dietary}
defaultOtherValue={data?.otherString}
className="sm:grid-cols-2 min-[1150px]:grid-cols-3"
/>
diff --git a/frontend/src/components/StudentForm/StudentInfo.tsx b/frontend/src/components/StudentForm/StudentInfo.tsx
index bcec3220..07532dc1 100644
--- a/frontend/src/components/StudentForm/StudentInfo.tsx
+++ b/frontend/src/components/StudentForm/StudentInfo.tsx
@@ -1,16 +1,18 @@
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
+import { Student } from "../../api/students";
import { cn } from "../../lib/utils";
import { Checkbox } from "../Checkbox";
import { Textfield } from "../Textfield";
-import { StudentData, StudentFormData } from "./types";
+import { convertDateToString } from "./StudentBackground";
+import { StudentFormData } from "./types";
type StudentInfoProps = {
register: UseFormRegister;
classname?: string;
setCalendarValue: UseFormSetValue;
- data: StudentData | null;
+ data: Student | null;
};
const regularPrograms = ["Intro", "ENTR"];
@@ -33,7 +35,7 @@ export default function StudentInfo({
placeholder="00/00/0000"
calendar={true}
setCalendarValue={setCalendarValue}
- defaultValue={data?.intakeDate}
+ defaultValue={convertDateToString(data?.intakeDate)}
/>
@@ -44,7 +46,7 @@ export default function StudentInfo({
placeholder="00/00/0000"
calendar={true}
setCalendarValue={setCalendarValue}
- defaultValue={data?.tourDate}
+ defaultValue={convertDateToString(data?.tourDate)}
/>
@@ -55,16 +57,16 @@ export default function StudentInfo({
register={register}
name="regular_programs"
options={regularPrograms}
- defaultValue={data?.prog1}
+ defaultValue={data?.regularPrograms}
/>
Varying Programs
diff --git a/frontend/src/components/StudentForm/types.ts b/frontend/src/components/StudentForm/types.ts
index ec93a05b..5b6f025f 100644
--- a/frontend/src/components/StudentForm/types.ts
+++ b/frontend/src/components/StudentForm/types.ts
@@ -11,11 +11,11 @@ export type StudentData = {
serviceCoordinator: Contact;
location: string;
medication: string;
- birthday: string;
- intakeDate: string;
- tourDate: string;
- prog1: string[];
- prog2: string[];
+ birthday: Date;
+ intakeDate: Date;
+ tourDate: Date;
+ regularPrograms: string[];
+ varyingPrograms: string[];
dietary: string[];
otherString: string;
};
@@ -34,11 +34,12 @@ export type StudentFormData = {
serviceCoordinator_email: string;
serviceCoordinator_phone: string;
address: string;
- birthdate: string;
+ birthdate: Date;
medication: string;
dietary: string[];
other: string;
- intake_date: string;
- tour_date: string;
+ intake_date: Date;
+ tour_date: Date;
regular_programs: string[];
+ varying_programs: string[];
};
diff --git a/frontend/src/components/StudentFormButton.tsx b/frontend/src/components/StudentFormButton.tsx
index bd19010b..c86a6c97 100644
--- a/frontend/src/components/StudentFormButton.tsx
+++ b/frontend/src/components/StudentFormButton.tsx
@@ -1,6 +1,8 @@
-import { useState } from "react";
+import Image from "next/image";
+import { Dispatch, SetStateAction, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
+import { Student, createStudent, editStudent } from "../api/students";
import { cn } from "../lib/utils";
import { Button } from "./Button";
@@ -8,32 +10,45 @@ import ContactInfo from "./StudentForm/ContactInfo";
import StudentBackground from "./StudentForm/StudentBackground";
import StudentInfo from "./StudentForm/StudentInfo";
import { StudentData, StudentFormData } from "./StudentForm/types";
+import { StudentMap } from "./StudentsTable/types";
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "./ui/dialog";
type BaseProps = {
classname?: string;
+ setAllStudents: Dispatch>;
};
type EditProps = BaseProps & {
type: "edit";
- data: StudentData | null;
+ data: Student | null;
};
type AddProps = BaseProps & {
type: "add";
- data?: StudentData | null;
+ data?: Student | null;
};
type StudentFormProps = EditProps | AddProps;
export default function StudentFormButton({
- type = "edit",
- data = null,
+ type,
+ data = null, //Student data so form can be populated
+ setAllStudents, //Update state of allStudents after creating or editing student
classname,
}: StudentFormProps) {
- const { register, setValue: setCalendarValue, reset, handleSubmit } = useForm();
+ const {
+ register,
+ setValue: setCalendarValue,
+ reset,
+ handleSubmit,
+ } = useForm({
+ defaultValues: { varying_programs: [], regular_programs: [], dietary: [] },
+ });
+ //Default values can be set for all fields but I specified these three fields because the checkbox value can sometimes be a string if it's a single value rather than array of strings. https://github.com/react-hook-form/react-hook-form/releases/tag/v7.30.0
- const onSubmit: SubmitHandler = (formData: StudentFormData) => {
+ const [openForm, setOpenForm] = useState(false);
+
+ const onFormSubmit: SubmitHandler = (formData: StudentFormData) => {
const transformedData: StudentData = {
student: {
firstName: formData.student_name,
@@ -55,34 +70,88 @@ export default function StudentFormButton({
},
location: formData.address,
medication: formData.medication,
- birthday: formData.birthdate,
- intakeDate: formData.intake_date,
- tourDate: formData.tour_date,
- prog1: formData.regular_programs,
- prog2: formData.regular_programs,
+ birthday: new Date(formData.birthdate),
+ intakeDate: new Date(formData.intake_date),
+ tourDate: new Date(formData.tour_date),
+ regularPrograms: formData.regular_programs,
+ varyingPrograms: formData.varying_programs,
dietary: formData.dietary,
otherString: formData.other,
};
- reset(); //Clear form
- console.log(`${type} student data:`, transformedData);
- };
- const [openForm, setOpenForm] = useState(false);
+ if (type === "add") {
+ createStudent(transformedData).then(
+ (result) => {
+ if (result.success) {
+ const newStudent = result.data;
+ reset(); // only clear form on success
+ setOpenForm(false);
+ setAllStudents((prevStudents: StudentMap) => {
+ return { ...prevStudents, [newStudent._id]: newStudent };
+ });
+ } else {
+ console.log(result.error);
+ alert("Unable to create student: " + result.error);
+ }
+ },
+ (error) => {
+ console.log(error);
+ },
+ );
+ }
+
+ if (type === "edit" && data) {
+ const editedData: Student = { ...transformedData, _id: data._id };
+ editStudent(editedData).then(
+ (result) => {
+ if (result.success) {
+ const editedStudent = result.data;
+ setOpenForm(false);
+ setAllStudents((prevStudents: StudentMap) => {
+ if (Object.keys(prevStudents).includes(editedStudent._id)) {
+ return { ...prevStudents, [editedStudent._id]: editedStudent };
+ } else {
+ console.log("Student ID is invalid");
+ alert("Student ID is invalid");
+ return prevStudents;
+ }
+ });
+ } else {
+ console.log(result.error);
+ alert("Unable to edit student: " + result.error);
+ }
+ },
+ (error) => {
+ console.log(error);
+ },
+ );
+ }
+ };
return (
<>
-
-
-
diff --git a/frontend/src/components/StudentsTable/ProgramFilter.tsx b/frontend/src/components/StudentsTable/ProgramFilter.tsx
new file mode 100644
index 00000000..c135f24d
--- /dev/null
+++ b/frontend/src/components/StudentsTable/ProgramFilter.tsx
@@ -0,0 +1,34 @@
+import { Column } from "@tanstack/react-table";
+import { useMemo } from "react";
+
+import { Student } from "../../api/students";
+import { Dropdown } from "../Dropdown";
+
+export default function ProgramFilter({ column }: { column: Column }) {
+ // Get unique programs to display in the program filter dropdown
+ const sortedUniqueValues = useMemo(() => {
+ const values = new Set();
+
+ Array.from(column.getFacetedUniqueValues().keys() as unknown as string[])
+ .sort()
+ .forEach((programs: string) => programs.split(";").map((v: string) => values.add(v)));
+
+ return Array.from(values)
+ .sort()
+ .filter((v) => v !== "");
+ }, [column.getFacetedUniqueValues()]);
+
+ return (
+ {
+ column.setFilterValue(value);
+ }}
+ />
+ );
+}
diff --git a/frontend/src/components/StudentsTable/StudentsTable.tsx b/frontend/src/components/StudentsTable/StudentsTable.tsx
new file mode 100644
index 00000000..83a6ce68
--- /dev/null
+++ b/frontend/src/components/StudentsTable/StudentsTable.tsx
@@ -0,0 +1,116 @@
+import {
+ ColumnFiltersState,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import React, { useEffect, useMemo, useState } from "react";
+
+import { getAllStudents } from "../../api/students";
+import { fuzzyFilter } from "../../lib/fuzzyFilter";
+import StudentFormButton from "../StudentFormButton";
+import { Table } from "../ui/table";
+
+import TBody from "./TBody";
+import THead from "./THead";
+import { StudentMap } from "./types";
+import { useColumnSchema } from "./useColumnSchema";
+
+import { useWindowSize } from "@/hooks/useWindowSize";
+import { cn } from "@/lib/utils";
+
+export default function StudentsTable() {
+ const [allStudents, setAllStudents] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [columnFilters, setColumnFilters] = useState([]);
+ const [globalFilter, setGlobalFilter] = useState("");
+ const { isTablet } = useWindowSize();
+
+ useEffect(() => {
+ getAllStudents().then(
+ (result) => {
+ if (result.success) {
+ // Convert student array to object with keys as ids and values as corresponding student
+ const studentsObject = result.data.reduce((obj, student) => {
+ obj[student._id] = student;
+ return obj;
+ }, {} as StudentMap);
+ console.log(result.data);
+
+ setAllStudents(studentsObject);
+ setIsLoading(false);
+ } else {
+ console.log(result.error);
+ }
+ },
+ (error) => {
+ console.log(error);
+ },
+ );
+ }, []);
+
+ const columns = useColumnSchema({ setAllStudents });
+ const data = useMemo(() => Object.values(allStudents), [allStudents]);
+ // const data = useMemo(() => [], [allStudents]); // uncomment this line and comment the line above to see empty table state
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ filterFns: {
+ fuzzy: fuzzyFilter,
+ },
+ state: {
+ columnFilters,
+ globalFilter,
+ },
+ onColumnFiltersChange: setColumnFilters,
+ onGlobalFilterChange: setGlobalFilter,
+ globalFilterFn: fuzzyFilter,
+ getFilteredRowModel: getFilteredRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ debugTable: true,
+ debugHeaders: true,
+ debugColumns: false,
+ });
+
+ if (isLoading) return Loading...
;
+
+ if (Object.keys(allStudents).length === 0)
+ return Please add a student first!
;
+
+ return (
+
+
+
+ Students
+
+ {isTablet && }
+
+
+
+ );
+}
diff --git a/frontend/src/components/StudentsTable/TBody.tsx b/frontend/src/components/StudentsTable/TBody.tsx
new file mode 100644
index 00000000..ef3fbe15
--- /dev/null
+++ b/frontend/src/components/StudentsTable/TBody.tsx
@@ -0,0 +1,45 @@
+import { Table, flexRender } from "@tanstack/react-table";
+import Image from "next/image";
+
+import { TableBody, TableCell, TableRow } from "../ui/table";
+
+import { Student } from "@/api/students";
+
+export default function TBody({ table }: { table: Table }) {
+ // If there are no students, display a placeholder
+ if (table.getRowModel().rows.length === 0) {
+ return (
+
+
+
+
+
+
+ No Students
+
+
+
+
+ );
+ }
+
+ return (
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/StudentsTable/THead.tsx b/frontend/src/components/StudentsTable/THead.tsx
new file mode 100644
index 00000000..a9aeccfe
--- /dev/null
+++ b/frontend/src/components/StudentsTable/THead.tsx
@@ -0,0 +1,94 @@
+import { HeaderGroup, Table, flexRender } from "@tanstack/react-table";
+
+import DebouncedInput from "../DebouncedInput";
+import StudentFormButton from "../StudentFormButton";
+import { TableHead, TableHeader, TableRow } from "../ui/table";
+
+import ProgramFilter from "./ProgramFilter";
+import { StudentMap } from "./types";
+
+import { Student } from "@/api/students";
+import { useWindowSize } from "@/hooks/useWindowSize";
+
+function TableActionsHeader({
+ headerGroup,
+ globalFilter,
+ setGlobalFilter,
+ setAllStudents,
+}: {
+ headerGroup: HeaderGroup;
+ globalFilter: string;
+ setGlobalFilter: React.Dispatch>;
+ setAllStudents: React.Dispatch>;
+}) {
+ const { isTablet } = useWindowSize();
+
+ return (
+
+
+
+
+ {headerGroup.headers.map((header) =>
+ header.column.getCanFilter() ? (
+
+ ) : null,
+ )}
+
+ {
+ setGlobalFilter(val);
+ }}
+ className="font-lg border-block border p-2 shadow"
+ placeholder="Search in Students"
+ />
+
+
+ {!isTablet &&
}
+
+
+
+ );
+}
+
+function TableDataHeader({ headerGroup }: { headerGroup: HeaderGroup }) {
+ return (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+
+ ))}
+
+ );
+}
+
+export default function THead({
+ globalFilter,
+ setGlobalFilter,
+ setAllStudents,
+ table,
+}: {
+ globalFilter: string;
+ setGlobalFilter: React.Dispatch>;
+ setAllStudents: React.Dispatch>;
+ table: Table;
+}) {
+ return (
+
+ {table.getHeaderGroups().map((headerGroup) => (
+ <>
+
+
+ >
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/StudentsTable/types.ts b/frontend/src/components/StudentsTable/types.ts
new file mode 100644
index 00000000..1f077890
--- /dev/null
+++ b/frontend/src/components/StudentsTable/types.ts
@@ -0,0 +1,7 @@
+import { ColumnDef } from "@tanstack/react-table";
+
+import { Student } from "../../api/students";
+import { Contact } from "../StudentForm/types";
+
+export type Columns = ColumnDef[];
+export type StudentMap = Record;
diff --git a/frontend/src/components/StudentsTable/useColumnSchema.tsx b/frontend/src/components/StudentsTable/useColumnSchema.tsx
new file mode 100644
index 00000000..aef1a6f6
--- /dev/null
+++ b/frontend/src/components/StudentsTable/useColumnSchema.tsx
@@ -0,0 +1,100 @@
+import { useEffect, useMemo } from "react";
+
+import { Contact } from "../StudentForm/types";
+import StudentFormButton from "../StudentFormButton";
+
+import { Columns, StudentMap } from "./types";
+
+import { useWindowSize } from "@/hooks/useWindowSize";
+import { formatPhoneNumber } from "@/lib/formatPhoneNumber";
+
+export function useColumnSchema({
+ setAllStudents,
+}: {
+ setAllStudents: React.Dispatch>;
+}) {
+ const { isTablet } = useWindowSize();
+
+ useEffect(() => {
+ console.log(isTablet);
+ }, [isTablet]);
+
+ const columns: Columns = [
+ {
+ accessorKey: "student",
+ header: "Name",
+ accessorFn: (row) => row.student.firstName + " " + row.student.lastName,
+ cell: (info) => (
+ {info.getValue() as string}
+ ),
+ enableColumnFilter: false,
+ },
+ {
+ accessorKey: "regularPrograms",
+ id: isTablet ? "Curr. P1" : "Curr. Program 1",
+ header: isTablet ? "Curr. P1" : "Curr. Program 1",
+ accessorFn: (row) => row.regularPrograms.join(";"),
+ cell: (info) =>
+ (info.getValue() as string).split(";")[0] && (
+
+ {(info.getValue() as string).split(";")[0]}
+
+ ),
+ filterFn: "arrIncludes",
+ },
+ {
+ accessorKey: "regularPrograms",
+ id: isTablet ? "Curr. P2" : "Curr. Program 2",
+ header: isTablet ? "Curr. P2" : "Curr. Program 2",
+ accessorFn: (row) => row.regularPrograms.join(";"),
+ cell: (info) =>
+ (info.getValue() as string).split(";")[1] && (
+
+ {(info.getValue() as string).split(";")[1]}
+
+ ),
+ enableColumnFilter: false,
+ },
+ {
+ accessorKey: "regularPrograms",
+ id: "Program History",
+ header: "Program History",
+ accessorFn: (row) => row.regularPrograms.length.toString(),
+ cell: (info) => (
+ {(info.getValue() as string) + " programs"}
+ ),
+ enableColumnFilter: false,
+ },
+ {
+ accessorKey: "emergency",
+ header: "Emergency Contact",
+ size: 200,
+ cell: (info) => {
+ const contact = info.getValue() as Contact;
+ return (
+
+ {contact.firstName + " " + contact.lastName}
+ {formatPhoneNumber(contact.phoneNumber)}
+
+ );
+ },
+ enableColumnFilter: false,
+ },
+ {
+ id: "Actions",
+ cell: (info) => {
+ return (
+
+
+
+ );
+ },
+ },
+ ];
+
+ return useMemo(() => columns, [setAllStudents, isTablet]);
+}
diff --git a/frontend/src/components/Textfield.tsx b/frontend/src/components/Textfield.tsx
index 6b55b652..b6658f1d 100644
--- a/frontend/src/components/Textfield.tsx
+++ b/frontend/src/components/Textfield.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { useEffect, useState } from "react";
import { FieldValues, Path, PathValue, UseFormRegister, UseFormSetValue } from "react-hook-form";
@@ -72,15 +70,7 @@ export function Textfield({
type={type}
onChange={handleInputChange}
placeholder={placeholder}
- defaultValue={
- calendar && defaultValue
- ? new Date(defaultValue).toLocaleDateString(undefined, {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- })
- : defaultValue
- }
+ defaultValue={defaultValue}
/>
{label ? (
diff --git a/frontend/src/components/ui/dropdown.tsx b/frontend/src/components/ui/dropdown.tsx
new file mode 100644
index 00000000..42e809a2
--- /dev/null
+++ b/frontend/src/components/ui/dropdown.tsx
@@ -0,0 +1,187 @@
+"use client";
+
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx
new file mode 100644
index 00000000..7a7026f0
--- /dev/null
+++ b/frontend/src/components/ui/table.tsx
@@ -0,0 +1,91 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Table = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+Table.displayName = "Table";
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHeader.displayName = "TableHeader";
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableBody.displayName = "TableBody";
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0", className)}
+ {...props}
+ />
+));
+TableFooter.displayName = "TableFooter";
+
+const TableRow = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+TableRow.displayName = "TableRow";
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+));
+TableHead.displayName = "TableHead";
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
diff --git a/frontend/src/hooks/useWindowSize.ts b/frontend/src/hooks/useWindowSize.ts
index 35f4bd80..d18d467f 100644
--- a/frontend/src/hooks/useWindowSize.ts
+++ b/frontend/src/hooks/useWindowSize.ts
@@ -2,7 +2,7 @@
* Hook that allows components to keep track of current page width and height
*/
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
@@ -24,5 +24,9 @@ export function useWindowSize() {
window.removeEventListener("resize", handleResize);
};
}, []);
- return windowSize;
+
+ const isMobile = useMemo(() => windowSize.width < 640, [windowSize.width]);
+ const isTablet = useMemo(() => windowSize.width < 1024, [windowSize.width]);
+
+ return { windowSize, isMobile, isTablet };
}
diff --git a/frontend/src/lib/formatPhoneNumber.ts b/frontend/src/lib/formatPhoneNumber.ts
new file mode 100644
index 00000000..32cc675e
--- /dev/null
+++ b/frontend/src/lib/formatPhoneNumber.ts
@@ -0,0 +1,8 @@
+export function formatPhoneNumber(phoneNumberString: string) {
+ const cleaned = ("" + phoneNumberString).replace(/\D/g, "");
+ const match = /^(\d{3})(\d{3})(\d{4})$/.exec(cleaned);
+ if (match) {
+ return "(" + match[1] + ") " + match[2] + "-" + match[3];
+ }
+ return null;
+}
diff --git a/frontend/src/lib/fuzzyFilter.ts b/frontend/src/lib/fuzzyFilter.ts
new file mode 100644
index 00000000..985c0508
--- /dev/null
+++ b/frontend/src/lib/fuzzyFilter.ts
@@ -0,0 +1,28 @@
+import { RankingInfo, rankItem } from "@tanstack/match-sorter-utils";
+import { FilterFn } from "@tanstack/react-table";
+
+import { Student } from "@/api/students";
+
+/* eslint-disable */
+declare module "@tanstack/table-core" {
+ interface FilterFns {
+ fuzzy: FilterFn;
+ }
+ interface FilterMeta {
+ itemRank: RankingInfo;
+ }
+}
+/* eslint-enable */
+
+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;
+};
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 034bdbed..83b7a073 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -1,20 +1,26 @@
import { AppProps } from "next/app";
-
-import Landing from "../components/Landing";
-
import "../styles/global.css";
import "../styles/globals.css";
+import { ReactElement, ReactNode } from "react";
+
+import Navigation from "@/components/Navigation";
+
+// eslint-disable-next-line import/order
+import { NextPage } from "next";
// import Navigation from "../components/Navigation";
+export type NextPageWithLayout = NextPage
& {
+ getLayout?: (page: ReactElement) => ReactNode;
+};
+
+type AppPropsWithLayout = AppProps & {
+ Component: NextPageWithLayout;
+};
-function App({ Component, pageProps }: AppProps) {
- return (
-
-
-
- //
- //
- //
- );
+//Unless specified, the default layout will have the Navigation bar
+function App({ Component, pageProps }: AppPropsWithLayout) {
+ const getLayout = Component.getLayout ?? ((page) => {page});
+ return getLayout();
}
+
export default App;
diff --git a/frontend/src/pages/create_user.tsx b/frontend/src/pages/create_user.tsx
index bc9ff34e..03f4d325 100644
--- a/frontend/src/pages/create_user.tsx
+++ b/frontend/src/pages/create_user.tsx
@@ -1,9 +1,10 @@
import { AlertCircle, CheckCircle2 } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
-import { useMemo, useState } from "react";
+import { ReactElement, useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
+import Landing from "@/components/Landing";
import { Textfield } from "@/components/Textfield";
import { useWindowSize } from "@/hooks/useWindowSize";
import { cn } from "@/lib/utils";
@@ -44,8 +45,7 @@ export default function CreateUser() {
void router.push("/create_user_2");
};
- const { width } = useWindowSize();
- const isMobile = useMemo(() => width <= 640, [width]);
+ const { isMobile } = useWindowSize();
return (
@@ -183,3 +183,7 @@ export default function CreateUser() {
);
}
+
+CreateUser.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
diff --git a/frontend/src/pages/create_user_2.tsx b/frontend/src/pages/create_user_2.tsx
index ac63caa7..70184f04 100644
--- a/frontend/src/pages/create_user_2.tsx
+++ b/frontend/src/pages/create_user_2.tsx
@@ -1,10 +1,11 @@
import { ArrowLeft, ArrowRight } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
-import { MouseEvent, useMemo, useState } from "react";
+import { MouseEvent, ReactElement, useState } from "react";
import { FieldValues, SubmitHandler } from "react-hook-form";
import { Button } from "@/components/Button";
+import Landing from "@/components/Landing";
import { useWindowSize } from "@/hooks/useWindowSize";
import { cn } from "@/lib/utils";
@@ -22,8 +23,7 @@ export default function CreateUser() {
console.log(data);
void router.push("/create_user");
};
- const { width } = useWindowSize();
- const isMobile = useMemo(() => width <= 640, [width]);
+ const { isMobile } = useWindowSize();
function handleClick(event: MouseEvent): void {
switch (event.currentTarget.name) {
@@ -142,3 +142,7 @@ export default function CreateUser() {
);
}
+
+CreateUser.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
diff --git a/frontend/src/pages/create_user_3.tsx b/frontend/src/pages/create_user_3.tsx
index 63d357aa..3cc5a8a2 100644
--- a/frontend/src/pages/create_user_3.tsx
+++ b/frontend/src/pages/create_user_3.tsx
@@ -1,8 +1,9 @@
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/router";
-import { useMemo } from "react";
+import { ReactElement } from "react";
import { FieldValues, SubmitHandler } from "react-hook-form";
+import Landing from "@/components/Landing";
import { useWindowSize } from "@/hooks/useWindowSize";
import { cn } from "@/lib/utils";
@@ -13,8 +14,7 @@ export default function CreateUser() {
console.log(data);
void router.push("/create_user_2");
};
- const { width } = useWindowSize();
- const isMobile = useMemo(() => width <= 640, [width]);
+ const { isMobile } = useWindowSize();
return (
@@ -76,3 +76,7 @@ export default function CreateUser() {
);
}
+
+CreateUser.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
diff --git a/frontend/src/pages/home.tsx b/frontend/src/pages/home.tsx
index 663dde56..64bac628 100644
--- a/frontend/src/pages/home.tsx
+++ b/frontend/src/pages/home.tsx
@@ -1,7 +1,5 @@
+import StudentsTable from "../components/StudentsTable/StudentsTable";
+
export default function Home() {
- return (
-
- PIA Home Page!
-
- );
+ return ;
}
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 8dda2853..7f6044ba 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,5 +1,13 @@
+import { ReactElement } from "react";
+
import Login from "./login";
-export default function Home() {
+import Landing from "@/components/Landing";
+
+export default function MyApp() {
return ;
}
+
+MyApp.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index 2dbf89b6..2f8e2ce4 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -1,7 +1,8 @@
import Image from "next/image";
-import { useMemo } from "react";
+import { ReactElement } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
+import Landing from "@/components/Landing";
import { Textfield } from "@/components/Textfield";
import { Button } from "@/components/ui/button";
import { useWindowSize } from "@/hooks/useWindowSize";
@@ -14,9 +15,8 @@ export default function Login() {
const onSubmit: SubmitHandler = (data) => {
console.log(data);
};
- const { width } = useWindowSize();
- const isMobile = useMemo(() => width <= 640, [width]);
- const isTablet = useMemo(() => width <= 1300, [width]);
+ const { isMobile, isTablet } = useWindowSize();
+
return (
@@ -114,3 +114,7 @@ export default function Login() {
);
}
+
+Login.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 7fc79b1c..0445ce21 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -128,6 +128,9 @@ module.exports = {
pia_neutral_gray: {
DEFAULT: "#D8D8D8",
},
+ pia_primary_white: {
+ DEFAULT: "#FFF",
+ },
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",