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

Edit Student Route Connected with Frontend #61

Closed
wants to merge 12 commits into from
Closed
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
3 changes: 2 additions & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ jobs:
run: |
cd frontend
touch .env
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env
echo APP_FIREBASE_CONFIG=${{ secrets.APP_FIREBASE_CONFIG }} >> .env
- name: Create Backend .env
run: |
cd backend
touch .env
echo MONGO_URI=${{ secrets.MONGO_URI }} >> .env
echo SERVICE_ACCOUNT_KEY=${{ secrets.SERVICE_ACCOUNT_KEY }} >> .env
echo APP_PORT=${{ secrets.APP_PORT }} >> .env
echo APP_FIREBASE_CONFIG=${{ secrets.APP_FIREBASE_CONFIG }} >> .env
- name: Build Frontend
run: cd frontend && npm ci && npm run build
- name: Build Backend
Expand Down
54 changes: 52 additions & 2 deletions backend/src/controllers/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,35 @@ export type typedModel = {
birthday: string;
intakeDate: string;
tourDate: string;
prog1: string[];
prog2: string[];
regularPrograms: string[];
varyingPrograms: string[];
dietary: string[];
otherString: string;
};

type Contact = {
lastName: string;
firstName: string;
email: string;
phoneNumber: string;
};

type Student = {
_id: string;
student: Contact;
emergency: Contact;
serviceCoordinator: Contact;
location: string;
medication?: string;
birthday: Date;
intakeDate: Date;
tourDate: Date;
regularPrograms: string[];
varyingPrograms: string[];
dietary: string[];
otherString?: string;
};

export const createStudent: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);
Expand All @@ -45,6 +68,33 @@ export const createStudent: RequestHandler = async (req, res, next) => {
}
};

export const editStudent: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

const studentId = req.params.id;
const studentData = req.body as Student;

if (studentId !== studentData._id) {
return res.status(400).json({ message: "Invalid student ID" });
}

const editedStudent = await StudentModel.findOneAndUpdate({ _id: studentId }, studentData, {
new: true,
});

if (!editedStudent) {
return res.status(404).json({ message: "No object in database with provided ID" });
}

res.status(200).json(editedStudent);
} catch (error) {
next(error);
}
};

export const getAllStudents: RequestHandler = async (_, res, next) => {
try {
const students = await StudentModel.find();
Expand Down
4 changes: 2 additions & 2 deletions backend/src/models/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const studentSchema = new Schema({

//For now, chose to express these as a list of strings. Will probably be replaced with
//program subdocs in the future once they have been defined
prog1: { type: [String], required: true },
prog2: { type: [String], default: "" },
regularPrograms: { type: [String], required: true },
varyingPrograms: { type: [String], required: true },

//Will contain list of all dietary restrictions
dietary: { type: [String] },
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routes/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import * as StudentValidator from "../validators/student";

const router = express.Router();

router.post("/", StudentValidator.createStudent, StudentController.createStudent);

router.post("/create", StudentValidator.createStudent, StudentController.createStudent);
router.put("/edit/:id", StudentValidator.editStudent, StudentController.editStudent);
router.get("/all", StudentController.getAllStudents);

export default router;
50 changes: 33 additions & 17 deletions backend/src/validators/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
*/

import { body } from "express-validator";
import mongoose from "mongoose";

//designed these to use the globstar operator from express-validator to more cleanly

const makeIdValidator = () =>
body("**._id")
.exists()
.withMessage("ID is required")
.bail()
.custom((value: string) => mongoose.Types.ObjectId.isValid(value))
.withMessage("Mongo ID format is invalid");

//validate contacts
const makeLastNamesValidator = () =>
body("**.lastName")
Expand Down Expand Up @@ -43,16 +53,11 @@ const makeEmailsValidator = () =>
const makePhoneNumbersValidator = () =>
body("**.phoneNumber")
.trim()
.exists()
.exists({ checkFalsy: true })
.withMessage("Phone number required")
.bail()
.isNumeric()
.withMessage("Field must have a valid number")
.isLength({ min: 10, max: 11 })
.withMessage("Phone number has an incorrect length")
.bail()
.notEmpty()
.withMessage("Phone number required");
.matches(/^(?:\d{10}|\d{3}-\d{3}-\d{4})$/)
.withMessage("Phone number must be in the format 1234567890 or 123-123-1234");

//validate location
const makeLocationValidator = () =>
Expand Down Expand Up @@ -85,6 +90,7 @@ const makeBirthdayValidator = () =>
.withMessage("Birthday field required")
.bail()
.isISO8601()
.toDate()
.withMessage("Birthday string must be a valid date-time string");

//intake date
Expand All @@ -94,6 +100,7 @@ const makeIntakeDateValidator = () =>
.withMessage("Intake Date field required")
.bail()
.isISO8601()
.toDate()
.withMessage("Intake Date string must be a valid date-time string");

//tour date
Expand All @@ -103,20 +110,27 @@ const makeTourDateValidator = () =>
.withMessage("Tour Date field required")
.bail()
.isISO8601()
.toDate()
.withMessage("Tour Date string must be a valid date-time string");

//prog1 --placeholder, will later validate for a program objectid
const makeProg1Validator = () =>
body("prog1")
const makeRegularProgramsValidator = () =>
body("regularPrograms")
.exists()
.withMessage("Program 1 field required")
.withMessage("Regular Programs field required")
.bail()
.isString()
.withMessage("Program 1 must be a string");
.isArray({ min: 1 })
.withMessage("Regular Programs must be a non-empty array")
.bail();

//prog2
const makeProg2Validator = () =>
body("prog2").optional().isString().withMessage("Program 2 must be a string");
const makeVaryingProgramsValidator = () =>
body("varyingPrograms")
.exists()
.withMessage("Varying Programs field required")
.bail()
.isArray({ min: 1 })
.withMessage("Varying Programs must be a non-empty array");

//dietary
//validates entire array
Expand Down Expand Up @@ -146,9 +160,11 @@ export const createStudent = [
makeBirthdayValidator(),
makeIntakeDateValidator(),
makeTourDateValidator(),
makeProg1Validator(),
makeProg2Validator(),
makeRegularProgramsValidator(),
makeVaryingProgramsValidator(),
makeDietaryArrayValidator(),
makeDietaryItemsValidator(),
makeDietaryOtherValidator(),
];

export const editStudent = [...createStudent, makeIdValidator()];
11 changes: 6 additions & 5 deletions frontend/src/api/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Custom type definition for the HTTP methods handled by this module.
*/
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;

/**
* Throws an error if the status code of the HTTP response indicates an error. If an HTTP error was
Expand Down Expand Up @@ -68,7 +69,7 @@ async function fetchRequest(
* @returns A `Response` object returned by `fetch()`
*/
export async function GET(url: string, headers: Record<string, string> = {}): Promise<Response> {
return await fetchRequest("GET", url, undefined, headers);
return await fetchRequest("GET", API_BASE_URL + url, undefined, headers);
}

/**
Expand All @@ -84,7 +85,7 @@ export async function POST(
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("POST", url, body, headers);
return await fetchRequest("POST", API_BASE_URL + url, body, headers);
}

/**
Expand All @@ -100,7 +101,7 @@ export async function PUT(
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("PUT", url, body, headers);
return await fetchRequest("PUT", API_BASE_URL + url, body, headers);
}

/**
Expand All @@ -116,7 +117,7 @@ export async function PATCH(
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("PATCH", url, body, headers);
return await fetchRequest("PATCH", API_BASE_URL + url, body, headers);
}

/**
Expand All @@ -132,7 +133,7 @@ export async function DELETE(
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("DELETE", url, body, headers);
return await fetchRequest("DELETE", API_BASE_URL + url, body, headers);
}

/**
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/api/students.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { GET, POST, PUT, handleAPIError } from "../api/requests";
import { Contact, StudentData } from "../components/StudentForm/types";

import type { APIResult } from "../api/requests";

export type CreateStudentRequest = StudentData;

export type Student = {
_id: string;
student: Contact;
emergency: Contact;
serviceCoordinator: Contact;
location: string;
medication?: string;
birthday: Date;
intakeDate: Date;
tourDate: Date;
regularPrograms: string[];
varyingPrograms: string[];
dietary: string[];
otherString?: string;
};

export async function createStudent(student: CreateStudentRequest): Promise<APIResult<Student>> {
try {
const response = await POST("/student/create", student);
const json = (await response.json()) as Student;
console.log({ json });
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}

export async function editStudent(student: Student): Promise<APIResult<Student>> {
try {
const response = await PUT(`/student/edit/${student._id}`, student);
const json = (await response.json()) as Student;
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}

export async function getAllStudents(): Promise<APIResult<[Student]>> {
try {
const response = await GET("/student/all");
const json = (await response.json()) as [Student];
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
3 changes: 2 additions & 1 deletion frontend/src/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use client";
import { FieldValues, Path, UseFormRegister } from "react-hook-form";

import { cn } from "../lib/utils";
Expand All @@ -22,6 +21,8 @@ export function Checkbox<T extends FieldValues>({
defaultValue,
defaultOtherValue,
}: CheckboxProps<T>) {
defaultValue = defaultValue?.map((item) => item.toLowerCase());

return (
<div className={cn("sm:min-w-2/5 min-w-4/5 grid gap-x-5 gap-y-5 sm:grid-cols-3", className)}>
{options.map((item, index) => {
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/StudentForm/ContactInfo.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -13,7 +14,7 @@ type ContactInfoProps = {
register: UseFormRegister<StudentFormData>;
classname?: string;
type: "add" | "edit";
data: StudentData | null;
data: Student | null;
};

type FieldProps = {
Expand Down Expand Up @@ -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(" ");
};
Expand Down
Loading
Loading