From 354efb8768fdd8110b1ee7659f9c25873c43317e Mon Sep 17 00:00:00 2001 From: Ethan Ferguson Date: Mon, 9 Oct 2023 15:48:40 -0400 Subject: [PATCH 1/6] Added API fetching utility functions --- package-lock.json | 33 +++++++++++- package.json | 13 ++++- src/API/API.ts | 134 ++++++++++++++++++++++++++++++++++++++++++++++ src/API/Types.ts | 4 ++ 4 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 src/API/API.ts create mode 100644 src/API/Types.ts diff --git a/package-lock.json b/package-lock.json index fd4b6ce..cf90d1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,10 +36,12 @@ "react-router-dom": "^6.16.0", "react-scripts": "^5.0.1", "reactstrap": "^9.2.0", + "react-toastify": "^9.1.3", "typescript": "^4" }, "devDependencies": { - "sass": "^1.58.1" + "sass": "^1.58.1", + "typescript-eslint-language-service": "^5.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -6387,9 +6389,15 @@ } }, "node_modules/clsx": { +<<<<<<< HEAD "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", +======= + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", +>>>>>>> 6a96823 (Added API fetching utility functions) "engines": { "node": ">=6" } @@ -14967,6 +14975,18 @@ } } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -16961,6 +16981,17 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-eslint-language-service": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/typescript-eslint-language-service/-/typescript-eslint-language-service-5.0.5.tgz", + "integrity": "sha512-b7gWXpwSTqMVKpPX3WttNZEyVAMKs/2jsHKF79H+qaD6mjzCyU5jboJe/lOZgLJD+QRsXCr0GjIVxvl5kI1NMw==", + "dev": true, + "peerDependencies": { + "@typescript-eslint/parser": ">= 5.0.0", + "eslint": ">= 8.0.0", + "typescript": ">= 4.0.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 4dba4ab..24bad49 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,9 @@ "react-dom": "^18.2.0", "react-router": "^6.8.1", "react-router-dom": "^6.16.0", - "react-scripts": "^5.0.1", + "react-scripts": "^5.0.", "reactstrap": "^9.2.0", + "react-toastify": "^9.1.3", "typescript": "^4" }, "scripts": { @@ -58,6 +59,14 @@ ] }, "devDependencies": { - "sass": "^1.58.1" + "sass": "^1.58.1", + "typescript-eslint-language-service": "^5.0.5" + }, + "compilerOptions": { + "plugins": [ + { + "name": "typescript-eslint-language-service" + } + ] } } diff --git a/src/API/API.ts b/src/API/API.ts new file mode 100644 index 0000000..b353dd8 --- /dev/null +++ b/src/API/API.ts @@ -0,0 +1,134 @@ +import { toast } from "react-toastify"; +import { ErrorInfo } from "./Types"; + +export const baseURL: string = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`; + + +// Can't name a method `get`, so ... +export const getJSON = (url: string, params?: any): Promise => { + if (!url.startsWith("/")) { + url = "/" + url; + } + let qm = url.includes("?"); + for (const key in params || {}) { + if (!qm) { + url += "?"; + qm = true; + } else { + url += "&"; + } + url += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + return fetch(baseURL + url) + .then(body => { + if (body.status !== 200) { + throw body; + } + return body.json() + }) + .then(e => e as T); +} + +export const post = (url: string, body?: any, params?: any): Promise => { + if (!url.startsWith("/")) { + url = "/" + url; + } + let qm = url.includes("?"); + for (const key in params || {}) { + if (!qm) { + url += "?"; + qm = true; + } else { + url += "&"; + } + url += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + let describe: RequestInit = { + method: "POST", + } + if (body) { + describe = { + ...describe, + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json" + } + } + } + return fetch(baseURL + url, describe) + .then(response => { + if (response.status !== 200) { + throw response; + } + return response; + }); +} + +export const patch = (url: string, body?: any, params?: any): Promise => { + if (!url.startsWith("/")) { + url = "/" + url; + } + let qm = url.includes("?"); + for (const key in params || {}) { + if (!qm) { + url += "?"; + qm = true; + } else { + url += "&"; + } + url += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + let describe: RequestInit = { + method: "PATCH", + } + if (body) { + describe = { + ...describe, + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json" + } + } + } + return fetch(baseURL + url, describe) + .then(response => { + if (response.status !== 200) { + throw response; + } + return response; + }); +} + +//can't name function `delete` 😞 +export const apiDelete = (url: string, params?: any): Promise => { + if (!url.startsWith("/")) { + url = "/" + url; + } + let qm = url.includes("?"); + for (const key in params || {}) { + if (!qm) { + url += "?"; + qm = true; + } else { + url += "&"; + } + url += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + return fetch(baseURL + url, { + method: "DELETE" + }) + .then(response => { + if (response.status !== 200) { + throw response; + } + return response; + }); +} + +export const toastError = (message: string) => (resp: any) => { + resp.json() + .then((error: ErrorInfo) => + toast.error(`${message}: ${error.message}`, { + theme: "colored" + })) +} diff --git a/src/API/Types.ts b/src/API/Types.ts new file mode 100644 index 0000000..4cf072f --- /dev/null +++ b/src/API/Types.ts @@ -0,0 +1,4 @@ +export interface ErrorInfo { + message: string, + stackTrace: string +} From 8ff11e82aab75c283e6388ded18f200ed0154114 Mon Sep 17 00:00:00 2001 From: Ethan Ferguson Date: Mon, 9 Oct 2023 17:00:43 -0400 Subject: [PATCH 2/6] Added tsfmt.json --- tsfmt.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tsfmt.json diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 0000000..1f66fe0 --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,8 @@ +{ + "indentSize": 4, + "tabSize": 4, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} + From 131348c0a758eb2b8710f576f8d6e53205f379fb Mon Sep 17 00:00:00 2001 From: Ethan Ferguson Date: Wed, 11 Oct 2023 13:44:42 -0400 Subject: [PATCH 3/6] Added Submit Attendance pages --- package-lock.json | 89 ++++++++++++++++++++++++++++++++ package.json | 4 +- src/API/Types.ts | 8 +++ src/App.tsx | 31 ++++++----- src/components/UserSearch.tsx | 83 +++++++++++++++++++++++++++++ src/pages/SubmitDirectorship.tsx | 89 ++++++++++++++++++++++++++++++++ src/pages/SubmitSeminar.tsx | 81 +++++++++++++++++++++++++++++ 7 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 src/components/UserSearch.tsx create mode 100644 src/pages/SubmitDirectorship.tsx create mode 100644 src/pages/SubmitSeminar.tsx diff --git a/package-lock.json b/package-lock.json index cf90d1e..b5c9fd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,10 @@ "react-scripts": "^5.0.1", "reactstrap": "^9.2.0", "react-toastify": "^9.1.3", +<<<<<<< HEAD +======= + "reactstrap": "^9.2.0", +>>>>>>> c4979aa (Added Submit Attendance pages) "typescript": "^4" }, "devDependencies": { @@ -2476,6 +2480,22 @@ "stylis": "4.2.0" } }, +<<<<<<< HEAD +======= + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@emotion/cache": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", @@ -2732,6 +2752,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", "hasInstallScript": true, +<<<<<<< HEAD "dependencies": { "@fortawesome/fontawesome-common-types": "6.4.2" }, @@ -2756,6 +2777,9 @@ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz", "integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==", "hasInstallScript": true, +======= + "peer": true, +>>>>>>> c4979aa (Added Submit Attendance pages) "dependencies": { "@fortawesome/fontawesome-common-types": "6.4.2" }, @@ -3666,6 +3690,17 @@ } } }, +<<<<<<< HEAD +======= + "node_modules/@mui/base/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@mui/core-downloads-tracker": { "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.13.tgz", @@ -3740,6 +3775,17 @@ } } }, +<<<<<<< HEAD +======= + "node_modules/@mui/joy/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@mui/material": { "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.13.tgz", @@ -3784,6 +3830,22 @@ } } }, +<<<<<<< HEAD +======= + "node_modules/@mui/material/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@mui/private-theming": { "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.13.tgz", @@ -3880,6 +3942,17 @@ } } }, +<<<<<<< HEAD +======= + "node_modules/@mui/system/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@mui/types": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.6.tgz", @@ -3920,6 +3993,14 @@ } } }, +<<<<<<< HEAD +======= + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, +>>>>>>> c4979aa (Added Submit Attendance pages) "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4748,6 +4829,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz", + "integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", diff --git a/package.json b/package.json index 24bad49..4d0ccdc 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@mui/icons-material": "^5.14.9", "@mui/joy": "^5.0.0-beta.9", "@mui/material": "^5.14.12", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -29,7 +31,7 @@ "react-dom": "^18.2.0", "react-router": "^6.8.1", "react-router-dom": "^6.16.0", - "react-scripts": "^5.0.", + "react-scripts": "^5.0.1", "reactstrap": "^9.2.0", "react-toastify": "^9.1.3", "typescript": "^4" diff --git a/src/API/Types.ts b/src/API/Types.ts index 4cf072f..e2fbfc0 100644 --- a/src/API/Types.ts +++ b/src/API/Types.ts @@ -2,3 +2,11 @@ export interface ErrorInfo { message: string, stackTrace: string } + +export const DirectorshipTypes = ["Chairman", "Ad-Hoc", "Evaluations", "Financial", "Research and Development", "House Improvements", "OpComm", "History", "Social", "Public Relations"] as const; +export type DirectorshipType = typeof DirectorshipTypes[number] + +export interface UserInfo { + username: string, + fullName: string, +} diff --git a/src/App.tsx b/src/App.tsx index 71f0f55..9a6f648 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,33 @@ import React from 'react' import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' import Home from './pages/Home' +import AttendanceHistory from './pages/AttendanceHistory' +import SubmitDirectorship from './pages/SubmitDirectorship' import PageContainer from './containers/PageContainer' import 'csh-material-bootstrap/dist/csh-material-bootstrap.css' import NotFound from './pages/NotFound' import { request } from 'http' +import SubmitSeminar from './pages/SubmitSeminar' + type Props = { - rerouteHomeOn404?: boolean + rerouteHomeOn404?: boolean } const App: React.FC = ({ rerouteHomeOn404 = null }) => { - return ( - - - - } /> - } /> - : } /> - - - - ) + return ( + + + + } /> + } /> + } /> + } /> + } /> + : } /> + + + + ) } export default App diff --git a/src/components/UserSearch.tsx b/src/components/UserSearch.tsx new file mode 100644 index 0000000..dfafb77 --- /dev/null +++ b/src/components/UserSearch.tsx @@ -0,0 +1,83 @@ +import { faX } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Badge, Button, Container, Input } from "reactstrap"; +import { getJSON, toastError } from "../API/API"; +import { UserInfo } from "../API/Types"; + +const UserSearch: React.FC<{ + users: [UserInfo[], Dispatch>] +}> = ({ users: [users, setUsers] }) => { + + const [inputText, setInputText] = useState(""); + + const [results, setResults] = useState([]); + + useEffect(() => { + if (!inputText || inputText.trim().length < 2) { + setResults([]); + return; + } + getJSON("/api/users/search", { + query: inputText + }) + .then(e => e.slice(0, 10)) + .then(setResults) + .catch(toastError("Unable to search Users")); + }, [inputText]); + + const addUser = (user: UserInfo) => { + if (users.filter(u => u.username === user.username).length === 0) { + setUsers(old => [...old, user]); + } + setInputText(""); + } + + const removeUser = (user: UserInfo) => { + setUsers(old => old.filter(u => u.username !== user.username)); + } + + return ( + + { + users.map((user, index) => +

+ + {user.fullName} ({user.username}) + + +

+ ) + } + setInputText(e.target.value)} + /> + +
+ ) +} + +export default UserSearch; diff --git a/src/pages/SubmitDirectorship.tsx b/src/pages/SubmitDirectorship.tsx new file mode 100644 index 0000000..5d7dcf9 --- /dev/null +++ b/src/pages/SubmitDirectorship.tsx @@ -0,0 +1,89 @@ +import { post, toastError } from '../API/API'; +import { UserInfo } from "../API/Types"; +import { Button, Card, CardBody, CardHeader, Container, Form, FormGroup, Input } from "reactstrap"; +import { useState } from 'react'; +import { DirectorshipType, DirectorshipTypes } from '../API/Types'; +import UserSearch from "../components/UserSearch"; +import { toast } from 'react-toastify'; + +const SubmitDirectorship = () => { + + const [meetingType, setMeetingType] = useState(undefined); + + const [meetingDate, setMeetingDate] = useState(new Date()); + + const [attendees, setAttendees] = useState([]); + + const getDateAsStr = () => meetingDate.toISOString().split("T")[0] + + const canSubmit = () => meetingType !== undefined && attendees.length > 0 + + const submit = () => { + post("/attendance/directorship", { + date: meetingDate.toISOString(), + members: attendees.map(a => a.username), + type: meetingType as string, + frosh: [] //TODO: implement frosh + }) + .then(() => toast.success("Submitted successfully!", { theme: "colored" })) + .then(() => setTimeout(() => window.location.assign("/"), 3000)) + .catch(toastError("Unable to submit Attendance")) + } + + return ( + + +

Submit Directorship Attendance

+
+
+ + + Meeting Type + + setMeetingType(e.target.value as DirectorshipType)}> + + { + DirectorshipTypes.map((dt, index) => + + ) + } + + + + + + + Date + + setMeetingDate(new Date(e.target.value))} + /> + + + + + + Attendees + + + + + + + + +
+
+ ) +} + +export default SubmitDirectorship diff --git a/src/pages/SubmitSeminar.tsx b/src/pages/SubmitSeminar.tsx new file mode 100644 index 0000000..afd8d73 --- /dev/null +++ b/src/pages/SubmitSeminar.tsx @@ -0,0 +1,81 @@ +import { post, toastError } from '../API/API'; +import { UserInfo } from "../API/Types"; +import { Button, Card, CardBody, CardHeader, Container, Form, FormGroup, Input } from "reactstrap"; +import { useState } from 'react'; +import UserSearch from "../components/UserSearch"; +import { toast } from 'react-toastify'; + +const SubmitSeminar = () => { + + const [meetingName, setMeetingName] = useState(""); + + const [meetingDate, setMeetingDate] = useState(new Date()); + + const [attendees, setAttendees] = useState([]); + + const getDateAsStr = () => meetingDate.toISOString().split("T")[0] + + const canSubmit = () => meetingName.length > 1 && attendees.length > 0 + + const submit = () => { + post("/attendance/seminar", { + date: meetingDate.toISOString(), + members: attendees.map(a => a.username), + name: meetingName, + frosh: [] //TODO: implement frosh + }) + .then(() => toast.success("Submitted successfully!", { theme: "colored" })) + .then(() => setTimeout(() => window.location.assign("/"), 3000)) + .catch(toastError("Unable to submit Attendance")) + } + + return ( + + +

Submit Technical Seminar Attendance

+
+
+ + + Seminar Name + + setMeetingName(e.target.value)} /> + + + + + + Date + + setMeetingDate(new Date(e.target.value))} + /> + + + + + + Attendees + + + + + + + + +
+
+ ) +} + +export default SubmitSeminar From cd81aba8b21364aae9b7dd6a933abbb2639f7a98 Mon Sep 17 00:00:00 2001 From: Ethan Ferguson Date: Sun, 15 Oct 2023 19:07:35 -0400 Subject: [PATCH 4/6] Added Intro Evals Slideshow --- package-lock.json | 20 ++++- package.json | 6 +- src/API/Types.ts | 32 ++++++++ src/App.tsx | 6 +- src/containers/PageContainer.tsx | 7 +- src/pages/Slideshow/BatchSlide.tsx | 65 ++++++++++++++++ src/pages/Slideshow/IntroEvalsSlideshow.tsx | 70 +++++++++++++++++ src/pages/Slideshow/NumberBox.tsx | 19 +++++ src/pages/Slideshow/Slide.tsx | 83 +++++++++++++++++++++ src/pages/Slideshow/index.css | 47 ++++++++++++ 10 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 src/pages/Slideshow/BatchSlide.tsx create mode 100644 src/pages/Slideshow/IntroEvalsSlideshow.tsx create mode 100644 src/pages/Slideshow/NumberBox.tsx create mode 100644 src/pages/Slideshow/Slide.tsx create mode 100644 src/pages/Slideshow/index.css diff --git a/package-lock.json b/package-lock.json index b5c9fd8..bcd37a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,13 +37,11 @@ "react-scripts": "^5.0.1", "reactstrap": "^9.2.0", "react-toastify": "^9.1.3", -<<<<<<< HEAD -======= - "reactstrap": "^9.2.0", ->>>>>>> c4979aa (Added Submit Attendance pages) + "reveal.js": "^4.6.1", "typescript": "^4" }, "devDependencies": { + "@types/reveal.js": "^4.4.3", "sass": "^1.58.1", "typescript-eslint-language-service": "^5.0.5" } @@ -4850,6 +4848,12 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "node_modules/@types/reveal.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@types/reveal.js/-/reveal.js-4.4.3.tgz", + "integrity": "sha512-nI4Kp7x5uNn5op2IHNM0L1dLONejRuph2Rcn6KVk3jF1NOLpxW8xZciXX0V0sBkVAlWTn6JmW88EaeFkkWDYig==", + "dev": true + }, "node_modules/@types/scheduler": { "version": "0.16.4", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", @@ -15432,6 +15436,14 @@ "node": ">=0.10.0" } }, + "node_modules/reveal.js": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-4.6.1.tgz", + "integrity": "sha512-1CW0auaXNPmwmvQ7TwpszwVxMi2Xr5cTS3J3EBC/HHgbPF32Dn7aiu/LKWDOGjMbaDwKQiGmfqcoGQ74HUHCMw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 4d0ccdc..fce7ff4 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,6 @@ "@mui/icons-material": "^5.14.9", "@mui/joy": "^5.0.0-beta.9", "@mui/material": "^5.14.12", - "@fortawesome/free-solid-svg-icons": "^6.4.2", - "@fortawesome/react-fontawesome": "^0.2.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -32,8 +30,9 @@ "react-router": "^6.8.1", "react-router-dom": "^6.16.0", "react-scripts": "^5.0.1", - "reactstrap": "^9.2.0", "react-toastify": "^9.1.3", + "reactstrap": "^9.2.0", + "reveal.js": "^4.6.1", "typescript": "^4" }, "scripts": { @@ -61,6 +60,7 @@ ] }, "devDependencies": { + "@types/reveal.js": "^4.4.3", "sass": "^1.58.1", "typescript-eslint-language-service": "^5.0.5" }, diff --git a/src/API/Types.ts b/src/API/Types.ts index e2fbfc0..2ebdeba 100644 --- a/src/API/Types.ts +++ b/src/API/Types.ts @@ -10,3 +10,35 @@ export interface UserInfo { username: string, fullName: string, } + +export interface DirectorshipAttendanceRecord { + approved: boolean, + committe: DirectorshipType, + frosh: number[], + members: string[], + timestamp: Date +} + +export interface SeminarAttendanceRecord { + approved: boolean, + name: string, + frosh: number[], + members: string[], + timestamp: Date, +} + +export interface IntroEvalsSummary { + name: string, + uid: string | null, + seminars: number, + directorships: number, + missed_hms: number, + signatures: number, + max_signatures: number, +} + +export interface IntroEvalsForm { + uid: string, + social_events: string | null, + comments: string | null, +} diff --git a/src/App.tsx b/src/App.tsx index 9a6f648..cf2b647 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,20 @@ import React from 'react' import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' import Home from './pages/Home' -import AttendanceHistory from './pages/AttendanceHistory' +import AttendanceHistory from './pages/Attendance/AttendanceHistory' import SubmitDirectorship from './pages/SubmitDirectorship' import PageContainer from './containers/PageContainer' import 'csh-material-bootstrap/dist/csh-material-bootstrap.css' import NotFound from './pages/NotFound' -import { request } from 'http' import SubmitSeminar from './pages/SubmitSeminar' +import IntroEvalsSlideshow from './pages/Slideshow/IntroEvalsSlideshow' type Props = { rerouteHomeOn404?: boolean } const App: React.FC = ({ rerouteHomeOn404 = null }) => { + return ( @@ -23,6 +24,7 @@ const App: React.FC = ({ rerouteHomeOn404 = null }) => { } /> } /> } /> + } /> : } /> diff --git a/src/containers/PageContainer.tsx b/src/containers/PageContainer.tsx index e00dedf..2ba84d1 100644 --- a/src/containers/PageContainer.tsx +++ b/src/containers/PageContainer.tsx @@ -7,14 +7,15 @@ type Props = { } export const PageContainer: React.FC = ({ children }) => { + return (
- - + + {!window.location.href.includes("slideshow") && } {children}
) } -export default PageContainer \ No newline at end of file +export default PageContainer diff --git a/src/pages/Slideshow/BatchSlide.tsx b/src/pages/Slideshow/BatchSlide.tsx new file mode 100644 index 0000000..529af1f --- /dev/null +++ b/src/pages/Slideshow/BatchSlide.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { Button, Col, Container, Row } from "reactstrap" + +const BatchSlide = (props: { + name: string, + names: string[], + onPassFail: (passed: boolean) => void +}) => { + + const [passed, setPassed] = useState(null); + + return ( +
+ + + +

{props.name}

+ +
+ + { + props.names.map((f, i) => + +

{f}

+ + ) + } +
+ { + passed == null ? + + + + + + : +

{passed}

+ } +
+
+ ) +} + +export default BatchSlide diff --git a/src/pages/Slideshow/IntroEvalsSlideshow.tsx b/src/pages/Slideshow/IntroEvalsSlideshow.tsx new file mode 100644 index 0000000..bcee745 --- /dev/null +++ b/src/pages/Slideshow/IntroEvalsSlideshow.tsx @@ -0,0 +1,70 @@ +import { useEffect, useReducer, useState } from "react" +import Reveal from "reveal.js"; +import Slide from "./Slide" +import 'reveal.js/dist/reset.css' +import 'reveal.js/dist/reveal.css' +import 'reveal.js/dist/theme/white.css' +import './index.css' +import { IntroEvalsSummary } from "../../API/Types"; +import { getJSON, toastError } from "../../API/API"; +import { Container } from "reactstrap"; +import BatchSlide from "./BatchSlide"; + +const IntroEvalsSlideshow = () => { + + const initSlides = () => { + let slides = new Reveal({ + backgroundTransition: 'slide', + transition: 'slide' + }); + slides.initialize(); + } + + const [frosh, setFrosh] = useState([]); + + useEffect(() => { + getJSON("/api/evals/intro") + .then(setFrosh).then(initSlides) + .catch(toastError("Unable to fetch Intro Evals data")); + }, []) + + interface Batch { + name: string, + names: string[], + } + + const batches: Batch[] = []; + + return ( +
+
+
+ +

Intro Evals Slideshow

+ {/* placeholder, because slideshow doesn't work unless at least one slide is present from the beginning*/} +
+
+ { + batches.map((b, i) => + setFrosh(frosh.filter(f => !b.names.includes(f.name)))} /> + ) + } + + { + frosh.sort((a, b) => a.name.localeCompare(b.name)).map((f, i) => + ) + } +
+
+ + ) +} + +export default IntroEvalsSlideshow diff --git a/src/pages/Slideshow/NumberBox.tsx b/src/pages/Slideshow/NumberBox.tsx new file mode 100644 index 0000000..1348b33 --- /dev/null +++ b/src/pages/Slideshow/NumberBox.tsx @@ -0,0 +1,19 @@ +import { Card, CardText, Container, Row } from "reactstrap" + +const NumberBox = (props: { text: string, subtext: string, success: boolean }) => { + + return ( + + + +

{props.text}

+
+ +

{props.subtext}

+
+
+
+ ) +} + +export default NumberBox diff --git a/src/pages/Slideshow/Slide.tsx b/src/pages/Slideshow/Slide.tsx new file mode 100644 index 0000000..ecbfa0f --- /dev/null +++ b/src/pages/Slideshow/Slide.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react" +import { Button, Card, CardText, Col, Container, Row } from "reactstrap" +import { getJSON, toastError } from "../../API/API"; +import { IntroEvalsForm } from "../../API/Types"; +import NumberBox from "./NumberBox" + +const Slide = (props: { + name: string, + uid: string | null, + packet: number, // number from 0 - 100 + hm_absences: number, + directorships: number, + seminars: number, +}) => { + + const [comments, setComments] = useState(""); + const [socials, setSocials] = useState(""); + + useEffect(() => { + if (props.uid !== null) { + getJSON(`/api/forms/intro/${props.uid}`) + .then(e => { + setComments(comments + (e[0].comments || "")); + setSocials(socials + (e[0].social_events || "")); + }) + .catch(toastError(`Unable to fetch Data for ${props.uid}`)) + } + }, []); + + return ( +
+
+ + + +

{props.name}

+ +
+ + + = 60} /> + + + + + + = 6} /> + + + = 2} /> + + + + + + +
+
+
+ + + +

Comments

+ + +

Social Events

+ +
+ + +

{comments}

+ + +

{socials}

+ +
+
+
+
+ ) +} + +export default Slide diff --git a/src/pages/Slideshow/index.css b/src/pages/Slideshow/index.css new file mode 100644 index 0000000..76a5510 --- /dev/null +++ b/src/pages/Slideshow/index.css @@ -0,0 +1,47 @@ +/* for slide transitions */ +[hidden] { + display: inherit !important; +} + +.number-box{ + border-width: 10px !important; + border-radius: 20px !important; +} + +.pf-button { + border-width: 6px !important; + border-radius: 35px !important; +} + +.number-box-text { + font-size: 6em; + text-align: center; + white-space: nowrap; +} + +.number-box-subtext { + justify-content: flex-end; + vertical-align: bottom; + height: auto; + font-size: 16pt; +} + +div.main { + margin-left: 0px; + margin-right: 0px; +} + +.batch-name { + font-size: 12pt; + white-space: nowrap; +} + +.disband-gray { + border-color: lightgray !important; + color: gray !important; +} + +.soc-com { + font-size: 10pt; + white-space: pre-line; +} From 30a2c923daeef56be809bdaff8d2a96750fd3263 Mon Sep 17 00:00:00 2001 From: Ethan Ferguson Date: Sun, 15 Oct 2023 21:55:57 -0400 Subject: [PATCH 5/6] Added Batch API endpoint --- src/API/Types.ts | 7 ++ src/pages/Slideshow/BatchSlide.tsx | 112 +++++++++++--------- src/pages/Slideshow/IntroEvalsSlideshow.tsx | 59 ++++++++--- 3 files changed, 116 insertions(+), 62 deletions(-) diff --git a/src/API/Types.ts b/src/API/Types.ts index 2ebdeba..613751a 100644 --- a/src/API/Types.ts +++ b/src/API/Types.ts @@ -42,3 +42,10 @@ export interface IntroEvalsForm { social_events: string | null, comments: string | null, } + +export interface Batch { + name: string, + members: string[], + creator: string, + conditions: string[] +} diff --git a/src/pages/Slideshow/BatchSlide.tsx b/src/pages/Slideshow/BatchSlide.tsx index 529af1f..fafa111 100644 --- a/src/pages/Slideshow/BatchSlide.tsx +++ b/src/pages/Slideshow/BatchSlide.tsx @@ -1,63 +1,79 @@ import { useState } from "react"; import { Button, Col, Container, Row } from "reactstrap" +import { Batch } from "../../API/Types"; const BatchSlide = (props: { - name: string, - names: string[], + batch: Batch, onPassFail: (passed: boolean) => void }) => { const [passed, setPassed] = useState(null); return ( -
- - - -

{props.name}

- -
- +
+
+ + + +

{props.batch.name}

+ +
+ + { + props.batch.members.map((f, i) => + +

{f}

+ + ) + } +
{ - props.names.map((f, i) => - -

{f}

- - ) + passed == null ? + + + + + + : +

{passed}

} - - { - passed == null ? - - - - - - : -

{passed}

- } -
+ +
+
+ + + +

Creator: {props.batch.creator}

+ +
+ + + {props.batch.conditions.map(c =>

{c}

)} + +
+
+
) } diff --git a/src/pages/Slideshow/IntroEvalsSlideshow.tsx b/src/pages/Slideshow/IntroEvalsSlideshow.tsx index bcee745..4b1673e 100644 --- a/src/pages/Slideshow/IntroEvalsSlideshow.tsx +++ b/src/pages/Slideshow/IntroEvalsSlideshow.tsx @@ -5,10 +5,11 @@ import 'reveal.js/dist/reset.css' import 'reveal.js/dist/reveal.css' import 'reveal.js/dist/theme/white.css' import './index.css' -import { IntroEvalsSummary } from "../../API/Types"; +import { Batch, IntroEvalsSummary } from "../../API/Types"; import { getJSON, toastError } from "../../API/API"; -import { Container } from "reactstrap"; +import { Col, Container, Row } from "reactstrap"; import BatchSlide from "./BatchSlide"; +import NumberBox from "./NumberBox"; const IntroEvalsSlideshow = () => { @@ -20,38 +21,68 @@ const IntroEvalsSlideshow = () => { slides.initialize(); } + const [removedMembers, setRemovedMembers] = useState([]); + const [frosh, setFrosh] = useState([]); useEffect(() => { getJSON("/api/evals/intro") - .then(setFrosh).then(initSlides) + .then(setFrosh) + .then(initSlides) .catch(toastError("Unable to fetch Intro Evals data")); }, []) - interface Batch { - name: string, - names: string[], - } + const [batches, setBatches] = useState([]); + + useEffect(() => { + getJSON("/api/batch") + .then(e => setBatches(e.map(b => ({ + ...b, + members: b.members.map(m => m.split(",")[0]), + })))) + .catch(toastError("Unable to fetch Batches")) + }, []); - const batches: Batch[] = []; + const passFailBatch = (batch: Batch) => (pass: boolean) => { + setRemovedMembers([...removedMembers, ...batch.members]) + } return (
+ {/* placeholder, because slideshow doesn't work unless at least one slide is present from the beginning*/}
- -

Intro Evals Slideshow

- {/* placeholder, because slideshow doesn't work unless at least one slide is present from the beginning*/} -
+
+ +

Intro Evals Slideshow

+ + + + + + + + +
+
+
+ +

Hi :)

+
+
{ batches.map((b, i) => - setFrosh(frosh.filter(f => !b.names.includes(f.name)))} /> + ) } { - frosh.sort((a, b) => a.name.localeCompare(b.name)).map((f, i) => + frosh.filter(f => !removedMembers.includes(f.name)).sort((a, b) => a.name.localeCompare(b.name)).map((f, i) => Date: Thu, 19 Oct 2023 15:01:50 -0400 Subject: [PATCH 6/6] Fixed invalid date bug --- src/pages/SubmitDirectorship.tsx | 15 ++++++++------- src/pages/SubmitSeminar.tsx | 27 +++++++++++++++++---------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/pages/SubmitDirectorship.tsx b/src/pages/SubmitDirectorship.tsx index 5d7dcf9..facf220 100644 --- a/src/pages/SubmitDirectorship.tsx +++ b/src/pages/SubmitDirectorship.tsx @@ -10,17 +10,18 @@ const SubmitDirectorship = () => { const [meetingType, setMeetingType] = useState(undefined); - const [meetingDate, setMeetingDate] = useState(new Date()); + const [meetingDate, setMeetingDate] = useState(new Date().toISOString().split("T")[0]); const [attendees, setAttendees] = useState([]); - const getDateAsStr = () => meetingDate.toISOString().split("T")[0] - - const canSubmit = () => meetingType !== undefined && attendees.length > 0 + const canSubmit = () => + meetingType !== undefined + && attendees.length > 0 + && (d => d instanceof Date && !isNaN(d.getTime()))(new Date(meetingDate)) const submit = () => { post("/attendance/directorship", { - date: meetingDate.toISOString(), + date: meetingDate, members: attendees.map(a => a.username), type: meetingType as string, frosh: [] //TODO: implement frosh @@ -64,8 +65,8 @@ const SubmitDirectorship = () => { setMeetingDate(new Date(e.target.value))} + value={meetingDate} + onChange={e => setMeetingDate(e.target.value)} /> diff --git a/src/pages/SubmitSeminar.tsx b/src/pages/SubmitSeminar.tsx index afd8d73..db3ac9c 100644 --- a/src/pages/SubmitSeminar.tsx +++ b/src/pages/SubmitSeminar.tsx @@ -9,20 +9,27 @@ const SubmitSeminar = () => { const [meetingName, setMeetingName] = useState(""); - const [meetingDate, setMeetingDate] = useState(new Date()); + const [meetingDate, setMeetingDate] = useState(new Date().toISOString().split("T")[0]); - const [attendees, setAttendees] = useState([]); + const [attendees, setAttendees] = useState([ + { + username: "ethanf108", + fullName: "Ethan Ferguson", + } + ]); - const getDateAsStr = () => meetingDate.toISOString().split("T")[0] - - const canSubmit = () => meetingName.length > 1 && attendees.length > 0 + const canSubmit = () => + meetingName.length > 1 + && attendees.length > 0 + && (d => d instanceof Date && !isNaN(d.getTime()))(new Date(meetingDate)) const submit = () => { - post("/attendance/seminar", { - date: meetingDate.toISOString(), + post("/api/attendance/seminar", { + date: meetingDate, members: attendees.map(a => a.username), name: meetingName, - frosh: [] //TODO: implement frosh + frosh: [], //TODO: implement frosh + approved: false }) .then(() => toast.success("Submitted successfully!", { theme: "colored" })) .then(() => setTimeout(() => window.location.assign("/"), 3000)) @@ -56,8 +63,8 @@ const SubmitSeminar = () => { setMeetingDate(new Date(e.target.value))} + value={meetingDate} + onChange={e => setMeetingDate(e.target.value)} />