diff --git a/backend/package.json b/backend/package.json index fbd45f6..381d4f8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -53,5 +53,9 @@ }, "_moduleAliases": { "src": "src" - } + }, + "cacheDirectories": [ + "node_modules/", + ".next/cache/" + ] } diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index dcb01db..0443c5e 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -1,8 +1,24 @@ import { RequestHandler } from "express"; import { validationResult } from "express-validator"; +import createHttpError from "http-errors"; import VSRModel from "src/models/vsr"; import validationErrorParser from "src/util/validationErrorParser"; +export const getVSR: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const vsr = await VSRModel.findById(id); + + if (vsr === null) { + throw createHttpError(404, "VSR not found at id " + id); + } + res.status(200).json(vsr); + } catch (error) { + next(error); + } +}; + export const createVSR: RequestHandler = async (req, res, next) => { // extract any errors that were found by the validator const errors = validationResult(req); @@ -25,11 +41,10 @@ export const createVSR: RequestHandler = async (req, res, next) => { validationErrorParser(errors); // Get the current date as a timestamp for when VSR was submitted - const date = new Date(); + const currentDate = new Date(); const vsr = await VSRModel.create({ name, - date, gender, age, maritalStatus, @@ -40,6 +55,10 @@ export const createVSR: RequestHandler = async (req, res, next) => { employmentStatus, incomeLevel, sizeOfHome, + + // Use current date as timestamp for received & updated + dateReceived: currentDate, + lastUpdated: currentDate, }); // 201 means a new resource has been created successfully diff --git a/backend/src/models/vsr.ts b/backend/src/models/vsr.ts index 8f5b551..5145009 100644 --- a/backend/src/models/vsr.ts +++ b/backend/src/models/vsr.ts @@ -2,7 +2,6 @@ import { InferSchemaType, Schema, model } from "mongoose"; const vsrSchema = new Schema({ 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 }, @@ -13,6 +12,29 @@ const vsrSchema = new Schema({ employmentStatus: { type: String, require: true }, incomeLevel: { type: String, require: true }, sizeOfHome: { type: String, require: true }, + streetAddress: { type: String, required: true }, + city: { type: String, required: true }, + state: { type: String, required: true }, + zipCode: { type: Number, required: true }, + phoneNumber: { type: String, required: true }, + email: { type: String, required: true }, + branch: { type: [String], required: true }, + conflicts: { type: [String], required: true }, + dischargeStatus: { type: String, required: true }, + serviceConnected: { type: String, required: true }, + lastRank: { type: String, required: true }, + militaryId: { type: Number, required: true }, + petCompanion: { type: String, required: true }, + hearFrom: { type: String, required: true }, + bedroomFurnishing: { type: [String], required: true }, + bathroomFurnishing: { type: [String], required: true }, + kitchenFurnishing: { type: [String], required: true }, + livingRoomFurnishing: { type: [String], required: true }, + diningRoomFurnishing: { type: [String], required: true }, + otherFurnishing: { type: [String], required: true }, + dateReceived: { type: Date, required: true }, + lastUpdated: { type: Date, required: true }, + status: { type: String, required: true }, }); type VSR = InferSchemaType; diff --git a/backend/src/routes/vsr.ts b/backend/src/routes/vsr.ts index 19cf6e5..f981e34 100644 --- a/backend/src/routes/vsr.ts +++ b/backend/src/routes/vsr.ts @@ -4,6 +4,7 @@ import * as VSRValidator from "src/validators/vsr"; const router = express.Router(); +router.get("/:id", VSRController.getVSR); router.post("/", VSRValidator.createVSR, VSRController.createVSR); router.get("/", VSRController.getAllVSRS); diff --git a/frontend/__tests__/sampleData/pap.vsrs.json b/frontend/__tests__/sampleData/pap.vsrs.json new file mode 100644 index 0000000..6f7d040 --- /dev/null +++ b/frontend/__tests__/sampleData/pap.vsrs.json @@ -0,0 +1,41 @@ +[ + { + "_id": { + "$oid": "65bc31561826f0d6ee2c4b21" + }, + "name": "Sophia Yu", + "gender": "Female", + "age": "25", + "agesOfGirls": ["11"], + "employmentStatus": "Employed", + "ethnicity": "Asian", + "incomeLevel": "$12,500 - $25,000", + "maritalStatus": "Single", + "sizeOfHome": "House", + "spouseName": "Name", + "branch": ["Air Force", "Navy"], + "city": "San Diego", + "conflicts": ["WWII", "Irani Crisis"], + "email": "yusophia2011@gmail.com", + "phoneNumber": "(858) 790-2406", + "state": "CA", + "streetAddress": "13571 Marguerite Creek Way", + "zipCode": "92130", + "agesOfBoys": [10], + "bedroomFurnishing": [], + "dischargeStatus": "dischargeStatus-1", + "lastRank": "lastRank-1", + "militaryId": 1030, + "petCompanion": "Yes-1", + "serviceConnected": "Yes-1", + "bathroomFurnishing": ["Toiletries"], + "dateReceived": "2024-02-26T03:43:15.152Z", + "diningRoomFurnishing": ["Chairs", "Table"], + "kitchenFurnishing": ["Oven"], + "lastUpdated": "2024-02-26T03:43:15.152Z", + "livingRoomFurnishing": ["Couch"], + "otherFurnishing": ["Lawn Chair"], + "status": "Received", + "hearFrom": "Family-1" + } +] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b69db31..c0615ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "firebase": "^10.6.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "moment": "^2.30.1", "next": "14.0.3", "react": "^18", "react-dom": "^18", @@ -59,9 +60,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==" + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==" }, "node_modules/@ampproject/remapping": { "version": "2.2.1", @@ -636,9 +637,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", - "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2253,12 +2254,12 @@ } }, "node_modules/@mui/styled-engine-sc": { - "version": "6.0.0-alpha.13", - "resolved": "https://registry.npmjs.org/@mui/styled-engine-sc/-/styled-engine-sc-6.0.0-alpha.13.tgz", - "integrity": "sha512-QOv9oEZL3J/DYUJaQ6QFE7fTMxy/6J9mKKsdmQgHpgeESddiiCS0MCYoPsKoPcj6RABfzG12uqnq6o5s1seRLg==", + "version": "6.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@mui/styled-engine-sc/-/styled-engine-sc-6.0.0-alpha.16.tgz", + "integrity": "sha512-hZT4xPk/zvFtNvUS/VumrtSe+sV2eZB2Z048z0RGgxMDLTGgWAIs4kJ+YAS16ugmE2nwrlaZhpfr/1lD7gd7xg==", "dependencies": { - "@babel/runtime": "^7.23.8", - "csstype": "^3.1.2", + "@babel/runtime": "^7.23.9", + "csstype": "^3.1.3", "hoist-non-react-statics": "^3.3.2", "prop-types": "^15.8.1" }, @@ -2273,6 +2274,11 @@ "styled-components": "^6.0.0" } }, + "node_modules/@mui/styled-engine-sc/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/@mui/system": { "version": "5.15.6", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.6.tgz", @@ -5148,19 +5154,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7019,6 +7012,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7715,12 +7716,11 @@ } }, "node_modules/react-hook-form": { - "version": "7.49.3", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", - "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "version": "7.50.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz", + "integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==", "engines": { - "node": ">=18", - "pnpm": "8" + "node": ">=12.22.0" }, "funding": { "type": "opencollective", diff --git a/frontend/package.json b/frontend/package.json index 3c12ae1..c08b4d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "firebase": "^10.6.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "moment": "^2.30.1", "next": "14.0.3", "react": "^18", "react-dom": "^18", diff --git a/frontend/public/dropdown.svg b/frontend/public/dropdown.svg new file mode 100644 index 0000000..59ea220 --- /dev/null +++ b/frontend/public/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/ic_arrowback.svg b/frontend/public/ic_arrowback.svg new file mode 100644 index 0000000..087c099 --- /dev/null +++ b/frontend/public/ic_arrowback.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/ic_edit.svg b/frontend/public/ic_edit.svg new file mode 100644 index 0000000..728339f --- /dev/null +++ b/frontend/public/ic_edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/ic_upload.svg b/frontend/public/ic_upload.svg new file mode 100644 index 0000000..960913d --- /dev/null +++ b/frontend/public/ic_upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..de05a25 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/frontend/public/keyboard_arrow_down10.svg b/frontend/public/keyboard_arrow_down10.svg new file mode 100644 index 0000000..6756205 --- /dev/null +++ b/frontend/public/keyboard_arrow_down10.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/keyboard_arrow_down24.svg b/frontend/public/keyboard_arrow_down24.svg new file mode 100644 index 0000000..9cdceb6 --- /dev/null +++ b/frontend/public/keyboard_arrow_down24.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/lora.woff b/frontend/public/lora.woff new file mode 100644 index 0000000..97c7482 Binary files /dev/null and b/frontend/public/lora.woff differ diff --git a/frontend/public/opensans.woff b/frontend/public/opensans.woff new file mode 100644 index 0000000..828dc86 Binary files /dev/null and b/frontend/public/opensans.woff differ diff --git a/frontend/public/user_ellipse.svg b/frontend/public/user_ellipse.svg new file mode 100644 index 0000000..f4c320e --- /dev/null +++ b/frontend/public/user_ellipse.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index a8cd7cd..6efc691 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -3,7 +3,6 @@ import { APIResult, get, handleAPIError, post } from "@/api/requests"; export interface VSRJson { _id: string; name: string; - date: Date; gender: string; age: number; maritalStatus: string; @@ -14,6 +13,29 @@ export interface VSRJson { employmentStatus: string; incomeLevel: string; sizeOfHome: string; + streetAddress: string; + city: string; + state: string; + zipCode: number; + phoneNumber: string; + email: string; + branch: string[]; + conflicts: string[]; + dischargeStatus: string; + serviceConnected: boolean; + lastRank: string; + militaryId: number; + petCompanion: boolean; + bedroomFurnishing: string[]; + bathroomFurnishing: string[]; + kitchenFurnishing: string[]; + livingRoomFurnishing: string[]; + diningRoomFurnishing: string[]; + otherFurnishing: string[]; + dateReceived: string; + lastUpdated: string; + status: string; + hearFrom: string; } export interface VSRListJson { @@ -23,7 +45,6 @@ export interface VSRListJson { export interface VSR { _id: string; name: string; - date: string; gender: string; age: number; maritalStatus: string; @@ -34,6 +55,29 @@ export interface VSR { employmentStatus: string; incomeLevel: string; sizeOfHome: string; + streetAddress: string; + city: string; + state: string; + zipCode: number; + phoneNumber: string; + email: string; + branch: string[]; + conflicts: string[]; + dischargeStatus: string; + serviceConnected: boolean; + lastRank: string; + militaryId: number; + petCompanion: boolean; + bedroomFurnishing: string[]; + bathroomFurnishing: string[]; + kitchenFurnishing: string[]; + livingRoomFurnishing: string[]; + diningRoomFurnishing: string[]; + otherFurnishing: string[]; + dateReceived: Date; + lastUpdated: Date; + status: string; + hearFrom: string; } export interface CreateVSRRequest { @@ -48,13 +92,35 @@ export interface CreateVSRRequest { employmentStatus: string; incomeLevel: string; sizeOfHome: string; + + // Comment-out page 2 & 3 fields for now because they're not implemented on the form yet + // streetAddress: string; + // city: string; + // state: string; + // zipCode: number; + // phoneNumber: string; + // email: string; + // branch: string[]; + // conflicts: string[]; + // dischargeStatus: string; + // serviceConnected: boolean; + // lastRank: string; + // militaryId: number; + // petCompanion: boolean; + // bedroomFurnishing: string[]; + // bathroomFurnishing: string[]; + // kitchenFurnishing: string[]; + // livingRoomFurnishing: string[]; + // diningRoomFurnishing: string[]; + // otherFurnishing: string[]; + // status: string; + // hearFrom: string; } function parseVSR(vsr: VSRJson) { return { _id: vsr._id, name: vsr.name, - date: new Date(vsr.date).toISOString(), gender: vsr.gender, age: vsr.age, maritalStatus: vsr.maritalStatus, @@ -65,6 +131,29 @@ function parseVSR(vsr: VSRJson) { employmentStatus: vsr.employmentStatus, incomeLevel: vsr.incomeLevel, sizeOfHome: vsr.sizeOfHome, + streetAddress: vsr.streetAddress, + city: vsr.city, + state: vsr.state, + zipCode: vsr.zipCode, + phoneNumber: vsr.phoneNumber, + email: vsr.email, + branch: vsr.branch, + conflicts: vsr.conflicts, + dischargeStatus: vsr.dischargeStatus, + serviceConnected: vsr.serviceConnected, + lastRank: vsr.lastRank, + militaryId: vsr.militaryId, + petCompanion: vsr.petCompanion, + bedroomFurnishing: vsr.bedroomFurnishing, + bathroomFurnishing: vsr.bathroomFurnishing, + kitchenFurnishing: vsr.kitchenFurnishing, + livingRoomFurnishing: vsr.livingRoomFurnishing, + diningRoomFurnishing: vsr.diningRoomFurnishing, + otherFurnishing: vsr.otherFurnishing, + dateReceived: new Date(vsr.dateReceived), + lastUpdated: new Date(vsr.lastUpdated), + status: vsr.status, + hearFrom: vsr.hearFrom, }; } @@ -87,3 +176,13 @@ export async function getAllVSRs(): Promise> { return handleAPIError(error); } } + +export async function getVSR(id: string): Promise> { + try { + const response = await get(`/api/vsr/${id}`); + const json = (await response.json()) as VSRJson; + return { success: true, data: parseVSR(json) }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1b915bd..dd66367 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -9,11 +9,10 @@ --color-tse-accent-red-1: #be2d46; --color-tse-accent-red-2: #620c02; --color-tse-light-gray: #818181; - --color-tse-neutral-gray-0: #f3f3f3; --color-tse-dark-gray: #484848; --color-tse-secondary-light-blue: #c7def1; --font-title: "Lora", serif; - --font-open-sans: "Open Sans", sans-serif; + --font-open-sans: "OpenSans", sans-serif; --font-inter: "Inter", sans-serif; } @@ -60,6 +59,7 @@ html, body { max-width: 100vw; overflow-x: hidden; + background-color: var(--color-tse-primary-light); } body { @@ -78,3 +78,15 @@ a { color-scheme: dark; } } + +@font-face { + font-family: Lora; + src: url("/lora.woff") format("woff"); + font-style: normal; +} + +@font-face { + font-family: OpenSans; + src: url("/opensans.woff") format("woff"); + font-style: normal; +} diff --git a/frontend/src/app/staff/vsr/[id]/page.tsx b/frontend/src/app/staff/vsr/[id]/page.tsx new file mode 100644 index 0000000..29226fa --- /dev/null +++ b/frontend/src/app/staff/vsr/[id]/page.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Page } from "@/components/VSRIndividual"; + +export default function Individual() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/VSRIndividual/AdditionalInfo/index.tsx b/frontend/src/components/VSRIndividual/AdditionalInfo/index.tsx new file mode 100644 index 0000000..f0cac31 --- /dev/null +++ b/frontend/src/components/VSRIndividual/AdditionalInfo/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/ContactInfo/styles.module.css"; +import { ListDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface AdditionalInfoProps { + vsr: VSR; +} + +export const AdditionalInfo = ({ vsr }: AdditionalInfoProps) => { + return ( + +
+ +
+
+ +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/AdditionalInfo/styles.module.css b/frontend/src/components/VSRIndividual/AdditionalInfo/styles.module.css new file mode 100644 index 0000000..6de75e7 --- /dev/null +++ b/frontend/src/components/VSRIndividual/AdditionalInfo/styles.module.css @@ -0,0 +1,14 @@ +.row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.row > * { + padding-right: 32px; + width: 100%; +} + +.row:not(:last-child) { + padding-bottom: 32px; +} diff --git a/frontend/src/components/VSRIndividual/CaseDetails/index.tsx b/frontend/src/components/VSRIndividual/CaseDetails/index.tsx new file mode 100644 index 0000000..990c6f3 --- /dev/null +++ b/frontend/src/components/VSRIndividual/CaseDetails/index.tsx @@ -0,0 +1,38 @@ +import styles from "src/components/VSRIndividual/CaseDetails/styles.module.css"; +import { SingleDetail, DropdownDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import moment from "moment"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface CaseDetailsProp { + vsr: VSR; +} +export const CaseDetails = ({ vsr }: CaseDetailsProp) => { + /** + * Formats a Date object as a string in our desired format, using Moment.js library + */ + const formatDate = (date: Date) => { + const dateMoment = moment(date); + // We need to do 2 separate format() calls because Moment treats brackets ("[]") as escape chars + return `${dateMoment.format("MM-DD-YYYY")} [${dateMoment.format("hh:mm A")}]`; + }; + + return ( + +
+ + + + +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/CaseDetails/styles.module.css b/frontend/src/components/VSRIndividual/CaseDetails/styles.module.css new file mode 100644 index 0000000..ec40012 --- /dev/null +++ b/frontend/src/components/VSRIndividual/CaseDetails/styles.module.css @@ -0,0 +1,13 @@ +.details { + display: flex; + flex-direction: row; + margin-left: 4px; + margin-right: 4px; + padding-bottom: 5px; + margin-top: 15px; + overflow: auto; +} + +.details > * { + padding-right: 100px; +} diff --git a/frontend/src/components/VSRIndividual/ContactInfo/index.tsx b/frontend/src/components/VSRIndividual/ContactInfo/index.tsx new file mode 100644 index 0000000..9169c4b --- /dev/null +++ b/frontend/src/components/VSRIndividual/ContactInfo/index.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/ContactInfo/styles.module.css"; +import { SingleDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface ContactInfoProps { + vsr: VSR; +} +export const ContactInfo = ({ vsr }: ContactInfoProps) => { + return ( + +
+ {" "} +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/ContactInfo/styles.module.css b/frontend/src/components/VSRIndividual/ContactInfo/styles.module.css new file mode 100644 index 0000000..19d2092 --- /dev/null +++ b/frontend/src/components/VSRIndividual/ContactInfo/styles.module.css @@ -0,0 +1,19 @@ +.row { + display: flex; + flex-direction: row; + position: relative; +} + +.row:not(:last-child) { + padding-bottom: 32px; +} + +.row > *:not(:last-child) { + width: calc(50% - 16px); +} + +.second { + position: absolute; + left: 50%; + transform: translateX(16px); +} diff --git a/frontend/src/components/VSRIndividual/DropdownDetail/index.tsx b/frontend/src/components/VSRIndividual/DropdownDetail/index.tsx new file mode 100644 index 0000000..f9572b2 --- /dev/null +++ b/frontend/src/components/VSRIndividual/DropdownDetail/index.tsx @@ -0,0 +1,117 @@ +import React, { useState } from "react"; +import styles from "src/components/VSRIndividual/DropdownDetail/styles.module.css"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import Select from "@mui/material/Select"; +import Image from "next/image"; + +export interface DropdownDetailProps { + title: string; + value: string; +} + +export function DropdownDetail({ title, value }: DropdownDetailProps) { + const DropdownIcon = () => ( + dropdown + ); + + const [selectedValue, setSelectedValue] = useState(value); + + const handleChange = (event: { target: { value: React.SetStateAction } }) => { + setSelectedValue(event.target.value); + }; + + return ( +
+
+
{title}
+ + + +
+
+ ); +} diff --git a/frontend/src/components/VSRIndividual/DropdownDetail/styles.module.css b/frontend/src/components/VSRIndividual/DropdownDetail/styles.module.css new file mode 100644 index 0000000..e379479 --- /dev/null +++ b/frontend/src/components/VSRIndividual/DropdownDetail/styles.module.css @@ -0,0 +1,61 @@ +.items { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.items .title { + color: rgba(35, 34, 32, 0.55); + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 10px; +} + +.items { + color: #222; + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + padding-top: 6px; + padding-bottom: 6px; +} + +.select { + padding-left: 0px; + border: none; +} + +.statusOption { + font-family: var(--font-open-sans); + padding: 2px 8px; + border-radius: 8px; +} + +.received { + background-color: #e6e6e6; +} + +.scheduled { + background-color: #c5e1f6; +} + +.approved { + background-color: #d7eebc; +} + +.resubmit { + background-color: #fae69e; +} + +.incomplete { + background-color: #f9cfc9; +} + +.archived { + background-color: #e4cef1; +} diff --git a/frontend/src/components/VSRIndividual/HeaderBar/index.tsx b/frontend/src/components/VSRIndividual/HeaderBar/index.tsx new file mode 100644 index 0000000..266dc8e --- /dev/null +++ b/frontend/src/components/VSRIndividual/HeaderBar/index.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/HeaderBar/styles.module.css"; +import Image from "next/image"; + +export const HeaderBar = () => { + return ( +
+ logo +
+ logo + logo +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/HeaderBar/styles.module.css b/frontend/src/components/VSRIndividual/HeaderBar/styles.module.css new file mode 100644 index 0000000..a4fa07c --- /dev/null +++ b/frontend/src/components/VSRIndividual/HeaderBar/styles.module.css @@ -0,0 +1,32 @@ +.headerBar { + position: sticky; + top: 0; + left: 0; + width: 100%; + height: 101px; + background-color: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; +} + +.logo { + margin-left: 63px; +} + +.profileButton { + display: flex; + border-radius: 50px; + border: 2px solid #bfbfbf; + margin-right: 63px; + width: 67px; + height: 43px; + padding-top: 6px; + padding-bottom: 5px; + padding-left: 6px; + padding-right: 9.5px; + align-items: center; + justify-content: space-between; +} diff --git a/frontend/src/components/VSRIndividual/ListDetail/index.tsx b/frontend/src/components/VSRIndividual/ListDetail/index.tsx new file mode 100644 index 0000000..b3a92a6 --- /dev/null +++ b/frontend/src/components/VSRIndividual/ListDetail/index.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/ListDetail/styles.module.css"; + +export interface ListDetailProps { + title: string; + values: string[]; +} + +export function ListDetail({ title, values }: ListDetailProps) { + const list = ( +
+ {values.map((value, index) => ( +
+
{value}
+
+ ))} +
+ ); + const noList =
N/A
; + + return ( +
+
+
{title}
+ {values.includes("N/A") ? noList : list} +
+
+ ); +} diff --git a/frontend/src/components/VSRIndividual/ListDetail/styles.module.css b/frontend/src/components/VSRIndividual/ListDetail/styles.module.css new file mode 100644 index 0000000..69f310e --- /dev/null +++ b/frontend/src/components/VSRIndividual/ListDetail/styles.module.css @@ -0,0 +1,50 @@ +.items { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.title { + color: rgba(35, 34, 32, 0.55); + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 10px; +} + +.list { + gap: 16px; +} + +.list:last-child { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.listItem { + color: #fff; + text-align: center; + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + height: 40px; + border-radius: 64px; + border: 1px solid var(--color-tse-accent-blue-1); + background: var(--color-tse-accent-blue-1); + white-space: nowrap; +} + +.listItem > * { + padding: 8px 16px; +} + +.noList { + color: #818181; + font-family: var(--font-open-sans); + font-size: 16px; + font-style: italic; +} diff --git a/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx b/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx new file mode 100644 index 0000000..6edea5e --- /dev/null +++ b/frontend/src/components/VSRIndividual/MilitaryBackground/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import styles from "@/components/VSRIndividual/ContactInfo/styles.module.css"; +import { SingleDetail, ListDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface MilitaryBackgroundProps { + vsr: VSR; +} + +export const MilitaryBackground = ({ vsr }: MilitaryBackgroundProps) => { + return ( + +
+ 0 ? vsr.branch : ["N/A"]} + /> +
+
+ 0 ? vsr.conflicts : ["N/A"]} + /> +
+
+ 0 ? [vsr.dischargeStatus] : ["N/A"] + } + /> +
+
+ +
+
+ + +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/MilitaryBackground/styles.module.css b/frontend/src/components/VSRIndividual/MilitaryBackground/styles.module.css new file mode 100644 index 0000000..1b30a8d --- /dev/null +++ b/frontend/src/components/VSRIndividual/MilitaryBackground/styles.module.css @@ -0,0 +1,13 @@ +.row { + display: flex; + flex-direction: row; +} + +.row > * { + padding-right: 32px; + width: 60%; +} + +.row:not(:last-child) { + padding-bottom: 32px; +} diff --git a/frontend/src/components/VSRIndividual/Page/index.tsx b/frontend/src/components/VSRIndividual/Page/index.tsx new file mode 100644 index 0000000..40d312e --- /dev/null +++ b/frontend/src/components/VSRIndividual/Page/index.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from "react"; +import { + HeaderBar, + VeteranTag, + ContactInfo, + CaseDetails, + PersonalInformation, + MilitaryBackground, + AdditionalInfo, + RequestedFurnishings, +} from "@/components/VSRIndividual"; +import styles from "src/components/VSRIndividual/Page/styles.module.css"; +import Image from "next/image"; +import { type VSR, getVSR } from "@/api/VSRs"; +import { useParams } from "next/navigation"; + +export const Page = () => { + const [vsr, setVSR] = useState({} as VSR); + const { id } = useParams(); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + getVSR(id as string) + .then((result) => { + if (result.success) { + setVSR(result.data); + setErrorMessage(null); + } else { + setErrorMessage("VSR not found."); + } + }) + .catch((error) => { + setErrorMessage(`An error occurred: ${error.message}`); + }); + }, [id]); + return ( +
+ + + + +
+
+
+ {errorMessage &&
{errorMessage}
} + + +
+ +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+
+
+
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/Page/styles.module.css b/frontend/src/components/VSRIndividual/Page/styles.module.css new file mode 100644 index 0000000..842eee4 --- /dev/null +++ b/frontend/src/components/VSRIndividual/Page/styles.module.css @@ -0,0 +1,157 @@ +.page { + background-color: var(--color-background); +} + +.toDashboard { + display: flex; + color: var(--color-tse-accent-blue-1); + margin-top: 35px; + max-width: 137px; + width: 100%; + height: 40px; + padding-left: 10px; + padding-top: 8px; + margin-left: calc(100vw * 1 / 12); + border-radius: 4px; + border: 1px solid var(--Secondary-Accent1, #102d5f); + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.toDashboard > * { + vertical-align: middle; +} + +.headerRow { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.actions { + margin-top: 29px; + margin-bottom: 32px; + gap: 32px; + align-self: center; + display: flex; + flex-direction: row; +} + +.button { + border-radius: 4px; + border: 1px solid var(--color-tse-accent-blue-1); + background-color: var(--color-tse-accent-blue-1); + height: 46px; + padding: 8px 16px; + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-align: center; + color: white; + cursor: pointer; +} + +#edit:hover { + background-color: #c7def1; +} + +.button > * { + padding-right: 6px; + vertical-align: middle; +} + +.name { + margin-top: 29px; + margin-bottom: 32px; +} + +.allDetails { + display: flex; + flex-direction: column; + align-items: center; + width: calc(100% * 5 / 6); + margin: 0 auto; +} + +.bodyDetails { + width: 100%; +} + +.otherDetails { + margin-top: 32px; + display: flex; + flex-direction: row; + gap: 42px; +} + +.personalInfo { + display: flex; + flex-direction: column; + flex: 38%; +} + +.personalInfo > * { + margin-bottom: 32px; +} + +.rightColumn { + flex: 62%; +} + +.finalActions { + display: flex; + flex-direction: row; + margin-top: 48px; + justify-content: flex-end; +} + +.request { + width: calc(100% * 239 / 713); + border-radius: 4px; + border: 1px solid var(--color-tse-accent-blue-1); + color: var(--color-tse-accent-blue-1); + text-align: center; + font-family: "Lora"; + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: normal; + background: rgba(255, 255, 255, 0); + align-items: end; + padding: 12px 24px; + margin-right: 32px; +} + +.approve { + width: calc(100% * 207 / 713); + border-radius: 4px; + border: 1px solid var(--color-tse-secondary-1); + color: white; + text-align: center; + font-family: "Lora"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + background: var(--color-tse-secondary-1); + align-items: end; + padding: 20px 24px; +} + +.footer { + height: 64px; +} + +.error { + color: red; + font-family: var(--font-open-sans); + font-size: 40px; + font-weight: 400; + line-height: 40px; +} diff --git a/frontend/src/components/VSRIndividual/PersonalInformation/index.tsx b/frontend/src/components/VSRIndividual/PersonalInformation/index.tsx new file mode 100644 index 0000000..86b06d9 --- /dev/null +++ b/frontend/src/components/VSRIndividual/PersonalInformation/index.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/ContactInfo/styles.module.css"; +import { SingleDetail, ListDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface PersonalInformationProps { + vsr: VSR; +} +export const PersonalInformation = ({ vsr }: PersonalInformationProps) => { + return ( + +
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ 0 ? vsr.spouseName : "N/A"} + /> +
+
+ + 0 ? vsr.agesOfBoys.join(", ") : "N/A"} + /> +
+
+ + 0 ? vsr.agesOfGirls.join(", ") : "N/A"} + /> +
+
+ 0 ? vsr.ethnicity : ["N/A"]} + /> +
+
+ +
+
+ +
+
+ +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/PersonalInformation/styles.module.css b/frontend/src/components/VSRIndividual/PersonalInformation/styles.module.css new file mode 100644 index 0000000..1b30a8d --- /dev/null +++ b/frontend/src/components/VSRIndividual/PersonalInformation/styles.module.css @@ -0,0 +1,13 @@ +.row { + display: flex; + flex-direction: row; +} + +.row > * { + padding-right: 32px; + width: 60%; +} + +.row:not(:last-child) { + padding-bottom: 32px; +} diff --git a/frontend/src/components/VSRIndividual/RequestedFurnishings/index.tsx b/frontend/src/components/VSRIndividual/RequestedFurnishings/index.tsx new file mode 100644 index 0000000..f088418 --- /dev/null +++ b/frontend/src/components/VSRIndividual/RequestedFurnishings/index.tsx @@ -0,0 +1,80 @@ +import styles from "src/components/VSRIndividual/RequestedFurnishings/styles.module.css"; +import { SingleDetail, ListDetail } from "@/components/VSRIndividual"; +import { type VSR } from "@/api/VSRs"; +import { VSRIndividualAccordion } from "../VSRIndividualAccordion"; + +export interface RequestedFurnishingsProps { + vsr: VSR; +} + +export const RequestedFurnishings = ({ vsr }: RequestedFurnishingsProps) => { + return ( + +
+ 0 + ? vsr.bedroomFurnishing + : ["N/A"] + } + /> +
+
+ 0 + ? vsr.bathroomFurnishing + : ["N/A"] + } + /> +
+
+ 0 + ? vsr.kitchenFurnishing + : ["N/A"] + } + /> +
+
+ 0 + ? vsr.livingRoomFurnishing + : ["N/A"] + } + /> +
+
+ 0 + ? vsr.diningRoomFurnishing + : ["N/A"] + } + /> +
+
+ 0 + ? vsr.otherFurnishing + : ["N/A"] + } + /> +
+
+ +
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/RequestedFurnishings/styles.module.css b/frontend/src/components/VSRIndividual/RequestedFurnishings/styles.module.css new file mode 100644 index 0000000..44c8480 --- /dev/null +++ b/frontend/src/components/VSRIndividual/RequestedFurnishings/styles.module.css @@ -0,0 +1,9 @@ +.row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.row:not(:last-child) { + padding-bottom: 32px; +} diff --git a/frontend/src/components/VSRIndividual/SingleDetail/index.tsx b/frontend/src/components/VSRIndividual/SingleDetail/index.tsx new file mode 100644 index 0000000..cb0d4e3 --- /dev/null +++ b/frontend/src/components/VSRIndividual/SingleDetail/index.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import styles from "src/components/VSRIndividual/SingleDetail/styles.module.css"; + +export interface SingleDetailProps { + title: string; + value: string | number | number[]; + valueFontSize?: string; + className?: string; +} + +export function SingleDetail({ title, value, valueFontSize, className }: SingleDetailProps) { + const valueStyle = { + fontSize: valueFontSize, // Use the passed font size or default to CSS class + }; + + const email = ( + + {value} + + ); + + const date = ( +
+
+ {typeof value === "string" ? value.substring(0, 11) : value} +
+
+ {typeof value === "string" ? value.substring(10) : value} +
+
+ ); + + const basic = ( +
+ {value} +
+ ); + + const noValue =
N/A
; + + return ( +
+
+
{title}
+ {typeof value === "string" && value.includes("[") + ? date + : typeof value === "string" && value.includes("@") + ? email + : typeof value === "string" && value.includes("N/A") + ? noValue + : basic} +
+
+ ); +} diff --git a/frontend/src/components/VSRIndividual/SingleDetail/styles.module.css b/frontend/src/components/VSRIndividual/SingleDetail/styles.module.css new file mode 100644 index 0000000..a0ba4be --- /dev/null +++ b/frontend/src/components/VSRIndividual/SingleDetail/styles.module.css @@ -0,0 +1,55 @@ +.items { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.title { + color: rgba(35, 34, 32, 0.55); + font-family: var(--font-open-sans); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 10px; + white-space: nowrap; +} + +.value { + color: #222; + font-family: var(--font-open-sans); + font-style: normal; + font-weight: 400; + line-height: normal; + padding-bottom: 6px; +} + +.row { + display: flex; + flex-direction: row; + color: #222; + font-family: var(--font-open-sans); + font-style: normal; + font-weight: 400; + line-height: normal; + flex-wrap: wrap; +} + +.time { + color: #818181; + white-space: pre; +} + +.email { + color: #4274f4; + font-family: var(--font-open-sans); + font-size: 16px; + text-decoration: underline; +} + +.noValue { + color: #818181; + font-family: var(--font-open-sans); + font-size: 16px; + font-style: italic; +} diff --git a/frontend/src/components/VSRIndividual/VSRIndividualAccordion/index.tsx b/frontend/src/components/VSRIndividual/VSRIndividualAccordion/index.tsx new file mode 100644 index 0000000..1d5872c --- /dev/null +++ b/frontend/src/components/VSRIndividual/VSRIndividualAccordion/index.tsx @@ -0,0 +1,62 @@ +import React, { ReactNode, useEffect, useState } from "react"; +import styles from "@/components/VSRIndividual/VSRIndividualAccordion/styles.module.css"; +import Accordion from "@mui/material/Accordion"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import Typography from "@mui/material/Typography"; +import Image from "next/image"; + +export interface VSRIndividualAccordionProps { + title: string; + permanentlyExpanded: boolean; + children: ReactNode; +} + +export const VSRIndividualAccordion = ({ + title, + permanentlyExpanded, + children, +}: VSRIndividualAccordionProps) => { + const [expanded, setExpanded] = useState(permanentlyExpanded); + + useEffect(() => { + setExpanded(permanentlyExpanded); + }, [permanentlyExpanded]); + + return ( +
+ setExpanded(isExpanded || permanentlyExpanded)} + sx={{ + paddingTop: "6px", + "&.Mui-expanded": { + paddingTop: "0px", + }, + }} + > + + ) + } + aria-controls="panel1-content" + id="military-background-header" + sx={{ + ...(expanded && { + borderBottom: "1px solid rgba(214, 214, 214)", // Custom line style + marginBottom: -1, // Adjust as needed + }), + }} + > + {title} + + +
{children}
+
+
+
+ ); +}; diff --git a/frontend/src/components/VSRIndividual/VSRIndividualAccordion/styles.module.css b/frontend/src/components/VSRIndividual/VSRIndividualAccordion/styles.module.css new file mode 100644 index 0000000..9f221b8 --- /dev/null +++ b/frontend/src/components/VSRIndividual/VSRIndividualAccordion/styles.module.css @@ -0,0 +1,36 @@ +.accordion { + display: inline-block; + background-color: hsl(0, 0%, 100%); + width: 100%; + border-radius: 6px; + box-shadow: none; + padding: 8px 6px; +} + +.title { + font-family: var(--font-title); + color: var(--Primary-Background-Dark, #232220); + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.title { + font-family: var(--font-title); + color: var(--Primary-Background-Dark, #232220); + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: normal; + padding-left: 6px; +} + +.details { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-top: 15px; + padding-left: 6px; + padding-right: 6px; +} diff --git a/frontend/src/components/VSRIndividual/VeteranTag/index.tsx b/frontend/src/components/VSRIndividual/VeteranTag/index.tsx new file mode 100644 index 0000000..fa6a3b7 --- /dev/null +++ b/frontend/src/components/VSRIndividual/VeteranTag/index.tsx @@ -0,0 +1,14 @@ +import styles from "src/components/VSRIndividual/VeteranTag/styles.module.css"; +import { type VSR } from "@/api/VSRs"; + +export interface AdditionalInfoProps { + vsr: VSR; +} + +export function VeteranTag({ vsr }: AdditionalInfoProps) { + return ( +
+ {vsr.name && vsr.name.length > 0 ? {vsr.name} : } +
+ ); +} diff --git a/frontend/src/components/VSRIndividual/VeteranTag/styles.module.css b/frontend/src/components/VSRIndividual/VeteranTag/styles.module.css new file mode 100644 index 0000000..9b521b5 --- /dev/null +++ b/frontend/src/components/VSRIndividual/VeteranTag/styles.module.css @@ -0,0 +1,8 @@ +.items span { + display: inline-block; + color: var(--Accent-Blue-2, #04183b); + font-family: "Lora"; + font-size: 40px; + font-weight: 700; + line-height: 40px; +} diff --git a/frontend/src/components/VSRIndividual/index.ts b/frontend/src/components/VSRIndividual/index.ts new file mode 100644 index 0000000..6b16db0 --- /dev/null +++ b/frontend/src/components/VSRIndividual/index.ts @@ -0,0 +1,12 @@ +export { HeaderBar } from "./HeaderBar"; +export { Page } from "./Page"; +export { VeteranTag } from "./VeteranTag"; +export { CaseDetails } from "./CaseDetails"; +export { ContactInfo } from "./ContactInfo"; +export { PersonalInformation } from "./PersonalInformation"; +export { MilitaryBackground } from "./MilitaryBackground"; +export { AdditionalInfo } from "./AdditionalInfo"; +export { RequestedFurnishings } from "./RequestedFurnishings"; +export { SingleDetail } from "./SingleDetail"; +export { DropdownDetail } from "./DropdownDetail"; +export { ListDetail } from "./ListDetail";