From 9dc911bc327a98edd19f1855d45afd689ae3f24d Mon Sep 17 00:00:00 2001
From: Yoto Kim
Date: Mon, 5 Feb 2024 12:26:10 -0800
Subject: [PATCH 01/19] Added title and description, required fields, and
overall more styling
---
frontend/src/api/VSRs.ts | 2 +-
frontend/src/app/vsr/page.module.css | 64 ++++++
frontend/src/app/vsr/page.tsx | 194 +++++++++---------
frontend/src/components/Dropdown.module.css | 28 ++-
frontend/src/components/Dropdown.tsx | 34 ++-
frontend/src/components/HeaderBar.module.css | 14 ++
frontend/src/components/HeaderBar.tsx | 12 ++
.../src/components/MultipleChoice.module.css | 31 ++-
frontend/src/components/MultipleChoice.tsx | 33 +--
frontend/src/components/TextField.module.css | 9 +-
frontend/src/components/TextField.tsx | 33 ++-
11 files changed, 312 insertions(+), 142 deletions(-)
create mode 100644 frontend/src/app/vsr/page.module.css
create mode 100644 frontend/src/components/HeaderBar.module.css
create mode 100644 frontend/src/components/HeaderBar.tsx
diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts
index f7d850d..3222d05 100644
--- a/frontend/src/api/VSRs.ts
+++ b/frontend/src/api/VSRs.ts
@@ -1,4 +1,4 @@
-import { APIResult, handleAPIError, post } from "../src/api/requests";
+import { APIResult, handleAPIError, post } from "@/api/requests";
/*
diff --git a/frontend/src/app/vsr/page.module.css b/frontend/src/app/vsr/page.module.css
new file mode 100644
index 0000000..42398a4
--- /dev/null
+++ b/frontend/src/app/vsr/page.module.css
@@ -0,0 +1,64 @@
+.main {
+ padding: 64px;
+ background-color: #d8d8d8;
+ display: flex;
+ flex-direction: column;
+ color: var(--Accent-Blue-1, #102d5f);
+ text-align: left;
+ /* Desktop/H1 */
+ font-family: Lora;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: normal;
+}
+
+.description {
+ color: #000;
+ /* Desktop/Body 1 */
+ font-family: "Open Sans";
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ padding-top: 32px;
+ padding-bottom: 32px;
+}
+
+.footer {
+ color: #000;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.formContainer {
+ display: flex;
+ background-color: white;
+ border-radius: 20px;
+ padding: 64px;
+ gap: 10px;
+ align-items: flex-start;
+ width: 100%;
+ height: 100%;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ align-self: stretch;
+}
+
+.formRow {
+ display: flex;
+ flex-direction: row;
+ gap: 32px;
+ align-items: center;
+ padding-top: 32px;
+ padding-bottom: 32px;
+}
+
+.asterisk {
+ color: var(--Secondary-2, #be2d46);
+}
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index 270e2bc..3f619e0 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -1,9 +1,11 @@
"use client";
import React from "react";
+import styles from "src/app/vsr/page.module.css";
import { useForm, Controller, SubmitHandler } from "react-hook-form";
import TextField from "@/components/TextField";
import MultipleChoice from "@/components/MultipleChoice";
import Dropdown from "@/components/Dropdown";
+import HeaderBar from "@/components/HeaderBar";
import * as validators from "@/util/validateResponses";
import { createVSR, CreateVSRRequest } from "@/api/VSRs";
@@ -12,40 +14,9 @@ interface IFormInput {
date: string;
marital_status: string;
gender: string;
+ age: number;
}
-// const handleSubmit = () => {
-// // first, do any validation that we can on the frontend
-// //todo
-
-// let createVSRRequest: CreateVSRRequest = {
-// name: "",
-// date: "",
-// gender: "",
-// age: 0,
-// maritalStatus: "",
-// ethnicity: "",
-// employmentStatus: "",
-// incomeLevel: "",
-// sizeOfHome: "",
-// };
-
-// createVSR(createVSRRequest).then((result) => {
-// if (result.success) {
-// } else {
-// // You should always clearly inform the user when something goes wrong.
-// // In this case, we're just doing an `alert()` for brevity, but you'd
-// // generally want to show some kind of error state or notification
-// // within your UI. If the problem is with the user's input, then use
-// // the error states of your smaller components (like the `TextField`s).
-// // If the problem is something we don't really control, such as network
-// // issues or an unexpected exception on the server side, then use a
-// // banner, modal, popup, or similar.
-// alert(result.error);
-// }
-// });
-// };
-
const VeteranServiceRequest: React.FC = () => {
const {
register,
@@ -65,7 +36,7 @@ const VeteranServiceRequest: React.FC = () => {
name: data.name,
date: data.date,
gender: data.gender,
- age: 42,
+ age: data.age,
maritalStatus: data.marital_status,
ethnicity: "PLACEHOLDER", // You'll need to add fields for these if they are required
employmentStatus: "PLACEHOLDER",
@@ -88,25 +59,74 @@ const VeteranServiceRequest: React.FC = () => {
};
return (
-
-
Veteran Service Request Form Page 1
-
-
diff --git a/frontend/src/components/PageNumber.module.css b/frontend/src/components/PageNumber.module.css
new file mode 100644
index 0000000..6424c4c
--- /dev/null
+++ b/frontend/src/components/PageNumber.module.css
@@ -0,0 +1,10 @@
+.pageNumber{
+ color: var(--Primary-Background-Dark, #232220);
+ text-align: center;
+ /* Desktop/Body 2 */
+ font-family: "Open Sans";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+}
\ No newline at end of file
diff --git a/frontend/src/components/PageNumber.tsx b/frontend/src/components/PageNumber.tsx
new file mode 100644
index 0000000..e009ef9
--- /dev/null
+++ b/frontend/src/components/PageNumber.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import styles from "src/components/PageNumber.module.css";
+
+export interface PageNumberProps {
+ pageNum: number;
+}
+
+const PageNumber = ({ pageNum }: PageNumberProps) => {
+ return (
+
+ );
+};
+
+export default PageNumber;
From f9a7374541f157b54a21f297c32d550e7d58cb37 Mon Sep 17 00:00:00 2001
From: 2s2e
Date: Thu, 15 Feb 2024 18:05:15 -0800
Subject: [PATCH 14/19] added girls form
---
frontend/src/app/vsr/page.tsx | 61 ++++++++++++++++++++++++++++++++++-
1 file changed, 60 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index c595748..685254c 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -99,7 +99,9 @@ const VeteranServiceRequest: React.FC = () => {
age: data.age,
maritalStatus: data.marital_status,
spouseName: data.spouse,
- agesOfBoys: data.ages_of_boys.slice(0, data.num_boys),
+ agesOfBoys: data.ages_of_boys === undefined ? [] : data.ages_of_boys.slice(0, data.num_boys),
+ agesOfGirls:
+ data.ages_of_girls === undefined ? [] : data.ages_of_girls.slice(0, data.num_girls),
ethnicity: finalEthnicity,
employmentStatus: data.employment_status,
incomeLevel: data.income_level,
@@ -282,6 +284,63 @@ const VeteranServiceRequest: React.FC = () => {
))}
+
+
+
+ {
+ //Need to fix (make OnChange function?)? Issue: If I input 5 then 3 for num boys,
+ //the data will make an array with 3 values then two null like:
+ //[2, 4, 6, null, null] rather than just [2, 4, 6]
+ // Convert the input value to a number and check if it exceeds 20
+ const intValue = parseInt(value);
+ if (intValue > 20) {
+ return 20; // Return 20 if the input value exceeds 20
+ }
+ return intValue > 0 ? intValue : 0; // Ensure negative values are not accepted, return 0 as a fallback
+ },
+ })}
+ {...register("num_girls", { required: "Number of girls is required" })}
+ onChange={(e) => {
+ console.log("Errors and watch", errors, watch());
+ //if number is greater than 20, set it to 20, and set form value to 20
+ if (parseInt(e.target.value) > 20) {
+ setNumGirls(20);
+ } else {
+ setNumGirls(parseInt(e.target.value));
+ }
+ }}
+ required={true}
+ error={!!errors.num_girls}
+ helperText={errors.num_girls?.message}
+ />
+
+
+
+
+ {Array.from({ length: numGirls }, (_, index) => (
+
+
+
+ ))}
+
+
From 2d491882c91f8929da16b6ada973cfdd9dab44b6 Mon Sep 17 00:00:00 2001
From: Yoto Kim
Date: Thu, 15 Feb 2024 20:26:09 -0800
Subject: [PATCH 15/19] changes with validation
---
frontend/src/app/vsr/page.module.css | 3 ---
frontend/src/app/vsr/page.tsx | 5 -----
frontend/src/util/validateResponses.ts | 10 ----------
3 files changed, 18 deletions(-)
diff --git a/frontend/src/app/vsr/page.module.css b/frontend/src/app/vsr/page.module.css
index 4d587cc..68cb5df 100644
--- a/frontend/src/app/vsr/page.module.css
+++ b/frontend/src/app/vsr/page.module.css
@@ -108,6 +108,3 @@
height: 56px;
}
-.bottom {
-
-}
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index 80994ce..2fb3b18 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -35,7 +35,6 @@ const VeteranServiceRequest: React.FC = () => {
control,
formState: { errors },
watch,
- setValue,
} = useForm();
const [selectedEthnicity, setSelectedEthnicity] = useState("");
@@ -86,8 +85,6 @@ const VeteranServiceRequest: React.FC = () => {
"Prefer not to say",
];
- // Determine if the "Other" textbox should be shown
- const showOtherTextbox = selectedEthnicity?.length === 0 || selectedEthnicity === undefined;
const onSubmit: SubmitHandler = async (data) => {
console.log(data);
@@ -387,7 +384,6 @@ const VeteranServiceRequest: React.FC = () => {
-
Submit
@@ -396,7 +392,6 @@ const VeteranServiceRequest: React.FC = () => {
-
diff --git a/frontend/src/util/validateResponses.ts b/frontend/src/util/validateResponses.ts
index 7f8b668..9d66c1f 100644
--- a/frontend/src/util/validateResponses.ts
+++ b/frontend/src/util/validateResponses.ts
@@ -2,16 +2,6 @@ export function isnum(num: string): boolean {
return /^\d+$/.test(num);
}
-export function validateDate(date: string): string {
- //2 digit number / 2 digit number / 4 digit number
- const dateRegex = /^\d{4}\-\d{2}\-\d{2}$/;
- if (dateRegex.test(date)) {
- return "Success";
- } else {
- return "Date is not in the correct format";
- }
-}
-
export function validateAge(age: string): string {
if (!isnum(age)) {
return "Age is not a number";
From 6fc45e9f15dbde5e922aa6759bacb387df25e4f8 Mon Sep 17 00:00:00 2001
From: 2s2e
Date: Tue, 20 Feb 2024 12:51:34 -0800
Subject: [PATCH 16/19] Yet even more lint fixes
---
frontend/src/app/vsr/page.module.css | 1 -
frontend/src/app/vsr/page.tsx | 29 ++++++++-----------
frontend/src/components/Dropdown.tsx | 2 +-
frontend/src/components/MultipleChoice.tsx | 1 -
frontend/src/components/PageNumber.module.css | 20 ++++++-------
frontend/src/util/validateResponses.ts | 2 +-
6 files changed, 24 insertions(+), 31 deletions(-)
diff --git a/frontend/src/app/vsr/page.module.css b/frontend/src/app/vsr/page.module.css
index 4d587cc..fd55c68 100644
--- a/frontend/src/app/vsr/page.module.css
+++ b/frontend/src/app/vsr/page.module.css
@@ -109,5 +109,4 @@
}
.bottom {
-
}
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index cea2467..6683246 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -6,8 +6,7 @@ import TextField from "@/components/TextField";
import MultipleChoice from "@/components/MultipleChoice";
import Dropdown from "@/components/Dropdown";
import HeaderBar from "@/components/HeaderBar";
-import PageNumber from "@/components/PageNumber"
-import * as validators from "@/util/validateResponses";
+import PageNumber from "@/components/PageNumber";
import { createVSR, CreateVSRRequest } from "@/api/VSRs";
interface IFormInput {
@@ -35,7 +34,6 @@ const VeteranServiceRequest: React.FC = () => {
control,
formState: { errors },
watch,
- setValue,
} = useForm();
const [selectedEthnicity, setSelectedEthnicity] = useState("");
@@ -87,7 +85,7 @@ const VeteranServiceRequest: React.FC = () => {
];
// Determine if the "Other" textbox should be shown
- const showOtherTextbox = selectedEthnicity?.length === 0 || selectedEthnicity === undefined;
+ //const showOtherTextbox = selectedEthnicity?.length === 0 || selectedEthnicity === undefined;
const onSubmit: SubmitHandler = async (data) => {
console.log(data);
@@ -100,9 +98,8 @@ const VeteranServiceRequest: React.FC = () => {
age: data.age,
maritalStatus: data.marital_status,
spouseName: data.spouse,
- agesOfBoys: data.ages_of_boys === undefined ? [] : data.ages_of_boys.slice(0, data.num_boys),
- agesOfGirls:
- data.ages_of_girls === undefined ? [] : data.ages_of_girls.slice(0, data.num_girls),
+ agesOfBoys: data.ages_of_boys.slice(0, numBoys),
+ agesOfGirls: data.ages_of_girls.slice(0, numGirls),
ethnicity: finalEthnicity,
employmentStatus: data.employment_status,
incomeLevel: data.income_level,
@@ -135,12 +132,12 @@ const VeteranServiceRequest: React.FC = () => {
warehouse.
- Let us know your specific needs, and we'll provide the best assistance possible. Expect
- a response within 48 business hours; remember to check your junk mail if needed.
+ Let us know your specific needs, and we'll provide the best assistance possible.
+ Expect a response within 48 business hours; remember to check your junk mail if needed.
- If you're a Veteran or Active Military Reservist in search of our services, simply fill
- out and submit the form below.
+ If you're a Veteran or Active Military Reservist in search of our services, simply
+ fill out and submit the form below.
@@ -448,13 +445,11 @@ const VeteranServiceRequest: React.FC = () => {
-
- Submit
-
+
+ Submit
+
-
+
diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx
index c845bb8..176ca53 100644
--- a/frontend/src/components/Dropdown.tsx
+++ b/frontend/src/components/Dropdown.tsx
@@ -1,6 +1,6 @@
import React from "react";
import styles from "src/components/Dropdown.module.css";
-import { FormControl, InputLabel, Select, MenuItem, SelectChangeEvent } from "@mui/material";
+import { FormControl, Select, MenuItem, SelectChangeEvent } from "@mui/material";
export interface DropDownProps {
label: string;
diff --git a/frontend/src/components/MultipleChoice.tsx b/frontend/src/components/MultipleChoice.tsx
index 879244c..0d48848 100644
--- a/frontend/src/components/MultipleChoice.tsx
+++ b/frontend/src/components/MultipleChoice.tsx
@@ -1,4 +1,3 @@
-import React, { useState } from "react";
import Chip from "@mui/material/Chip";
import styles from "@/components/MultipleChoice.module.css";
diff --git a/frontend/src/components/PageNumber.module.css b/frontend/src/components/PageNumber.module.css
index 6424c4c..baa68f8 100644
--- a/frontend/src/components/PageNumber.module.css
+++ b/frontend/src/components/PageNumber.module.css
@@ -1,10 +1,10 @@
-.pageNumber{
- color: var(--Primary-Background-Dark, #232220);
- text-align: center;
- /* Desktop/Body 2 */
- font-family: "Open Sans";
- font-size: 16px;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
-}
\ No newline at end of file
+.pageNumber {
+ color: var(--Primary-Background-Dark, #232220);
+ text-align: center;
+ /* Desktop/Body 2 */
+ font-family: "Open Sans";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+}
diff --git a/frontend/src/util/validateResponses.ts b/frontend/src/util/validateResponses.ts
index 7f8b668..d849914 100644
--- a/frontend/src/util/validateResponses.ts
+++ b/frontend/src/util/validateResponses.ts
@@ -4,7 +4,7 @@ export function isnum(num: string): boolean {
export function validateDate(date: string): string {
//2 digit number / 2 digit number / 4 digit number
- const dateRegex = /^\d{4}\-\d{2}\-\d{2}$/;
+ const dateRegex = /^\d{4}\d{2}\d{2}$/;
if (dateRegex.test(date)) {
return "Success";
} else {
From 40f0fa07253bffb71eb15e0c0c3b9b165909c65b Mon Sep 17 00:00:00 2001
From: benjaminjohnson2204
Date: Thu, 22 Feb 2024 17:00:06 -0800
Subject: [PATCH 17/19] Fix CSS styles & clean up code
---
backend/src/controllers/vsr.ts | 8 +-
backend/src/routes/vsr.ts | 7 -
backend/src/validators/vsr.ts | 40 +-
frontend/__tests__/exampleTest.test.ts | 107 +----
frontend/public/logo.svg | 9 +
frontend/src/api/VSRs.ts | 15 -
frontend/src/app/vsr/page.module.css | 19 +-
frontend/src/app/vsr/page.tsx | 421 ++++++++----------
frontend/src/components/Dropdown.module.css | 7 +
frontend/src/components/Dropdown.tsx | 12 +-
frontend/src/components/HeaderBar.tsx | 3 +-
.../src/components/MultipleChoice.module.css | 3 +-
frontend/src/components/MultipleChoice.tsx | 4 +-
frontend/src/components/TextField.module.css | 7 +
frontend/src/components/TextField.tsx | 10 +-
frontend/src/util/validateResponses.ts | 30 --
16 files changed, 253 insertions(+), 449 deletions(-)
create mode 100644 frontend/public/logo.svg
delete mode 100644 frontend/src/util/validateResponses.ts
diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts
index f67eda8..838d5be 100644
--- a/backend/src/controllers/vsr.ts
+++ b/backend/src/controllers/vsr.ts
@@ -9,7 +9,6 @@ export const createVSR: RequestHandler = async (req, res, next) => {
const errors = validationResult(req);
const {
name,
- date,
gender,
age,
maritalStatus,
@@ -22,12 +21,13 @@ export const createVSR: RequestHandler = async (req, res, next) => {
sizeOfHome,
} = req.body;
- console.log(req.body);
-
try {
// if there are errors, then this function throws an exception
validationErrorParser(errors);
+ // Get the current date as a timestamp for when VSR was submitted
+ const date = new Date();
+
const vsr = await VSRModel.create({
name,
date,
@@ -44,7 +44,7 @@ export const createVSR: RequestHandler = async (req, res, next) => {
});
// 201 means a new resource has been created successfully
- // the newly created task is sent back to the user
+ // the newly created VSR is sent back to the user
res.status(201).json(vsr);
} catch (error) {
next(error);
diff --git a/backend/src/routes/vsr.ts b/backend/src/routes/vsr.ts
index 0f584d3..4a81cd4 100644
--- a/backend/src/routes/vsr.ts
+++ b/backend/src/routes/vsr.ts
@@ -4,13 +4,6 @@ import * as VSRValidator from "src/validators/vsr";
const router = express.Router();
-/**
- * TaskValidator.createTask serves as middleware for this route. This means
- * that instead of immediately serving up the route when the request is made,
- * Express firsts passes the request to TaskValidator.createTask.
- * TaskValidator.createTask processes the request and determines whether the
- * request should be sent through or an error should be thrown.
- */
router.post("/", VSRValidator.createVSR, VSRController.createVSR);
export default router;
diff --git a/backend/src/validators/vsr.ts b/backend/src/validators/vsr.ts
index 48b46d3..c296e33 100644
--- a/backend/src/validators/vsr.ts
+++ b/backend/src/validators/vsr.ts
@@ -1,20 +1,5 @@
import { body } from "express-validator";
-// name: {type: String, required: true },
-// date: {type: Date, required: true},
-// gender: {type: String, require: true},
-// age: {type: Number, require: true},
-// martialStatus: {type: String, required: true },
-// spouseName: {type: String},
-// numOfBoys: {type: Number},
-// numOfGirls: {type: Number},
-// agesOfBoys: {type: [Number] },
-// agesOfGirls: {type: [Number] },
-// ethnicity: {type: String, require: true},
-// employmentStatus: {type: String, require: true},
-// incomeLevel: {type: String, require: true},
-// sizeOfHome: {type: String, require: true}
-
const makeNameValidator = () =>
body("name")
.exists({ checkFalsy: true })
@@ -22,13 +7,6 @@ const makeNameValidator = () =>
.isString()
.withMessage("Name must be a string");
-const makeDateValidator = () =>
- body("date")
- .exists({ checkFalsy: true })
- .withMessage("Date is required")
- .isISO8601()
- .withMessage("Date must be in ISO 8601 format");
-
const makeGenderValidator = () =>
body("gender")
.exists({ checkFalsy: true })
@@ -56,14 +34,17 @@ const makeSpouseNameValidator = () =>
.isString()
.withMessage("Spouse Name must be a string");
-const makeNumOfBoysValidator = () =>
- body("numOfBoys")
- .optional({ checkFalsy: true })
- .isInt({ min: 0 })
- .withMessage("Number of Boys must be a positive integer");
+const makeAgesOfBoysValidator = () =>
+ body("agesOfBoys")
+ .exists({ checkFalsy: true })
+ .isArray()
+ .withMessage("Ages of Boys must be an array of numbers")
+ .custom((ages: number[]) => ages.every((age) => Number.isInteger(age) && age >= 0))
+ .withMessage("Each age in Ages of Boys must be a positive integer");
+
const makeAgesOfGirlsValidator = () =>
body("agesOfGirls")
- .optional({ checkFalsy: true })
+ .exists({ checkFalsy: true })
.isArray()
.withMessage("Ages of Girls must be an array of numbers")
.custom((ages: number[]) => ages.every((age) => Number.isInteger(age) && age >= 0))
@@ -99,12 +80,11 @@ const makeSizeOfHomeValidator = () =>
export const createVSR = [
makeNameValidator(),
- makeDateValidator(),
makeGenderValidator(),
makeAgeValidator(),
makeMaritalStatusValidator(),
makeSpouseNameValidator(),
- makeNumOfBoysValidator(),
+ makeAgesOfBoysValidator(),
makeAgesOfGirlsValidator(),
makeEthnicityValidator(),
makeEmploymentStatusValidator(),
diff --git a/frontend/__tests__/exampleTest.test.ts b/frontend/__tests__/exampleTest.test.ts
index 71edd7a..3f99694 100644
--- a/frontend/__tests__/exampleTest.test.ts
+++ b/frontend/__tests__/exampleTest.test.ts
@@ -1,108 +1,7 @@
import "@testing-library/jest-dom";
-import {
- isnum,
- validateAge,
- validateEthnicityOther,
- validateSpouseName,
-} from "../src/util/validateResponses";
-
-describe("Frontend Validator Tests", () => {
- describe("IsNum", () => {
- it("Correctly classifies '0' as a number", () => {
- expect(isnum("0")).toEqual(true);
- });
-
- it("Correctly classifies various numbers as numbers", () => {
- expect(isnum("1")).toEqual(true);
- expect(isnum("2")).toEqual(true);
- expect(isnum("3")).toEqual(true);
- expect(isnum("23948")).toEqual(true);
- expect(isnum("56")).toEqual(true);
- });
-
- it("Correctly classifies various non-numbers as non-numbers", () => {
- expect(isnum("")).toEqual(false);
- expect(isnum(" ")).toEqual(false);
- expect(isnum("abc")).toEqual(false);
- expect(isnum("a")).toEqual(false);
- expect(isnum("32 049")).toEqual(false);
- expect(isnum("32,049")).toEqual(false);
- expect(isnum("32.049")).toEqual(false);
- expect(isnum("32-049")).toEqual(false);
- expect(isnum("32/049")).toEqual(false);
- expect(isnum("-5")).toEqual(false);
- });
- });
-
- describe("ValidateAge", () => {
- it("Correctly classifies valid ages", () => {
- expect(validateAge("0")).toEqual("Success");
- expect(validateAge("1")).toEqual("Success");
- expect(validateAge("2")).toEqual("Success");
- expect(validateAge("50")).toEqual("Success");
- });
-
- it("Correctly classifies invalid ages", () => {
- expect(validateAge("")).toEqual("Age is not a number");
- expect(validateAge("five years old")).toEqual("Age is not a number");
- expect(validateAge("abc")).toEqual("Age is not a number");
- expect(validateAge("a")).toEqual("Age is not a number");
- expect(validateAge("-5")).toEqual("Age is not a number");
- });
- });
-
- describe("ValidateSpouseName", () => {
- it("Correctly classifies married and has spouse name", () => {
- expect(validateSpouseName("Married", "Bob")).toEqual("Success");
- expect(validateSpouseName("Married", "Bob Smith")).toEqual("Success");
- expect(validateSpouseName("Married", "Bob Smith Jr.")).toEqual("Success");
- });
-
- it("Correctly classifies married and has no spouse name", () => {
- expect(validateSpouseName("Married", "")).toEqual("Spouse name is required");
- });
-
- it("Correctly classifies single and has spouse name", () => {
- expect(validateSpouseName("Single", "Bob")).toEqual("Spouse name is not required");
- expect(validateSpouseName("Single", "Bob Smith")).toEqual("Spouse name is not required");
- expect(validateSpouseName("Single", "Bob Smith Jr.")).toEqual("Spouse name is not required");
- });
-
- it("Correctly classifies single and has no spouse name", () => {
- expect(validateSpouseName("Single", "")).toEqual("Success");
- });
-
- it("Correctly classifies is complicated", () => {
- expect(validateSpouseName("It's Complicated", "")).toEqual("Success");
- expect(validateSpouseName("It's Complicated", "Bob")).toEqual("Success");
- expect(validateSpouseName("It's Complicated", "Bob Smith")).toEqual("Success");
- });
- });
-
- describe("ValidateEthnicityOther", () => {
- it("Correctly classifies no ethnicity and no other", () => {
- expect(validateEthnicityOther("", "")).toEqual("Please fill out the other field");
- });
-
- it("Correctly classifies no ethnicity and other", () => {
- expect(validateEthnicityOther("", "Other")).toEqual("Success");
- expect(validateEthnicityOther("", "Polynesian")).toEqual("Success");
- });
-
- it("Correctly classifies has ethnicity and has other", () => {
- expect(validateEthnicityOther("Asian", "Other")).toEqual(
- "Please leave the other field empty",
- );
- expect(validateEthnicityOther("Asian", "Polynesian")).toEqual(
- "Please leave the other field empty",
- );
- });
-
- it("Correctly classifies ethnicity, no other", () => {
- expect(validateEthnicityOther("Asian", "")).toEqual("Success");
- expect(validateEthnicityOther("Hispanic", "")).toEqual("Success");
- expect(validateEthnicityOther("Not Given", "")).toEqual("Success");
- });
+describe("ExampleTests", () => {
+ it("Passes example test", () => {
+ expect(1 + 1).toEqual(2);
});
});
diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg
new file mode 100644
index 0000000..433e0ee
--- /dev/null
+++ b/frontend/public/logo.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts
index 3222d05..c3cd35e 100644
--- a/frontend/src/api/VSRs.ts
+++ b/frontend/src/api/VSRs.ts
@@ -1,19 +1,5 @@
import { APIResult, handleAPIError, post } from "@/api/requests";
-/*
-
- name: { type: String, required: true },
- date: { type: Date, required: true },
- gender: { type: String, require: true },
- age: { type: Number, require: true },
- maritalStatus: { type: String, required: true },
- spouseName: { type: String },
- agesOfBoys: { type: [Number] },
- agesOfGirls: { type: [Number] },
- ethnicity: { type: String, require: true },
- employmentStatus: { type: String, require: true },
- incomeLevel: { type: String, require: true },
- sizeOfHome: { type: String, require: true },*/
export interface VSRJson {
_id: string;
name: string;
@@ -48,7 +34,6 @@ export interface VSR {
export interface CreateVSRRequest {
name: string;
- date: string;
gender: string;
age: number;
maritalStatus: string;
diff --git a/frontend/src/app/vsr/page.module.css b/frontend/src/app/vsr/page.module.css
index e3f72ae..9b2f10e 100644
--- a/frontend/src/app/vsr/page.module.css
+++ b/frontend/src/app/vsr/page.module.css
@@ -1,12 +1,12 @@
.main {
padding: 64px;
padding-top: 128px;
- background-color: #d8d8d8;
+ background-color: #eceff3;
display: flex;
flex-direction: column;
color: var(--Accent-Blue-1, #102d5f);
text-align: left;
- gap: 64px;
+ gap: 32px;
/* Desktop/H1 */
font-family: Lora;
font-size: 20px;
@@ -47,6 +47,7 @@
background-color: white;
border-radius: 20px;
padding: 64px;
+ margin-top: 32px;
gap: 10px;
align-items: flex-start;
width: 100%;
@@ -74,14 +75,15 @@
display: flex;
flex-direction: row;
gap: 64px;
- align-items: center;
+ align-items: start;
}
.numChildren {
display: flex;
- flex-direction: row;
+ flex-direction: column;
gap: 20px;
align-items: center;
+ flex: 1;
}
.asterisk {
@@ -96,14 +98,21 @@
width: 306px;
height: 64px;
background-color: #102d5f;
+ color: white;
+ cursor: pointer;
font-family: Lora;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: normal;
+ border: none;
+ border-radius: 4px;
}
.longText {
flex: 1;
- height: 56px;
+}
+
+.childInputWrapper {
+ width: 100%;
}
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index ee19f78..49ae66e 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -11,7 +11,6 @@ import { createVSR, CreateVSRRequest } from "@/api/VSRs";
interface IFormInput {
name: string;
- date: string;
marital_status: string;
gender: string;
spouse: string;
@@ -36,16 +35,13 @@ const VeteranServiceRequest: React.FC = () => {
watch,
} = useForm();
const [selectedEthnicity, setSelectedEthnicity] = useState("");
-
const [otherEthnicity, setOtherEthnicity] = useState("");
- const [finalEthnicity, setFinalEthnicity] = useState("");
- const [numBoys, setNumBoys] = useState(0);
- const [numGirls, setNumGirls] = useState(0);
+ const numBoys = watch("num_boys");
+ const numGirls = watch("num_girls");
- console.log("selected", selectedEthnicity);
- const maritalOptions = ["Married", "Single", "Widow/Widower", "It's Complicated"];
- const genderOptions = ["", "Male", "Female", "Other"];
+ const maritalOptions = ["Married", "Single", "It's Complicated", "Widowed/Widower"];
+ const genderOptions = ["Male", "Female", "Other"];
const employmentOptions = [
"Employed",
"Unemployed",
@@ -53,7 +49,6 @@ const VeteranServiceRequest: React.FC = () => {
"Retired",
"In School",
"Unable to work",
- "Other",
];
const incomeOptions = [
@@ -85,19 +80,16 @@ const VeteranServiceRequest: React.FC = () => {
];
const onSubmit: SubmitHandler = async (data) => {
- console.log(data);
-
// Construct the request object
const createVSRRequest: CreateVSRRequest = {
name: data.name,
- date: new Date().toISOString().slice(0, 10),
gender: data.gender,
age: data.age,
maritalStatus: data.marital_status,
spouseName: data.spouse,
- agesOfBoys: data.ages_of_boys.slice(0, numBoys),
- agesOfGirls: data.ages_of_girls.slice(0, numGirls),
- ethnicity: finalEthnicity,
+ agesOfBoys: data.ages_of_boys?.slice(0, data.num_boys) ?? [],
+ agesOfGirls: data.ages_of_girls?.slice(0, data.num_girls) ?? [],
+ ethnicity: selectedEthnicity === "" ? otherEthnicity : selectedEthnicity,
employmentStatus: data.employment_status,
incomeLevel: data.income_level,
sizeOfHome: data.size_of_home,
@@ -107,16 +99,67 @@ const VeteranServiceRequest: React.FC = () => {
const response = await createVSR(createVSRRequest);
if (!response.success) {
+ // TODO: better way of displaying error
throw new Error(`HTTP error! status: ${response.error}`);
}
- const responseJson = await response.data;
- console.log(responseJson);
+ // TODO: better way of displaying successful submission (popup/modal)
+ alert("VSR submitted successfully!");
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
}
};
+ const renderChildInput = (gender: "boy" | "girl") => {
+ const numChildrenThisGender = gender === "boy" ? numBoys : numGirls;
+
+ return (
+ <>
+
+
+
+
+
+ {Array.from({ length: numChildrenThisGender }, (_, index) => (
+
+
+
+ ))}
+
+ >
+ );
+ };
+
return (
@@ -147,17 +190,21 @@ const VeteranServiceRequest: React.FC = () => {
Personal Information
-
-
console.log("Errors and watch", errors, watch())}
- required={true}
- error={!!errors.name}
- helperText={errors.name?.message}
- />
+
+
+
+
+
+ {/* Add an empty div here with flex: 1 to take up the right half of the row */}
+
@@ -176,6 +223,7 @@ const VeteranServiceRequest: React.FC = () => {
required={true}
error={!!errors.gender}
helperText={errors.gender?.message}
+ placeholder="Select your gender"
/>
)}
/>
@@ -186,258 +234,141 @@ const VeteranServiceRequest: React.FC = () => {
variant="outlined"
placeholder="Enter your age"
{...register("age", { required: "Age is required" })}
- onChange={(e) => console.log("Errors and watch", errors, watch())}
required={true}
error={!!errors.age}
helperText={errors.age?.message}
/>
+
-
-
(
- field.onChange(newValue)}
- required={true}
- error={!!errors.marital_status}
- helperText={errors.marital_status?.message}
- />
- )}
- />
+
+
(
+ field.onChange(newValue)}
+ required={true}
+ error={!!errors.marital_status}
+ helperText={errors.marital_status?.message}
+ />
+ )}
+ />
+
console.log("Errors and watch", errors, watch())}
- required={false}
+ required={["Married", "Widowed/Widower"].includes(watch().marital_status)}
error={!!errors.spouse}
helperText={errors.spouse?.message}
/>
+ {/* Add an empty div here with flex: 1 to take up the right half of the row */}
+
-
- {
- //Need to fix (make OnChange function?)? Issue: If I input 5 then 3 for num boys,
- //the data will make an array with 3 values then two null like:
- //[2, 4, 6, null, null] rather than just [2, 4, 6]
- // Convert the input value to a number and check if it exceeds 20
- const intValue = parseInt(value);
- if (intValue > 20) {
- return 20; // Return 20 if the input value exceeds 20
- }
- return intValue > 0 ? intValue : 0; // Ensure negative values are not accepted, return 0 as a fallback
- },
- })}
- {...register("num_boys", { required: "Number of boys is required" })}
- onChange={(e) => {
- console.log("Errors and watch", errors, watch());
- //if number is greater than 20, set it to 20, and set form value to 20
- if (parseInt(e.target.value) > 20) {
- setNumBoys(20);
- } else {
- setNumBoys(parseInt(e.target.value));
- }
- }}
- required={true}
- error={!!errors.num_boys}
- helperText={errors.num_boys?.message}
- />
-
-
-
- {Array.from({ length: numBoys }, (_, index) => (
-
-
-
- ))}
-
-
-
-
- {
- //Need to fix (make OnChange function?)? Issue: If I input 5 then 3 for num boys,
- //the data will make an array with 3 values then two null like:
- //[2, 4, 6, null, null] rather than just [2, 4, 6]
- // Convert the input value to a number and check if it exceeds 20
- const intValue = parseInt(value);
- if (intValue > 20) {
- return 20; // Return 20 if the input value exceeds 20
- }
- return intValue > 0 ? intValue : 0; // Ensure negative values are not accepted, return 0 as a fallback
- },
- })}
- {...register("num_girls", { required: "Number of girls is required" })}
- onChange={(e) => {
- console.log("Errors and watch", errors, watch());
- //if number is greater than 20, set it to 20, and set form value to 20
- if (parseInt(e.target.value) > 20) {
- setNumGirls(20);
- } else {
- setNumGirls(parseInt(e.target.value));
- }
- }}
- required={true}
- error={!!errors.num_girls}
- helperText={errors.num_girls?.message}
- />
-
-
-
-
- {Array.from({ length: numGirls }, (_, index) => (
-
-
-
- ))}
+ {renderChildInput("boy")}
+ {renderChildInput("girl")}
+
-
-
-
- (
+ (
+ <>
{
field.onChange(newValue);
- setOtherEthnicity("");
setSelectedEthnicity(newValue);
-
- if (newValue === "") {
- setFinalEthnicity(otherEthnicity);
- } else {
- setFinalEthnicity(newValue);
- }
}}
- required={false}
+ required={true}
error={!!errors.ethnicity}
helperText={errors.ethnicity?.message}
/>
- )}
- />
-
- {
- const value = e.target.value;
- setOtherEthnicity(value);
- if (selectedEthnicity === "") {
- setFinalEthnicity(value);
- } else {
- setFinalEthnicity(selectedEthnicity);
- }
- }}
- required={!selectedEthnicity || selectedEthnicity.length === 0}
- label={""}
- variant={"outlined"}
- />
-
- {errors.ethnicity && {errors.ethnicity.message}
}
+
+ {
+ const value = e.target.value;
+ field.onChange(value);
+ setOtherEthnicity(value);
+ }}
+ variant={"outlined"}
+ required={false}
+ />
+
+ >
+ )}
+ />
- (
- field.onChange(newValue)}
- required={true}
- error={!!errors.employment_status}
- helperText={errors.employment_status?.message}
- />
- )}
- />
+ (
+ field.onChange(newValue)}
+ required={true}
+ error={!!errors.employment_status}
+ helperText={errors.employment_status?.message}
+ />
+ )}
+ />
- (
- field.onChange(newValue)}
- required={true}
- error={!!errors.income_level}
- helperText={errors.income_level?.message}
- />
- )}
- />
+ (
+ field.onChange(newValue)}
+ required={true}
+ error={!!errors.income_level}
+ helperText={errors.income_level?.message}
+ />
+ )}
+ />
- (
- field.onChange(newValue)}
- required={true}
- error={!!errors.size_of_home}
- helperText={errors.size_of_home?.message}
- />
- )}
- />
-
+
(
+ field.onChange(newValue)}
+ required={true}
+ error={!!errors.size_of_home}
+ helperText={errors.size_of_home?.message}
+ />
+ )}
+ />
diff --git a/frontend/src/components/Dropdown.module.css b/frontend/src/components/Dropdown.module.css
index 6154523..dfd998b 100644
--- a/frontend/src/components/Dropdown.module.css
+++ b/frontend/src/components/Dropdown.module.css
@@ -38,3 +38,10 @@
color: var(--Secondary-2, #be2d46);
font-size: 12px;
}
+
+.placeholder {
+ font-size: 16px;
+ font-style: italic;
+ font-weight: 300;
+ color: var(--Dark-Gray, #484848);
+}
diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx
index 176ca53..9cf8841 100644
--- a/frontend/src/components/Dropdown.tsx
+++ b/frontend/src/components/Dropdown.tsx
@@ -10,6 +10,7 @@ export interface DropDownProps {
required: boolean;
error?: boolean;
helperText?: string;
+ placeholder?: string;
}
const Dropdown = ({
@@ -20,11 +21,12 @@ const Dropdown = ({
required,
error,
helperText,
+ placeholder,
}: DropDownProps) => {
return (
- {required && * }
+ {required ? * : null}
{label}
@@ -35,7 +37,13 @@ const Dropdown = ({
error={error}
displayEmpty
fullWidth={true}
+ renderValue={(value) =>
+ value === "" ? {placeholder}
: value
+ }
>
+
+ {placeholder}
+
{options.map((option) => (
{option}
@@ -43,7 +51,7 @@ const Dropdown = ({
))}
-
{helperText}
+ {helperText ?
{helperText}
: null}
);
};
diff --git a/frontend/src/components/HeaderBar.tsx b/frontend/src/components/HeaderBar.tsx
index c178f7d..843f93d 100644
--- a/frontend/src/components/HeaderBar.tsx
+++ b/frontend/src/components/HeaderBar.tsx
@@ -1,10 +1,11 @@
+import Image from "next/image";
import React from "react";
import styles from "src/components/HeaderBar.module.css";
const HeaderBar = () => {
return (
-
+
);
};
diff --git a/frontend/src/components/MultipleChoice.module.css b/frontend/src/components/MultipleChoice.module.css
index 588b1f3..b3a1f93 100644
--- a/frontend/src/components/MultipleChoice.module.css
+++ b/frontend/src/components/MultipleChoice.module.css
@@ -4,7 +4,7 @@
display: flex;
flex-direction: column;
align-items: flex-start;
- gap: 4px;
+ gap: 16px;
flex: 1 0 0;
font-size: 16px;
color: var(--Light-Gray, #818181);
@@ -40,6 +40,7 @@
display: flex;
flex-direction: row;
gap: 16px;
+ flex-wrap: wrap;
}
.chipUnselected {
diff --git a/frontend/src/components/MultipleChoice.tsx b/frontend/src/components/MultipleChoice.tsx
index 0d48848..bd62206 100644
--- a/frontend/src/components/MultipleChoice.tsx
+++ b/frontend/src/components/MultipleChoice.tsx
@@ -22,7 +22,7 @@ const MultipleChoice = ({
return (
- {required && * }
+ {required ? * : null}
{label}
@@ -44,7 +44,7 @@ const MultipleChoice = ({
/>
))}
-
{helperText}
+ {helperText ?
{helperText}
: null}
);
};
diff --git a/frontend/src/components/TextField.module.css b/frontend/src/components/TextField.module.css
index 84825f0..971ff18 100644
--- a/frontend/src/components/TextField.module.css
+++ b/frontend/src/components/TextField.module.css
@@ -32,3 +32,10 @@
color: var(--Secondary-2, #be2d46);
font-size: 12px;
}
+
+.input::placeholder {
+ font-size: 16px;
+ font-style: italic;
+ font-weight: 300;
+ color: var(--Dark-Gray, #484848);
+}
diff --git a/frontend/src/components/TextField.tsx b/frontend/src/components/TextField.tsx
index 92fed03..47e08b2 100644
--- a/frontend/src/components/TextField.tsx
+++ b/frontend/src/components/TextField.tsx
@@ -14,11 +14,10 @@ const TextField = forwardRef(
{ label, error, required, helperText, ...props }: TextFieldProps,
ref: ForwardedRef
,
) => {
- console.log(props);
return (
- {required && * }
+ {required ? * : null}
{label}
-
{helperText}
+ {helperText ?
{helperText}
: null}
);
},
diff --git a/frontend/src/util/validateResponses.ts b/frontend/src/util/validateResponses.ts
deleted file mode 100644
index 9d66c1f..0000000
--- a/frontend/src/util/validateResponses.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export function isnum(num: string): boolean {
- return /^\d+$/.test(num);
-}
-
-export function validateAge(age: string): string {
- if (!isnum(age)) {
- return "Age is not a number";
- } else {
- return "Success";
- }
-}
-
-export function validateSpouseName(maritalStatus: string, spouseName: string): string {
- if (maritalStatus === "Married") {
- return spouseName === "" ? "Spouse name is required" : "Success";
- } else if (maritalStatus === "Single") {
- return spouseName === "" ? "Success" : "Spouse name is not required";
- }
- return "Success";
-}
-
-export function validateEthnicityOther(ethnicities: string, other: string) {
- if ((ethnicities === "" && other !== "") || (ethnicities !== "" && other === "")) {
- return "Success";
- } else if (ethnicities === "" && other === "") {
- return "Please fill out the other field";
- } else {
- return "Please leave the other field empty";
- }
-}
From ad04b6ac0300fdbbcc73884bcee3d72f36c03ae9 Mon Sep 17 00:00:00 2001
From: benjaminjohnson2204
Date: Thu, 22 Feb 2024 17:08:00 -0800
Subject: [PATCH 18/19] Update favicon, title, & description, remove unused
boilerplate
---
frontend/public/next.svg | 1 -
frontend/public/vercel.svg | 1 -
frontend/src/app/dummyPage/layout.tsx | 4 +-
frontend/src/app/favicon.ico | Bin 25931 -> 15406 bytes
frontend/src/app/layout.tsx | 4 +-
frontend/src/app/login/layout.tsx | 4 +-
frontend/src/app/page.module.css | 227 --------------------------
7 files changed, 6 insertions(+), 235 deletions(-)
delete mode 100644 frontend/public/next.svg
delete mode 100644 frontend/public/vercel.svg
diff --git a/frontend/public/next.svg b/frontend/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/frontend/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg
deleted file mode 100644
index d2f8422..0000000
--- a/frontend/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/app/dummyPage/layout.tsx b/frontend/src/app/dummyPage/layout.tsx
index 1914d33..5ffb94e 100644
--- a/frontend/src/app/dummyPage/layout.tsx
+++ b/frontend/src/app/dummyPage/layout.tsx
@@ -1,6 +1,6 @@
export const metadata = {
- title: "Next.js",
- description: "Generated by Next.js",
+ title: "Patriots & Paws",
+ description: "Web application for Patriots & Paws",
};
export default function DummyLayout({ children }: { children: React.ReactNode }) {
diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico
index 718d6fea4835ec2d246af9800eddb7ffb276240c..b643ed91520810b773f38808bb1f7a5c2c9cd2c7 100644
GIT binary patch
literal 15406
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
XDl02L+gP0%l?VOr^FI!JGY9?;o=1KE
literal 25931
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
LrJugUA?W`A8`#=m
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 9359ec2..9bdea3d 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -7,8 +7,8 @@ import "@/app/globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Patriots & Paws",
+ description: "Web application for Patriots & Paws",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
diff --git a/frontend/src/app/login/layout.tsx b/frontend/src/app/login/layout.tsx
index 05464f6..cd37163 100644
--- a/frontend/src/app/login/layout.tsx
+++ b/frontend/src/app/login/layout.tsx
@@ -1,6 +1,6 @@
export const metadata = {
- title: "Next.js",
- description: "Generated by Next.js",
+ title: "Patriots & Paws",
+ description: "Web application for Patriots & Paws",
};
export default function LoginLayout({ children }: { children: React.ReactNode }) {
diff --git a/frontend/src/app/page.module.css b/frontend/src/app/page.module.css
index 61e0547..e69de29 100644
--- a/frontend/src/app/page.module.css
+++ b/frontend/src/app/page.module.css
@@ -1,227 +0,0 @@
-.main {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
- padding: 6rem;
- min-height: 100vh;
-}
-
-.description {
- display: inherit;
- justify-content: inherit;
- align-items: inherit;
- font-size: 0.85rem;
- max-width: var(--max-width);
- width: 100%;
- z-index: 2;
- font-family: var(--font-mono);
-}
-
-.description a {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 0.5rem;
-}
-
-.description p {
- position: relative;
- margin: 0;
- padding: 1rem;
- background-color: rgba(var(--callout-rgb), 0.5);
- border: 1px solid rgba(var(--callout-border-rgb), 0.3);
- border-radius: var(--border-radius);
-}
-
-.code {
- font-weight: 700;
- font-family: var(--font-mono);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(4, minmax(25%, auto));
- max-width: 100%;
- width: var(--max-width);
-}
-
-.card {
- padding: 1rem 1.2rem;
- border-radius: var(--border-radius);
- background: rgba(var(--card-rgb), 0);
- border: 1px solid rgba(var(--card-border-rgb), 0);
- transition:
- background 200ms,
- border 200ms;
-}
-
-.card span {
- display: inline-block;
- transition: transform 200ms;
-}
-
-.card h2 {
- font-weight: 600;
- margin-bottom: 0.7rem;
-}
-
-.card p {
- margin: 0;
- opacity: 0.6;
- font-size: 0.9rem;
- line-height: 1.5;
- max-width: 30ch;
-}
-
-.center {
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- padding: 4rem 0;
-}
-
-.center::before {
- background: var(--secondary-glow);
- border-radius: 50%;
- width: 480px;
- height: 360px;
- margin-left: -400px;
-}
-
-.center::after {
- background: var(--primary-glow);
- width: 240px;
- height: 180px;
- z-index: -1;
-}
-
-.center::before,
-.center::after {
- content: "";
- left: 50%;
- position: absolute;
- filter: blur(45px);
- transform: translateZ(0);
-}
-
-.logo {
- position: relative;
-}
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- .card:hover {
- background: rgba(var(--card-rgb), 0.1);
- border: 1px solid rgba(var(--card-border-rgb), 0.15);
- }
-
- .card:hover span {
- transform: translateX(4px);
- }
-}
-
-@media (prefers-reduced-motion) {
- .card:hover span {
- transform: none;
- }
-}
-
-/* Mobile */
-@media (max-width: 700px) {
- .content {
- padding: 4rem;
- }
-
- .grid {
- grid-template-columns: 1fr;
- margin-bottom: 120px;
- max-width: 320px;
- text-align: center;
- }
-
- .card {
- padding: 1rem 2.5rem;
- }
-
- .card h2 {
- margin-bottom: 0.5rem;
- }
-
- .center {
- padding: 8rem 0 6rem;
- }
-
- .center::before {
- transform: none;
- height: 300px;
- }
-
- .description {
- font-size: 0.8rem;
- }
-
- .description a {
- padding: 1rem;
- }
-
- .description p,
- .description div {
- display: flex;
- justify-content: center;
- position: fixed;
- width: 100%;
- }
-
- .description p {
- align-items: center;
- inset: 0 0 auto;
- padding: 2rem 1rem 1.4rem;
- border-radius: 0;
- border: none;
- border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
- background: linear-gradient(
- to bottom,
- rgba(var(--background-start-rgb), 1),
- rgba(var(--callout-rgb), 0.5)
- );
- background-clip: padding-box;
- backdrop-filter: blur(24px);
- }
-
- .description div {
- align-items: flex-end;
- pointer-events: none;
- inset: auto 0 0;
- padding: 2rem;
- height: 200px;
- background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%);
- z-index: 1;
- }
-}
-
-/* Tablet and Smaller Desktop */
-@media (min-width: 701px) and (max-width: 1120px) {
- .grid {
- grid-template-columns: repeat(2, 50%);
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .vercelLogo {
- filter: invert(1);
- }
-
- .logo {
- filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
- }
-}
-
-@keyframes rotate {
- from {
- transform: rotate(360deg);
- }
- to {
- transform: rotate(0deg);
- }
-}
From 01f7db20397270775a2c3e6d4180e5f7f34bd78f Mon Sep 17 00:00:00 2001
From: benjaminjohnson2204
Date: Thu, 22 Feb 2024 17:44:28 -0800
Subject: [PATCH 19/19] Remove commented-out code & make ethnicity an array
---
backend/src/controllers/vsr.ts | 1 -
backend/src/models/vsr.ts | 2 +-
backend/src/validators/vsr.ts | 8 +++-
frontend/src/api/VSRs.ts | 18 +++----
frontend/src/api/requests.ts | 4 +-
frontend/src/app/vsr/page.tsx | 44 ++++++++++-------
frontend/src/components/Dropdown.tsx | 2 +-
frontend/src/components/MultipleChoice.tsx | 56 ++++++++++++++--------
frontend/src/components/TextField.tsx | 2 +-
9 files changed, 85 insertions(+), 52 deletions(-)
diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts
index 838d5be..641eb40 100644
--- a/backend/src/controllers/vsr.ts
+++ b/backend/src/controllers/vsr.ts
@@ -1,5 +1,4 @@
import { RequestHandler } from "express";
-//import createHttpError from "http-errors";
import { validationResult } from "express-validator";
import VSRModel from "src/models/vsr";
import validationErrorParser from "src/util/validationErrorParser";
diff --git a/backend/src/models/vsr.ts b/backend/src/models/vsr.ts
index a432db9..8f5b551 100644
--- a/backend/src/models/vsr.ts
+++ b/backend/src/models/vsr.ts
@@ -9,7 +9,7 @@ const vsrSchema = new Schema({
spouseName: { type: String },
agesOfBoys: { type: [Number] },
agesOfGirls: { type: [Number] },
- ethnicity: { type: String, require: true },
+ ethnicity: { type: [String], require: true },
employmentStatus: { type: String, require: true },
incomeLevel: { type: String, require: true },
sizeOfHome: { type: String, require: true },
diff --git a/backend/src/validators/vsr.ts b/backend/src/validators/vsr.ts
index c296e33..f57bc14 100644
--- a/backend/src/validators/vsr.ts
+++ b/backend/src/validators/vsr.ts
@@ -54,8 +54,12 @@ const makeEthnicityValidator = () =>
body("ethnicity")
.exists({ checkFalsy: true })
.withMessage("Ethnicity is required")
- .isString()
- .withMessage("Ethnicity must be a string");
+ .isArray()
+ .withMessage("Ethnicity must be an array")
+ .custom((ethnicities: string[]) =>
+ ethnicities.every((ethnicity) => typeof ethnicity === "string"),
+ )
+ .withMessage("Each ethnicity in Ethnicities must be a positive integer");
const makeEmploymentStatusValidator = () =>
body("employmentStatus")
diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts
index c3cd35e..ea672e3 100644
--- a/frontend/src/api/VSRs.ts
+++ b/frontend/src/api/VSRs.ts
@@ -8,9 +8,9 @@ export interface VSRJson {
age: number;
maritalStatus: string;
spouseName?: string;
- agesOfBoys?: number[];
- agesOfGirls?: number[];
- ethnicity: string;
+ agesOfBoys: number[];
+ agesOfGirls: number[];
+ ethnicity: string[];
employmentStatus: string;
incomeLevel: string;
sizeOfHome: string;
@@ -24,9 +24,9 @@ export interface VSR {
age: number;
maritalStatus: string;
spouseName?: string;
- agesOfBoys?: number[];
- agesOfGirls?: number[];
- ethnicity: string;
+ agesOfBoys: number[];
+ agesOfGirls: number[];
+ ethnicity: string[];
employmentStatus: string;
incomeLevel: string;
sizeOfHome: string;
@@ -38,9 +38,9 @@ export interface CreateVSRRequest {
age: number;
maritalStatus: string;
spouseName?: string;
- agesOfBoys?: number[];
- agesOfGirls?: number[];
- ethnicity: string;
+ agesOfBoys: number[];
+ agesOfGirls: number[];
+ ethnicity: string[];
employmentStatus: string;
incomeLevel: string;
sizeOfHome: string;
diff --git a/frontend/src/api/requests.ts b/frontend/src/api/requests.ts
index f442631..bc0883d 100644
--- a/frontend/src/api/requests.ts
+++ b/frontend/src/api/requests.ts
@@ -1,4 +1,6 @@
-const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL;
+import env from "@/util/validateEnv";
+
+const API_BASE_URL = env.NEXT_PUBLIC_BACKEND_URL;
type Method = "GET" | "POST" | "PUT";
/**
diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx
index 49ae66e..b49b19b 100644
--- a/frontend/src/app/vsr/page.tsx
+++ b/frontend/src/app/vsr/page.tsx
@@ -34,7 +34,7 @@ const VeteranServiceRequest: React.FC = () => {
formState: { errors },
watch,
} = useForm();
- const [selectedEthnicity, setSelectedEthnicity] = useState("");
+ const [selectedEthnicities, setSelectedEthnicities] = useState([]);
const [otherEthnicity, setOtherEthnicity] = useState("");
const numBoys = watch("num_boys");
@@ -89,7 +89,7 @@ const VeteranServiceRequest: React.FC = () => {
spouseName: data.spouse,
agesOfBoys: data.ages_of_boys?.slice(0, data.num_boys) ?? [],
agesOfGirls: data.ages_of_girls?.slice(0, data.num_girls) ?? [],
- ethnicity: selectedEthnicity === "" ? otherEthnicity : selectedEthnicity,
+ ethnicity: selectedEthnicities.concat(otherEthnicity === "" ? [] : [otherEthnicity]),
employmentStatus: data.employment_status,
incomeLevel: data.income_level,
sizeOfHome: data.size_of_home,
@@ -129,7 +129,7 @@ const VeteranServiceRequest: React.FC = () => {
message: "This field must be a number no greater than 100",
},
})}
- required={true}
+ required
error={!!errors[`num_${gender}s`]}
helperText={errors[`num_${gender}s`]?.message}
/>
@@ -151,7 +151,7 @@ const VeteranServiceRequest: React.FC = () => {
})}
error={!!errors[`ages_of_${gender}s`]?.[index]}
helperText={errors[`ages_of_${gender}s`]?.[index]?.message}
- required={true}
+ required
/>
))}
@@ -198,7 +198,7 @@ const VeteranServiceRequest: React.FC = () => {
variant="outlined"
placeholder="e.g. Justin Timberlake"
{...register("name", { required: "Name is required" })}
- required={true}
+ required
error={!!errors.name}
helperText={errors.name?.message}
/>
@@ -220,7 +220,7 @@ const VeteranServiceRequest: React.FC = () => {
options={genderOptions}
value={field.value}
onChange={(e) => field.onChange(e)}
- required={true}
+ required
error={!!errors.gender}
helperText={errors.gender?.message}
placeholder="Select your gender"
@@ -234,7 +234,7 @@ const VeteranServiceRequest: React.FC = () => {
variant="outlined"
placeholder="Enter your age"
{...register("age", { required: "Age is required" })}
- required={true}
+ required
error={!!errors.age}
helperText={errors.age?.message}
/>
@@ -253,7 +253,7 @@ const VeteranServiceRequest: React.FC = () => {
options={maritalOptions}
value={field.value}
onChange={(newValue) => field.onChange(newValue)}
- required={true}
+ required
error={!!errors.marital_status}
helperText={errors.marital_status?.message}
/>
@@ -265,7 +265,11 @@ const VeteranServiceRequest: React.FC = () => {
label="Spouse's Name"
variant="outlined"
placeholder="e.g. Jane Timberlake"
- {...register("spouse", {})}
+ {...register("spouse", {
+ required:
+ ["Married", "Widowed/Widower"].includes(watch().marital_status) &&
+ "Spouse's Name is required",
+ })}
required={["Married", "Widowed/Widower"].includes(watch().marital_status)}
error={!!errors.spouse}
helperText={errors.spouse?.message}
@@ -290,14 +294,18 @@ const VeteranServiceRequest: React.FC = () => {
{
- field.onChange(newValue);
- setSelectedEthnicity(newValue);
+ const valueToSet = ((newValue as string[]) ?? [])[0] ?? "";
+ if (valueToSet !== "" || otherEthnicity === "") {
+ field.onChange(valueToSet);
+ }
+ setSelectedEthnicities(newValue as string[]);
}}
- required={true}
+ required
error={!!errors.ethnicity}
helperText={errors.ethnicity?.message}
+ allowMultiple
/>
{
value={otherEthnicity}
onChange={(e) => {
const value = e.target.value;
- field.onChange(value);
+ if (value !== "" || selectedEthnicities.length === 0) {
+ field.onChange(value);
+ }
setOtherEthnicity(value);
}}
variant={"outlined"}
@@ -329,7 +339,7 @@ const VeteranServiceRequest: React.FC = () => {
options={employmentOptions}
value={field.value}
onChange={(newValue) => field.onChange(newValue)}
- required={true}
+ required
error={!!errors.employment_status}
helperText={errors.employment_status?.message}
/>
@@ -346,7 +356,7 @@ const VeteranServiceRequest: React.FC = () => {
options={incomeOptions}
value={field.value}
onChange={(newValue) => field.onChange(newValue)}
- required={true}
+ required
error={!!errors.income_level}
helperText={errors.income_level?.message}
/>
@@ -363,7 +373,7 @@ const VeteranServiceRequest: React.FC = () => {
options={homeOptions}
value={field.value}
onChange={(newValue) => field.onChange(newValue)}
- required={true}
+ required
error={!!errors.size_of_home}
helperText={errors.size_of_home?.message}
/>
diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx
index 9cf8841..e9f1204 100644
--- a/frontend/src/components/Dropdown.tsx
+++ b/frontend/src/components/Dropdown.tsx
@@ -36,7 +36,7 @@ const Dropdown = ({
onChange={onChange}
error={error}
displayEmpty
- fullWidth={true}
+ fullWidth
renderValue={(value) =>
value === "" ? {placeholder}
: value
}
diff --git a/frontend/src/components/MultipleChoice.tsx b/frontend/src/components/MultipleChoice.tsx
index bd62206..a9f8591 100644
--- a/frontend/src/components/MultipleChoice.tsx
+++ b/frontend/src/components/MultipleChoice.tsx
@@ -4,9 +4,10 @@ import styles from "@/components/MultipleChoice.module.css";
export interface MultipleChoiceProps {
label: string;
options: string[];
- value: string;
- onChange: (selected: string) => void;
+ value: string | string[];
+ onChange: (selected: string | string[]) => void;
required: boolean;
+ allowMultiple?: boolean;
error?: boolean;
helperText?: string;
}
@@ -17,6 +18,7 @@ const MultipleChoice = ({
value,
onChange,
required,
+ allowMultiple = false,
helperText,
}: MultipleChoiceProps) => {
return (
@@ -26,23 +28,39 @@ const MultipleChoice = ({
{label}
- {options.map((option) => (
- {
- if (value === option) {
- onChange("");
- } else {
- onChange(option);
- }
- }}
- className={`${styles.chip} ${
- value === option ? styles.chipSelected : styles.chipUnselected
- }`}
- clickable
- />
- ))}
+ {options.map((option) => {
+ const optionIsSelected = allowMultiple ? value?.includes(option) : value === option;
+
+ return (
+ {
+ if (allowMultiple) {
+ if (optionIsSelected) {
+ // Allow multiple + already selected -> remove option from selected
+ onChange(((value as string[]) ?? []).filter((_value) => _value !== option));
+ } else {
+ // Allow multiple + not already selected -> add option to selected
+ onChange(((value as string[]) ?? []).concat([option]));
+ }
+ } else {
+ if (optionIsSelected) {
+ // Disallow multiple + already selected -> set value to nothing selected
+ onChange("");
+ } else {
+ // Disallow multiple + not already selected -> set value to option
+ onChange(option);
+ }
+ }
+ }}
+ className={`${styles.chip} ${
+ optionIsSelected ? styles.chipSelected : styles.chipUnselected
+ }`}
+ clickable
+ />
+ );
+ })}
{helperText ? {helperText}
: null}
diff --git a/frontend/src/components/TextField.tsx b/frontend/src/components/TextField.tsx
index 47e08b2..34c6062 100644
--- a/frontend/src/components/TextField.tsx
+++ b/frontend/src/components/TextField.tsx
@@ -22,7 +22,7 @@ const TextField = forwardRef(