From 69e0f6578a124286db5f27b1f40a2786f023d677 Mon Sep 17 00:00:00 2001 From: Karandeep Lubana Date: Sat, 27 Jan 2024 19:21:14 -0500 Subject: [PATCH] IEEE-259 Add title and project description to admin page --- .../cart/CartErrorBox/CartErrorBox.tsx | 2 + .../cart/CartSummary/CartSummary.tsx | 21 ++- .../ProjectDescription.module.scss | 10 +- .../ProjectDescription/ProjectDescription.tsx | 155 +++++++++++------- .../ProjectDescriptionAlert.tsx | 24 +++ .../ProjectDescriptionDetail.tsx | 43 +++++ .../dashboard/frontend/src/constants.js | 3 +- .../src/pages/TeamDetail/TeamDetail.tsx | 2 + .../src/slices/event/teamDetailSlice.ts | 76 +++++++++ 9 files changed, 271 insertions(+), 65 deletions(-) create mode 100644 hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionAlert.tsx create mode 100644 hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionDetail.tsx diff --git a/hackathon_site/dashboard/frontend/src/components/cart/CartErrorBox/CartErrorBox.tsx b/hackathon_site/dashboard/frontend/src/components/cart/CartErrorBox/CartErrorBox.tsx index 3113830b1..410618f2e 100644 --- a/hackathon_site/dashboard/frontend/src/components/cart/CartErrorBox/CartErrorBox.tsx +++ b/hackathon_site/dashboard/frontend/src/components/cart/CartErrorBox/CartErrorBox.tsx @@ -7,6 +7,7 @@ import { maxTeamSize, minTeamSize } from "constants.js"; import AlertBox from "components/general/AlertBox/AlertBox"; import { Link } from "@material-ui/core"; import DateRestrictionAlert from "components/general/DateRestrictionAlert/DateRestrictionAlert"; +import ProjectDescriptionAlert from "components/teamDetail/ProjectDescription/ProjectDescriptionAlert"; const CartErrorBox = () => { const cartQuantity = useSelector(cartTotalSelector); @@ -22,6 +23,7 @@ const CartErrorBox = () => { + {orderSubmissionError && cartQuantity > 0 && ( { const cartQuantity = useSelector(cartTotalSelector); const cartOrderLoading = useSelector(isLoadingSelector); const teamSize = useSelector(teamSizeSelector); + const projectDescription = useSelector(projectDescriptionSelector); const teamSizeValid = teamSize >= minTeamSize && teamSize <= maxTeamSize; const dispatch = useDispatch(); const onSubmit = () => { if (cartQuantity > 0) { - dispatch(submitOrder()); + if ( + projectDescription && + projectDescription.length < MIN_DESCRIPTION_LENGTH + ) { + dispatch( + displaySnackbar({ + message: "Please provide a more detailed project description.", + options: { + variant: "error", + }, + }) + ); + } else { + dispatch(submitOrder()); + } } }; const currentDateTime = new Date(); @@ -37,6 +54,8 @@ const CartSummary = () => { currentDateTime < hardwareSignOutStartDate || currentDateTime > hardwareSignOutEndDate; + const MIN_DESCRIPTION_LENGTH = 50; + return ( diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.module.scss b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.module.scss index f7ca93b18..99771ee42 100644 --- a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.module.scss +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.module.scss @@ -1,5 +1,5 @@ .formTextField { - margin: 20px 5px 0 0; + margin: 10px 5px 0 0; } .actionBtn { @@ -10,3 +10,11 @@ width: 120px; margin-right: 10px; } + +.projectDescriptionDetail { + padding: 10px; +} + +.title { + margin-top: 30px; +} diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.tsx b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.tsx index a6b596831..19f6de655 100644 --- a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.tsx +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescription.tsx @@ -1,10 +1,17 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import styles from "./ProjectDescription.module.scss"; import { Formik, Form, Field, FormikValues } from "formik"; import * as Yup from "yup"; -import { TextField, Button, Box, Grid } from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import { updateProjectDescription } from "slices/event/teamDetailSlice"; +import { TextField, Button, Box, Grid, Typography } from "@material-ui/core"; +import { useDispatch, useSelector } from "react-redux"; +import { + updateProjectDescription, + fetchInitialProjectDescription, + projectDescriptionSelector, + isTeamInfoLoadingSelector, + isProjectDescriptionLoadingSelector, +} from "slices/event/teamDetailSlice"; +import CircularProgress from "@material-ui/core/CircularProgress"; interface ProjectDescriptionProps { teamCode: string; @@ -12,7 +19,13 @@ interface ProjectDescriptionProps { const ProjectDescription = ({ teamCode }: ProjectDescriptionProps) => { const dispatch = useDispatch(); - const initialProjectDescription = "Write your project description here"; + const isTeamInfoLoading: boolean = useSelector(isTeamInfoLoadingSelector); + const isProjectDescriptionLoading: boolean = useSelector( + isProjectDescriptionLoadingSelector + ); + const initialProjectDescription = + useSelector(projectDescriptionSelector) || + "Write your project description here"; const [isEditing, setIsEditing] = useState(false); const projectDescriptionSchema = Yup.object().shape({ projectDescription: Yup.string() @@ -30,64 +43,82 @@ const ProjectDescription = ({ teamCode }: ProjectDescriptionProps) => { setSubmitting(false); }; + useEffect(() => { + if (teamCode != "None") { + dispatch(fetchInitialProjectDescription(teamCode)); + } + }, [dispatch, teamCode]); + return ( -
- - {({ isSubmitting, isValid }) => ( -
- - - - {isEditing ? ( - <> - - - - ) : ( - - )} - - - - )} -
-
+ <> + {isProjectDescriptionLoading ? ( + "" + ) : ( +
+ + Project Description + + + {({ isSubmitting, isValid }) => ( +
+ + + + {isEditing ? ( + <> + + + + ) : ( + + )} + + + + )} +
+
+ )} + ); }; diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionAlert.tsx b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionAlert.tsx new file mode 100644 index 000000000..2a473399e --- /dev/null +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionAlert.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import AlertBox from "components/general/AlertBox/AlertBox"; +import { projectDescriptionSelector } from "slices/event/teamDetailSlice"; +import { useSelector } from "react-redux"; +import { minProjectDescriptionLength } from "constants.js"; + +const ProjectDescriptionAlert = () => { + const projectDescription = useSelector(projectDescriptionSelector); + + if (projectDescription && projectDescription.length < minProjectDescriptionLength) { + return ( + + ); + } + + return null; +}; + +export default ProjectDescriptionAlert; diff --git a/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionDetail.tsx b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionDetail.tsx new file mode 100644 index 000000000..765218177 --- /dev/null +++ b/hackathon_site/dashboard/frontend/src/components/teamDetail/ProjectDescription/ProjectDescriptionDetail.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + fetchInitialProjectDescription, + projectDescriptionSelector, + isTeamInfoLoadingSelector, + teamInfoErrorSelector, +} from "slices/event/teamDetailSlice"; +import styles from "./ProjectDescription.module.scss"; +import { LinearProgress, Paper, Typography } from "@material-ui/core"; +interface ProjectDescriptionProps { + teamCode: string; +} + +const ProjectDescriptionDetail = ({ teamCode }: ProjectDescriptionProps) => { + const dispatch = useDispatch(); + + const projectDescription = useSelector(projectDescriptionSelector); + const isTeamInfoLoading = useSelector(isTeamInfoLoadingSelector); + + useEffect(() => { + dispatch(fetchInitialProjectDescription(teamCode)); + }, [dispatch, teamCode]); + + return ( +
+ {isTeamInfoLoading ? ( + + ) : ( + <> + + Project Description + + + {projectDescription} + + + )} +
+ ); +}; + +export default ProjectDescriptionDetail; diff --git a/hackathon_site/dashboard/frontend/src/constants.js b/hackathon_site/dashboard/frontend/src/constants.js index 2ea30905d..49380c00f 100644 --- a/hackathon_site/dashboard/frontend/src/constants.js +++ b/hackathon_site/dashboard/frontend/src/constants.js @@ -3,5 +3,6 @@ export const adminGroup = "Hardware Site Admins"; export const minTeamSize = 2; export const maxTeamSize = 4; export const hardwareSignOutStartDate = new Date(2020, 9, 1, 23, 59); -export const hardwareSignOutEndDate = new Date(2023, 9, 30, 11, 59); +export const hardwareSignOutEndDate = new Date(2025, 9, 30, 11, 59); export const hssTestUserGroup = "HSS Test Users"; +export const minProjectDescriptionLength = 20; diff --git a/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx b/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx index 751dcf14e..c4540374e 100644 --- a/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx +++ b/hackathon_site/dashboard/frontend/src/pages/TeamDetail/TeamDetail.tsx @@ -29,6 +29,7 @@ import TeamCheckedOutOrderTable from "components/teamDetail/TeamCheckedOutOrderT import { getHardwareWithFilters, setFilters } from "slices/hardware/hardwareSlice"; import { getCategories } from "slices/hardware/categorySlice"; import ProductOverview from "components/inventory/ProductOverview/ProductOverview"; +import ProjectDescriptionDetail from "components/teamDetail/ProjectDescription/ProjectDescriptionDetail"; export interface PageParams { code: string; @@ -91,6 +92,7 @@ const TeamDetail = ({ match }: RouteComponentProps) => { ) : ( <> + diff --git a/hackathon_site/dashboard/frontend/src/slices/event/teamDetailSlice.ts b/hackathon_site/dashboard/frontend/src/slices/event/teamDetailSlice.ts index 262a1030b..5ea082f4e 100644 --- a/hackathon_site/dashboard/frontend/src/slices/event/teamDetailSlice.ts +++ b/hackathon_site/dashboard/frontend/src/slices/event/teamDetailSlice.ts @@ -14,6 +14,8 @@ interface TeamDetailExtraState { isParticipantIdLoading: boolean; teamInfoError: string | null; participantIdError: string | null; + projectDescription: string | null; + isProjectDescriptionLoading: boolean; } const extraState: TeamDetailExtraState = { @@ -21,6 +23,8 @@ const extraState: TeamDetailExtraState = { isParticipantIdLoading: false, teamInfoError: null, participantIdError: null, + projectDescription: null, + isProjectDescriptionLoading: false, }; const teamDetailAdapter = createEntityAdapter(); @@ -141,6 +145,38 @@ export const updateProjectDescription = createAsyncThunk< } ); +export const fetchInitialProjectDescription = createAsyncThunk< + string, + string, // Assuming you need to pass some parameter like teamCode to fetch the description + { state: RootState; rejectValue: RejectValue; dispatch: AppDispatch } +>( + `${teamDetailReducerName}/fetchInitialProjectDescription`, + async (teamCode, { rejectWithValue, dispatch }) => { + try { + const response = await get(`/api/event/teams/${teamCode}/`); + const initialProjectDescription = response.data.project_description; + return initialProjectDescription; + } catch (e: any) { + const message = + e.response.statusText === "Not Found" + ? `Could not find project description: Error ${e.response.status}` + : `Something went wrong: Error ${e.response.status}`; + dispatch( + displaySnackbar({ + message, + options: { + variant: "error", + }, + }) + ); + return rejectWithValue({ + status: e.response.status, + message, + }); + } + } +); + const teamDetailSlice = createSlice({ name: teamDetailReducerName, initialState, @@ -184,15 +220,45 @@ const teamDetailSlice = createSlice({ builder.addCase(updateProjectDescription.pending, (state) => { state.isTeamInfoLoading = true; state.teamInfoError = null; + state.projectDescription = null; }); builder.addCase(updateProjectDescription.fulfilled, (state, { payload }) => { state.isTeamInfoLoading = false; state.teamInfoError = null; + state.projectDescription = payload.project_description; }); builder.addCase(updateProjectDescription.rejected, (state, { payload }) => { state.isTeamInfoLoading = false; state.teamInfoError = payload?.message ?? "Something went wrong"; + state.projectDescription = null; }); + + builder.addCase(fetchInitialProjectDescription.pending, (state) => { + state.isTeamInfoLoading = true; + state.teamInfoError = null; + state.projectDescription = null; + state.isProjectDescriptionLoading = true; + }); + + builder.addCase( + fetchInitialProjectDescription.fulfilled, + (state, { payload }) => { + state.isTeamInfoLoading = false; + state.isProjectDescriptionLoading = false; + state.teamInfoError = null; + state.projectDescription = payload; // Update the projectDescription state with the fetched value. + } + ); + + builder.addCase( + fetchInitialProjectDescription.rejected, + (state, { payload }) => { + state.isTeamInfoLoading = false; + state.isProjectDescriptionLoading = false; + state.teamInfoError = payload?.message ?? "Something went wrong"; + state.projectDescription = null; + } + ); }, }); @@ -225,3 +291,13 @@ export const updateParticipantIdErrorSelector = createSelector( [teamDetailSliceSelector], (teamDetailSlice) => teamDetailSlice.participantIdError ); + +export const projectDescriptionSelector = createSelector( + [teamDetailSliceSelector], + (teamDetailSlice) => teamDetailSlice.projectDescription +); + +export const isProjectDescriptionLoadingSelector = createSelector( + [teamDetailSliceSelector], + (teamDetailSlice) => teamDetailSlice.isProjectDescriptionLoading +);