diff --git a/public/assets/actions/360-dashboard-24.png b/public/assets/actions/360-dashboard-24.png new file mode 100644 index 0000000..60432b0 Binary files /dev/null and b/public/assets/actions/360-dashboard-24.png differ diff --git a/public/assets/actions/Copy-icon-24.png b/public/assets/actions/Copy-icon-24.png new file mode 100644 index 0000000..6d4f0eb Binary files /dev/null and b/public/assets/actions/Copy-icon-24.png differ diff --git a/public/assets/actions/add-assignment-24.png b/public/assets/actions/add-assignment-24.png new file mode 100644 index 0000000..6756d3e Binary files /dev/null and b/public/assets/actions/add-assignment-24.png differ diff --git a/public/assets/actions/add-participant-24.png b/public/assets/actions/add-participant-24.png new file mode 100644 index 0000000..2a4c12b Binary files /dev/null and b/public/assets/actions/add-participant-24.png differ diff --git a/public/assets/actions/add-ta-24.png b/public/assets/actions/add-ta-24.png new file mode 100644 index 0000000..cf8e038 Binary files /dev/null and b/public/assets/actions/add-ta-24.png differ diff --git a/public/assets/actions/create-teams-24.png b/public/assets/actions/create-teams-24.png new file mode 100644 index 0000000..c4c4fa8 Binary files /dev/null and b/public/assets/actions/create-teams-24.png differ diff --git a/public/assets/actions/delete-icon-24.png b/public/assets/actions/delete-icon-24.png new file mode 100644 index 0000000..57b6eb6 Binary files /dev/null and b/public/assets/actions/delete-icon-24.png differ diff --git a/public/assets/actions/edit-icon-24.png b/public/assets/actions/edit-icon-24.png new file mode 100644 index 0000000..062d9c0 Binary files /dev/null and b/public/assets/actions/edit-icon-24.png differ diff --git a/src/App.js b/src/App.js index 3132f60..e51db32 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,12 @@ import React from "react"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import AssignmentForm from "./components/Assignments/AssignmentForm/AssignmentForm"; +import Rubrics from "./components/Assignments/AssignmentForm/Rubrics"; +import Assignments from "./components/Assignments/Assignments"; import Home from "./components/Layout/Home"; import RootLayout from "./components/Layout/Root"; import Users from "./components/Users/Users"; +import Courses from "./components/Courses/Courses"; function App() { const router = createBrowserRouter([ @@ -12,6 +16,10 @@ function App() { children: [ { index: true, element: }, { path: "users", element: }, + { path: "assignments", element: }, + { path: "assignments/new", element: }, + { path: "assignments/card", element: }, + { path: "courses", element: }, ], }, ]); diff --git a/src/components/Assignments/AssignmentForm/AssignmentForm.js b/src/components/Assignments/AssignmentForm/AssignmentForm.js new file mode 100644 index 0000000..99fcf63 --- /dev/null +++ b/src/components/Assignments/AssignmentForm/AssignmentForm.js @@ -0,0 +1,120 @@ +import { Form, Formik } from "formik"; +import React, { useState } from "react"; +import { Button, Col, Container, Row, Tab, Tabs } from "react-bootstrap"; +import * as Yup from "yup"; +import Badges from "./Badges"; +import Calibration from "./Calibration"; +import DueDate from "./DueDate"; +import { + General, + generalInitialValues, + generalValidationSchema, +} from "./General"; +import Miscellaneous from "./Miscellaneous"; +import ReviewStrategy, { + reviewStrategyInitialValues, + reviewStrategyValidationSchema, +} from "./ReviewStrategy"; +import Rubrics, { rubricInitialValues } from "./Rubrics"; +import { topicInitialValues, Topics, topicValidationSchema } from "./Topics"; + +const initialValues = { + ...generalInitialValues, + ...topicInitialValues, + ...reviewStrategyInitialValues, + ...rubricInitialValues, +}; + +const validationSchema = Yup.object().shape({ + ...generalValidationSchema, + ...topicValidationSchema, + ...reviewStrategyValidationSchema, +}); + +const onSubmit = (values, submitProps) => { + console.log("Form data", values); + console.log("Submit props", submitProps); + submitProps.setSubmitting(false); + submitProps.resetForm(); +}; + +const AssignmentForm = () => { + const [key, setKey] = useState("general"); + + return ( + + + +

Create New Assignment

+ +
+ + + {(formik) => { + console.log("Formik props", formik.values); + return ( +
+ setKey(k)} + className="mb-3" + > + + + + {formik.values.hasTopics && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); + }} +
+
+
+ ); +}; + +export default AssignmentForm; diff --git a/src/components/Assignments/AssignmentForm/Badges.js b/src/components/Assignments/AssignmentForm/Badges.js new file mode 100644 index 0000000..720155b --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Badges.js @@ -0,0 +1,9 @@ +const Badges = () => { + return ( +
+

Badges

+
+ ); +}; + +export default Badges; diff --git a/src/components/Assignments/AssignmentForm/Calibration.js b/src/components/Assignments/AssignmentForm/Calibration.js new file mode 100644 index 0000000..c4f3def --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Calibration.js @@ -0,0 +1,9 @@ +const Calibration = () => { + return ( +
+

Calibration

+
+ ); +}; + +export default Calibration; diff --git a/src/components/Assignments/AssignmentForm/DueDate.js b/src/components/Assignments/AssignmentForm/DueDate.js new file mode 100644 index 0000000..4c068c8 --- /dev/null +++ b/src/components/Assignments/AssignmentForm/DueDate.js @@ -0,0 +1,9 @@ +const DueDate = () => { + return ( +
+

DueDate

+
+ ); +}; + +export default DueDate; diff --git a/src/components/Assignments/AssignmentForm/General.js b/src/components/Assignments/AssignmentForm/General.js new file mode 100644 index 0000000..a79d202 --- /dev/null +++ b/src/components/Assignments/AssignmentForm/General.js @@ -0,0 +1,248 @@ +import { Field } from "formik"; +import React from "react"; +import { Col, Form, InputGroup, Row } from "react-bootstrap"; +import * as Yup from "yup"; +import FormCheckbox from "../../UI/Form/FormCheckbox"; +import FormCheckboxGroup from "../../UI/Form/FormCheckboxGroup"; +import FormInput from "../../UI/Form/FormInput"; +import FormRange from "../../UI/Form/FormRange"; +import FormSelect from "../../UI/Form/FormSelect"; + +export const generalInitialValues = { + name: "", + directory_path: "", + preferences: [], + course: "", + reputation_algorithm: "", + require_quiz: false, + hasTopics: false, + has_teams: false, + useSimicheck: false, + num_quiz_questions: 0, + max_team_size: 0, + simicheck: 0, + simicheck_threshold: 0, +}; + +export const generalValidationSchema = { + name: Yup.string().required("Required"), + course: Yup.string().required("Required"), + directory_path: Yup.string().required("Required"), + reputation_algorithm: Yup.string().required("Required"), + num_quiz_questions: Yup.number().when("require_quiz", { + is: true, + then: () => Yup.number().min(1, "Must be greater than 0"), + }), + max_team_size: Yup.number().when("has_teams", { + is: true, + then: () => Yup.number().min(2, "Must be greater than 1"), + }), + simicheck_threshold: Yup.number().when("useSimicheck", { + is: true, + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(99, "Must be less than 100"), + }), + simicheck: Yup.number().when("useSimicheck", { + is: true, + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(99, "Must be less than 100"), + }), +}; + +const availableCourses = [ + { key: "Select a course", value: "" }, + { key: "Course 1", value: 1 }, + { key: "Course 2", value: 2 }, + { key: "Course 3", value: 3 }, + { key: "Course 4", value: 4 }, + { key: "Course 5", value: 5 }, +]; + +const formCheckboxGroupOptions1 = [ + { label: "Available to Students?", value: "availability_flag" }, + { label: "Private Assignment?", value: "private" }, + { label: "Has Badge?", value: "has_badge" }, + { label: "Micro-task assignment?", value: "has_description" }, + { label: "Calibration for training?", value: "is_calibrated" }, + { + label: "Staggered deadline assignment?", + value: "staggered_deadline", + }, + { + label: "Reviews visible to all other reviewers?", + value: "reviews_visible_to_all", + }, + { + label: "Allow feedback comments to be tagged by the author?", + value: "is_answer_tagging_allowed", + }, +]; + +const handleHasTeamsChange = (e, form) => { + if (e.target.checked) { + form.setFieldValue("has_teams", true); + form.setFieldValue("max_team_size", 2); + } else { + form.setFieldValue("max_team_size", 0); + form.setFieldValue("has_teams", false); + form.setFieldValue("show_teammate_reviews", false); + } +}; + +export const General = (props) => { + return ( + <> + + + + + Course + } + /> + + + + + +
+
+ + + + + {({ field, form }) => { + return ( + + + handleHasTeamsChange(e, form)} + /> + + {form.errors[field.name]} + + + + ); + }} + + + {props.values.has_teams && ( + + Maximum Team Size + + } + /> + )} + + {props.values.require_quiz && ( + + Number of Quiz Questions + + } + /> + )} + + + + + {props.values.useSimicheck && ( + + )} + + {props.values.useSimicheck && ( + + )} + + + Reputation Algorithm + + } + /> + + + ); +}; diff --git a/src/components/Assignments/AssignmentForm/Miscellaneous.js b/src/components/Assignments/AssignmentForm/Miscellaneous.js new file mode 100644 index 0000000..7ac8c9b --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Miscellaneous.js @@ -0,0 +1,9 @@ +const Miscellaneous = () => { + return ( +
+

Miscellaneous

+
+ ); +}; + +export default Miscellaneous; diff --git a/src/components/Assignments/AssignmentForm/ReviewStrategy.js b/src/components/Assignments/AssignmentForm/ReviewStrategy.js new file mode 100644 index 0000000..4f1890f --- /dev/null +++ b/src/components/Assignments/AssignmentForm/ReviewStrategy.js @@ -0,0 +1,330 @@ +import React, { Fragment } from "react"; +import { Col, InputGroup, Row } from "react-bootstrap"; +import * as Yup from "yup"; +import FormCheckbox from "../../UI/Form/FormCheckbox"; +import FormInput from "../../UI/Form/FormInput"; +import FormSelect from "../../UI/Form/FormSelect"; + +export const reviewStrategyInitialValues = { + rounds_of_reviews: 1, + review_assignment_strategy: "Auto-Selected", + review_topic_threshold: 0, + max_reviews_per_submission: 0, + has_max_review_limit: false, + num_reviews_allowed: 0, + num_reviews_required: 0, + has_meta_review_limit: false, + num_meta_reviews_allowed: 0, + num_meta_reviews_required: 0, + instructor_selected_review_strategy: "", + num_reviews_per_student: 0, + num_reviewers_per_submission: 0, + num_calibrated_artifacts: 0, + num_uncalibrated_artifacts: 0, + is_anonymous: false, + is_self_review_enabled: false, + allow_selecting_additional_reviews_after_1st_round: false, +}; + +export const reviewStrategyValidationSchema = { + rounds_of_reviews: Yup.number().min(1, "Must be greater than 0"), + review_assignment_strategy: Yup.string().required("Required"), + max_reviews_per_submission: Yup.number() + .min(1, "Must be greater than 0") + .max(20, "Must be less than 20"), + num_reviews_allowed: Yup.number().when("has_max_review_limit", { + is: true, + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + }), + num_reviews_required: Yup.number().when( + ["has_max_review_limit", "num_reviews_allowed"], + { + is: (has_max_review_limit, num_reviews_allowed) => + has_max_review_limit && num_reviews_allowed > 1, + then: (num_reviews_allowed) => + Yup.number() + .min(1, "Must be greater than 0") + .max( + +num_reviews_allowed, + "Must be less than or equal to the number of reviews allowed" + ), + } + ), + num_meta_reviews_allowed: Yup.number().when("has_meta_review_limit", { + is: true, + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + }), + num_meta_reviews_required: Yup.number().when( + ["has_meta_review_limit", "num_meta_reviews_allowed"], + { + is: (has_meta_review_limit, num_meta_reviews_allowed) => + has_meta_review_limit && +num_meta_reviews_allowed > 1, + then: (num_meta_reviews_allowed) => + Yup.number() + .min(1, "Must be greater than 0") + .max( + +num_meta_reviews_allowed, + "Must be less than or equal to the number of meta reviews allowed" + ), + } + ), + instructor_selected_review_strategy: Yup.string().required("Required"), + num_reviews_per_student: Yup.number().when( + "instructor_selected_review_strategy", + { + is: "num_reviews", + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + } + ), + num_reviewers_per_submission: Yup.number().when( + "instructor_selected_review_strategy", + { + is: "num_reviewers", + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + } + ), + num_calibrated_artifacts: Yup.number().when( + "instructor_selected_review_strategy", + { + is: "calibrated_and_uncalibrated", + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + } + ), + num_uncalibrated_artifacts: Yup.number().when( + "instructor_selected_review_strategy", + { + is: "calibrated_and_uncalibrated", + then: () => + Yup.number() + .min(1, "Must be greater than 0") + .max(9, "Must be less than 10"), + } + ), +}; + +const reviewStrategyOptions = [ + { key: "Auto Selected", value: "Auto-Selected" }, + { key: "Instructor Selected", value: "Instructor-Selected" }, +]; + +const instructorSelectedReviewOptions = [ + { key: "Select a review strategy", value: "" }, + { + key: "Set number of calibrated artifacts", + value: "calibrated_and_uncalibrated", + }, + { key: "Set number of reviews done by each student", value: "num_reviews" }, + { + key: "Set minimum number of reviews done for each submission", + value: "num_reviewers", + }, +]; + +const renderReviewStrategyInput = (strategy) => { + switch (strategy) { + case "num_reviews": + return ( + + ); + case "num_reviewers": + return ( + + ); + case "calibrated_and_uncalibrated": + return ( + + + + + ); + default: + return <>; + } +}; + +const ReviewStrategy = (formik) => { + return ( + + + + + + + + {formik.values.review_assignment_strategy === + "Instructor-Selected" && ( + <> + + {renderReviewStrategyInput( + formik.values.instructor_selected_review_strategy + )} + + )} + + + + + + + + {formik.values.has_max_review_limit && ( + + Set allowed number of reviews per reviewer + + } + /> + )} + + {formik.values.has_max_review_limit && ( + + Set required number of reviews per reviewer + + } + /> + )} + + + + {formik.values.has_meta_review_limit && ( + + Set allowed number of meta-reviews per reviewer + + } + /> + )} + + {formik.values.has_meta_review_limit && ( + + Set required number of meta-reviews per reviewer + + } + /> + )} + + + + + + {formik.values.rounds_of_reviews > 1 && ( + + )} + + + + ); +}; + +export default ReviewStrategy; diff --git a/src/components/Assignments/AssignmentForm/Rubric/RubricItem.js b/src/components/Assignments/AssignmentForm/Rubric/RubricItem.js new file mode 100644 index 0000000..dc89bea --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Rubric/RubricItem.js @@ -0,0 +1,163 @@ +import { Field, FieldArray } from "formik"; +import React from "react"; +import { Button, Card, Col, InputGroup, Row } from "react-bootstrap"; +import FormInput from "../../../UI/Form/FormInput"; +import FormSelect from "../../../UI/Form/FormSelect"; +import InfoToolTip from "../../../UI/InfoToolTip"; +import TagPrompt from "./TagPrompt"; + +const RubricItem = (props) => { + const { + name, + values, + rubricName, + displayStyles, + questionTypes, + promptOptions, + questionnaireOptions, + } = props; + return ( + + + + + {rubricName} + + + + + + Questionnaire + + } + /> + + + + {({ field, form }) => ( + + + + Use dropdown instead{" "} + + + + )} + + + + + Scored question display type + + + } + /> + + + + + + Weight + + } + inputGroupPostpend={ + + % + + } + /> + + + + {"Notification Limit "} + + + } + inputGroupPostpend={ + + % + + } + /> + + + + + + + + {(arrayHelpers) => ( + <> + + + + + )} + + + + + + ); +}; + +export default RubricItem; diff --git a/src/components/Assignments/AssignmentForm/Rubric/TagPrompt.js b/src/components/Assignments/AssignmentForm/Rubric/TagPrompt.js new file mode 100644 index 0000000..440dfa1 --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Rubric/TagPrompt.js @@ -0,0 +1,58 @@ +import React from "react"; +import { Button, Col, InputGroup, Row } from "react-bootstrap"; +import FormInput from "../../../UI/Form/FormInput"; +import FormSelect from "../../../UI/Form/FormSelect"; + +const TagPrompt = ({ + name_path, + values, + tag_prompts, + question_types, + remove, +}) => { + return values.map((_, index) => ( + + + Tag prompt + + } + /> + + + Question type + + } + /> + + Comment length threshold + + } + /> + + + + + )); +}; + +export default TagPrompt; diff --git a/src/components/Assignments/AssignmentForm/Rubrics.js b/src/components/Assignments/AssignmentForm/Rubrics.js new file mode 100644 index 0000000..71e03b7 --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Rubrics.js @@ -0,0 +1,242 @@ +import { FieldArray, useFormikContext } from "formik"; +import React, { useCallback, useEffect } from "react"; +import { Col, Row } from "react-bootstrap"; +import * as Yup from "yup"; +import FormCheckbox from "../../UI/Form/FormCheckbox"; +import RubricItem from "./Rubric/RubricItem"; + +const REVIEW = "review"; +const META_REVIEW = "meta_review"; +const TEAMMATE_REVIEW = "teammate_review"; +const AUTHOR_FEEDBACK = "author_feedback"; +const BOOKMARK_RATING = "bookmark_rating"; + +const rubricItemInitialValues = { + questionnaire_id: "", + display_type: "Dropdown", + questionnaire_weight: "", + notification_limit: "", + dropdown: false, + tagPrompts: [], +}; + +export const rubricInitialValues = { + review_rubric_varies_by_round: false, + show_teammate_reviews: false, + rubrics: { + review_1: rubricItemInitialValues, + author_feedback: rubricItemInitialValues, + }, +}; + +const reviewQuestionnaireOptions = [ + { key: "questionnaire_1", value: "Questionnaire 1" }, + { key: "questionnaire_2", value: "Questionnaire 2" }, + { key: "questionnaire_3", value: "Questionnaire 3" }, +]; + +const questionTypes = [ + { key: "text", value: "Text" }, + { key: "textarea", value: "Text Area" }, + { key: "checkbox", value: "Checkbox" }, + { key: "radio", value: "Radio" }, +]; + +const tagPromptsOptions = [ + { key: "tag_prompt_1", value: "Tag Prompt 1" }, + { key: "tag_prompt_2", value: "Tag Prompt 2" }, + { key: "tag_prompt_3", value: "Tag Prompt 3" }, +]; + +const displayStyles = [ + { key: "Dropdown", value: "Dropdown" }, + { key: "Scale", value: "Scale" }, +]; + +const rubricItemValidationSchema = { + questionnaire_id: Yup.string().required("Required"), + display_type: Yup.string().required("Required"), + questionnaire_weight: Yup.number().required("Required").min(0).max(100), + notification_limit: Yup.number().required("Required").min(0).max(100), + tagPrompts: Yup.array() + .of( + Yup.object().shape({ + tag_prompt: Yup.string().required("Tag prompt is required"), + question_type: Yup.string().required("Question type is required"), + comment_length_threshold: Yup.number() + .min(1, "Threshold must be at least 1") + .notRequired(), + }) + ) + .notRequired(), +}; + +const renderQuestionnaires = ( + name, + values, + rubricName, + questionnaireOptions +) => { + return ( + + ); +}; + +const Rubrics = ({ values }) => { + const { setFieldValue } = useFormikContext(); + + const initializeRubricValues = useCallback( + (names, action = "add") => { + let newRubrics = { ...values.rubrics }; + let shouldUpdate = false; + + if (action === "remove") { + names.forEach((name) => { + if (newRubrics.hasOwnProperty(name)) { + delete newRubrics[name]; + shouldUpdate = true; + } + }); + } else { + names.forEach((name) => { + if (!values.rubrics[name]) { + newRubrics[name] = rubricItemInitialValues; + shouldUpdate = true; + } + }); + } + shouldUpdate && setFieldValue("rubrics", newRubrics); + }, + [values.rubrics, setFieldValue] + ); + + useEffect(() => { + const rubricNames = []; + for (let i = 2; i <= +values.rounds_of_reviews; i++) + rubricNames.push(`${REVIEW}_${i}`); + + if (values.review_rubric_varies_by_round) { + initializeRubricValues(rubricNames); + } else { + initializeRubricValues(rubricNames, "remove"); + } + }, [values.review_rubric_varies_by_round, values.rounds_of_reviews]); + + useEffect(() => { + values.has_meta_review_limit && initializeRubricValues([META_REVIEW]); + !values.has_meta_review_limit && + initializeRubricValues([META_REVIEW], "remove"); + }, [values.has_meta_review_limit]); + + useEffect(() => { + values.show_teammate_reviews && initializeRubricValues([TEAMMATE_REVIEW]); + !values.show_teammate_reviews && + initializeRubricValues([TEAMMATE_REVIEW], "remove"); + }, [values.show_teammate_reviews]); + + useEffect(() => { + values.use_bookmark && initializeRubricValues([BOOKMARK_RATING]); + !values.use_bookmark && initializeRubricValues([BOOKMARK_RATING], "remove"); + }, [values.use_bookmark]); + + return ( + <> + + +

Rubrics

+ +
+ + + {values.has_teams && ( + + )} + + +
+
+ + + {(arrayHelper) => { + console.log("Array Helper", arrayHelper); + return ( + <> + {renderQuestionnaires( + "review_1", + values, + "Review Round 1", + reviewQuestionnaireOptions + )} + {values.review_rubric_varies_by_round && + values.rubrics[`${REVIEW}_2`] && + Array.from(Array(+values.rounds_of_reviews - 1).keys()).map( + (_, i) => + renderQuestionnaires( + `${REVIEW}_${i + 2}`, + values, + `Review Round ${i + 2}`, + reviewQuestionnaireOptions + ) + )} + + {values.rubrics[META_REVIEW] && + renderQuestionnaires( + META_REVIEW, + values, + "Meta Review", + reviewQuestionnaireOptions + )} + + {renderQuestionnaires( + AUTHOR_FEEDBACK, + values, + "Author Feedback", + reviewQuestionnaireOptions + )} + + {values.rubrics[BOOKMARK_RATING] && + renderQuestionnaires( + BOOKMARK_RATING, + values, + "Bookmark Rating", + reviewQuestionnaireOptions + )} + + {values.rubrics[TEAMMATE_REVIEW] && + renderQuestionnaires( + TEAMMATE_REVIEW, + values, + "Teammate Review", + reviewQuestionnaireOptions + )} + + ); + }} + + + + ); +}; + +export default Rubrics; diff --git a/src/components/Assignments/AssignmentForm/Topics.js b/src/components/Assignments/AssignmentForm/Topics.js new file mode 100644 index 0000000..e9037ad --- /dev/null +++ b/src/components/Assignments/AssignmentForm/Topics.js @@ -0,0 +1,84 @@ +import React from "react"; +import { Col, Row } from "react-bootstrap"; +import * as Yup from "yup"; +import FormCheckbox from "../../UI/Form/FormCheckbox"; +import InfoToolTip from "../../UI/InfoToolTip"; + +export const topicInitialValues = { + use_bookmark: false, + is_intelligent: false, + allow_suggestions: false, + can_review_same_topic: false, + can_choose_topic_to_review: false, + topics: [], +}; + +export const topicValidationSchema = { + use_bookmark: Yup.boolean(), + is_intelligent: Yup.boolean(), + allow_suggestions: Yup.boolean(), + can_review_same_topic: Yup.boolean(), + can_choose_topic_to_review: Yup.boolean(), +}; + +export const Topics = (formik) => { + return ( + <> + + +

{`Topics for ${formik.values.name} Assignment`}

+ +
+ + + + + + + Enable authors to review others working on same topic? + + + } + /> + + +
+
+ + + ); +}; diff --git a/src/components/Assignments/Assignments.js b/src/components/Assignments/Assignments.js new file mode 100644 index 0000000..ea7458d --- /dev/null +++ b/src/components/Assignments/Assignments.js @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { Table } from "react-bootstrap"; +import { Link } from "react-router-dom"; + +const Assignments = () => { + const [assignments, setAssignments] = useState([]); + + useEffect(() => { + const dummyData = [ + { + id: 1, + name: "Assignment 1", + course: { + name: "Course 1", + }, + institution: "Institution 1", + createdate: "2021-01-01", + updatedate: "2021-01-01", + }, + { + id: 2, + name: "Assignment 2", + course: { + name: "Course 2", + }, + institution: "Institution 2", + createdate: "2021-01-01", + updatedate: "2021-01-01", + }, + { + id: 3, + name: "Assignment 3", + course: { + name: "Course 3", + }, + institution: "Institution 3", + createdate: "2021-01-01", + updatedate: "2021-01-01", + }, + ]; + setAssignments(dummyData); + }, []); + + function handleClick(id) {} + + return ( + + + + + + + + + + + + + {assignments.map((assignment) => ( + handleClick(assignment.id)}> + + + + + + + + ))} + +
AssignmentsCourseInstitutionCreatedUpdatedActions
{assignment.name}{assignment.course.name}{assignment.institution}{assignment.createdate}{assignment.updatedate} + Add +
+ ); +}; + +export default Assignments; diff --git a/src/components/Courses/Courses.js b/src/components/Courses/Courses.js new file mode 100644 index 0000000..2847eaf --- /dev/null +++ b/src/components/Courses/Courses.js @@ -0,0 +1,110 @@ +import 'bootstrap/dist/css/bootstrap.min.css'; +import CardList from '../UI/Card/CardList'; + + +const Courses = () => { + // Header values for Courses. + const columnKeys = [ + { key: "courseName", label: "Course Name" }, + { key: "institution", label: "Institution" }, + { key: "createDate", label: "Created Date" }, + { key: "updateDate", label: "Updated Date" }, + { key: "", label: "Actions" } + ]; + // Dummy Data for courses. + const dummyData = [ + { + "courseId": 1, + "courseName": "Computer Science 101", + "institution": "Harvard University", + "createDate": "2022-01-01", + "updateDate": "2022-02-01", + "assignments": [ + { + "assignmentName": "Project 1", + "institution": "Harvard University", + "createDate": "2022-01-15", + "updateDate": "2022-02-01" + }, + { + "assignmentName": "Quiz 1", + "institution": "Harvard University", + "createDate": "2022-01-30", + "updateDate": "2022-02-01" + } + ] + }, + { + "courseId": 2, + "courseName": "Mathematics 101", + "institution": "Massachusetts Institute of Technology", + "createDate": "2022-02-15", + "updateDate": "2022-03-01", + "assignments": [ + { + "assignmentName": "Problem Set 1", + "institution": "Massachusetts Institute of Technology", + "createDate": "2022-02-25", + "updateDate": "2022-03-01" + }, + { + "assignmentName": "Midterm Exam", + "institution": "Massachusetts Institute of Technology", + "createDate": "2022-03-01", + "updateDate": "2022-03-10" + } + ] + }, + { + "courseId": 3, + "courseName": "English 101", + "institution": "Stanford University", + "createDate": "2022-03-15", + "updateDate": "2022-04-01", + "assignments": [ + { + "assignmentName": "Essay 1", + "institution": "Stanford University", + "createDate": "2022-03-20", + "updateDate": "2022-04-01" + }, + { + "assignmentName": "Presentation", + "institution": "Stanford University", + "createDate": "2022-03-30", + "updateDate": "2022-04-01" + } + ] + }, + { + "courseId": 4, + "courseName": "History 101", + "institution": "Yale University", + "createDate": "2022-04-15", + "updateDate": "2022-05-01", + "assignments": [ + { + "assignmentName": "Research Paper", + "institution": "Yale University", + "createDate": "2022-04-20", + "updateDate": "2022-05-01" + }, + { + "assignmentName": "Exam 1", + "institution": "Yale University", + "createDate": "2022-04-30", + "updateDate": "2022-05-01" + } + ] + } + ]; + + return ( +
+

Courses

+ +
+ ); +}; + +export default Courses; \ No newline at end of file diff --git a/src/components/UI/Card/Card.js b/src/components/UI/Card/Card.js new file mode 100644 index 0000000..23d8c85 --- /dev/null +++ b/src/components/UI/Card/Card.js @@ -0,0 +1,75 @@ +import { useState } from "react"; +import 'bootstrap/dist/css/bootstrap.min.css'; +import CardList from "./CardList"; + +const Card = ({ course }) => { + const [showAssignments, setShowAssignments] = useState(false); + const keys = Object.keys(course).filter(key => key !== 'assignments' && !key.includes("Id")); + // Child header for assignments. + const columnKeys = [ + { key: "assignmentName", label: "Assignment Name" }, + { key: "institution", label: "Institution" }, + { key: "createDate", label: "Created Date" }, + { key: "updateDate", label: "Updated Date" }, + { key: "", label: "Actions" } + ]; + + // Handle click function for sort. + function handleClick(event) { + event.stopPropagation(); + } + + // To show Assignments + const toggleAssignments = (assignments) => { + if (!assignments) { + + } + else { + setShowAssignments(!showAssignments); + + } + }; + + if (!course) { + return null; // Or some other fallback component + } + + + return ( + +
+
e.currentTarget.style.backgroundColor = "#f2f2f2"} + onMouseOut={(e) => e.currentTarget.style.backgroundColor = ""}> + +
toggleAssignments(course.assignments)} > + {keys.map((key, index) => ( +
+ {course[key]} +
+ ))} +
+ Add Assignment + Add Participant + Add TA + Copy + Delete + Edit + Create Teams + Dashboard +
+ + {showAssignments && +
+ +
+ } + +
+
+
+ + ); +}; + +export default Card; diff --git a/src/components/UI/Card/CardHeader.js b/src/components/UI/Card/CardHeader.js new file mode 100644 index 0000000..14899ac --- /dev/null +++ b/src/components/UI/Card/CardHeader.js @@ -0,0 +1,50 @@ +import { useState } from "react"; +import 'bootstrap/dist/css/bootstrap.min.css'; + + +const CardHeader = ({ columnKeys, onSortClick }) => { + const [sortColumn, setSortColumn] = useState(null); + const [sortDirection, setSortDirection] = useState(null); + + const handleSortClick = (column) => { + if (column === '') { + + } + else { + if (onSortClick) { + onSortClick(column); + } + if (sortColumn === column) { + // If the same column is clicked twice, toggle the sort direction + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + // Otherwise, sort by the new column in ascending order + setSortColumn(column); + setSortDirection("asc"); + } + } + }; + + return ( +
+ +
+ {columnKeys.map((column) => ( +
handleSortClick(column.key)} + > + {column.label} + {sortColumn === column.key && ( + {sortDirection === "asc" ? " ▲" : " ▼"} + )} +
+ ))} +
+ +
+ ); +}; + +export default CardHeader; \ No newline at end of file diff --git a/src/components/UI/Card/CardList.js b/src/components/UI/Card/CardList.js new file mode 100644 index 0000000..a1764a3 --- /dev/null +++ b/src/components/UI/Card/CardList.js @@ -0,0 +1,63 @@ +import { useState } from "react"; +import 'bootstrap/dist/css/bootstrap.min.css'; +import Card from './Card'; +import CardHeader from './CardHeader'; + +const CardList = ({ courses, columnKeys }) => { + const [sortOrder, setSortOrder] = useState('asc'); + const [searchTerm, setSearchTerm] = useState(''); + + // Sort courses by name in ascending or descending order + let sortedCourses = [...courses]; + + // Filter courses by search term + const [sortColumn, setSortColumn] = useState(null); + + const handleSortClick = (columnKey) => { + if (sortColumn === columnKey) { + setSortOrder((prevState) => (prevState === "asc" ? "desc" : "asc")); + + } else { + setSortColumn(columnKey); + setSortOrder("asc"); + } + }; + + + if (sortColumn) { + sortedCourses.sort((a, b) => { + let comparison = a[sortColumn].localeCompare(b[sortColumn]); + return sortOrder === "asc" ? comparison : -comparison; + }); + } + const filteredCourses = sortedCourses.filter((course) => { + const values = Object.values(course); + return values.some((value) => + typeof value === "string" && value.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }); + + + return ( +
+
+ + + setSearchTerm(e.target.value)} + /> +
+

+ + + {filteredCourses.map((course) => ( + + ))} +
+ ); +}; + +export default CardList; \ No newline at end of file diff --git a/src/components/UI/Form/FormCheckbox.js b/src/components/UI/Form/FormCheckbox.js index ede8d5c..357f442 100644 --- a/src/components/UI/Form/FormCheckbox.js +++ b/src/components/UI/Form/FormCheckbox.js @@ -1,21 +1,23 @@ -import {Field} from "formik"; +import { Field } from "formik"; import React from "react"; -import {Form, InputGroup} from "react-bootstrap"; +import { Form, InputGroup } from "react-bootstrap"; import InfoToolTip from "../InfoToolTip"; const FormCheckbox = (props) => { - const {controlId, label, name, disabled, tooltip} = props; + const { controlId, label, name, disabled, tooltip } = props; const displayLabel = tooltip ? ( <> {label + " "} - + - ) : label; + ) : ( + label + ); return ( - {({field, form}) => { + {({ field, form }) => { return ( diff --git a/src/components/UI/Form/FormInput.js b/src/components/UI/Form/FormInput.js index 00e48e8..9a94526 100644 --- a/src/components/UI/Form/FormInput.js +++ b/src/components/UI/Form/FormInput.js @@ -1,21 +1,34 @@ -import {Field} from "formik"; +import { Field } from "formik"; import React from "react"; -import {Form, InputGroup} from "react-bootstrap"; +import { Form, InputGroup } from "react-bootstrap"; import InfoToolTip from "../InfoToolTip"; const FormInput = (props) => { - const {as, md, controlId, label, name, disabled, type, inputGroupPrepend, tooltip} = props; + const { + as, + md, + controlId, + label, + name, + disabled, + type, + inputGroupPrepend, + inputGroupPostpend, + tooltip, + } = props; const displayLabel = tooltip ? ( <> {label + " "} - + - ) : label; + ) : ( + label + ); return ( - {({field, form}) => { + {({ field, form }) => { const isValid = !form.errors[field.name]; const isInvalid = form.touched[field.name] && !isValid; return ( @@ -30,7 +43,7 @@ const FormInput = (props) => { isInvalid={isInvalid} feedback={form.errors[field.name]} /> - + {inputGroupPostpend} {form.errors[field.name]} @@ -46,6 +59,7 @@ FormInput.defaultProps = { type: "text", tooltip: null, inputGroupPrepend: null, + inputGroupPostpend: null, }; export default FormInput; diff --git a/src/custom.scss b/src/custom.scss index a727ea0..9cd85e2 100644 --- a/src/custom.scss +++ b/src/custom.scss @@ -10,6 +10,7 @@ $info: #9ab6da; $warning: #e49b1f; $danger: #f00678; + // test theme // scss-docs-start theme-color-variables $primary: #0d6efd; @@ -21,6 +22,7 @@ $danger: #dc3545; $light: #f8f9fa; $dark: #212529; $wolf-red: #a90201; +$smoke: #f5f5f5; $theme-colors: ( "primary": $primary, @@ -31,7 +33,8 @@ $theme-colors: ( "danger": $danger, "light": $light, "dark": $dark, - "wolf-red": $wolf-red + "wolf-red": $wolf-red, + "smoke": $smoke ); // import bootstrap styles at the bottom! diff --git a/src/index.css b/src/index.css index 00f86c3..7d12bb1 100644 --- a/src/index.css +++ b/src/index.css @@ -5,4 +5,5 @@ html { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + color: whitesmoke; } \ No newline at end of file diff --git a/src/store/assignment-form.js b/src/store/assignment-form.js new file mode 100644 index 0000000..1392912 --- /dev/null +++ b/src/store/assignment-form.js @@ -0,0 +1,23 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const assignmentFormSlice = createSlice({ + name: "assignmentForm", + initialState: { initialValues: {}, validationSchema: {} }, + reducers: { + setInitialValues(state, action) { + state.initialValues = { ...state.initialValues, ...action.payload }; + }, + setValidationSchema(state, action) { + state.validationSchema = { ...state.validationSchema, ...action.payload }; + }, + removeInitialValues(state, action) { + delete state.initialValues[action.payload]; + }, + removeValidationSchema(state, action) { + delete state.validationSchema[action.payload]; + }, + }, +}); + +export const assignmentFormActions = assignmentFormSlice.actions; +export default assignmentFormSlice.reducer; diff --git a/src/store/index.js b/src/store/index.js index 3974694..ffcaeb1 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,8 +1,9 @@ -import {configureStore} from '@reduxjs/toolkit'; -import alertReducer from './alert'; +import { configureStore } from "@reduxjs/toolkit"; +import alertReducer from "./alert"; +import assignmentReducer from "./assignment-form"; const store = configureStore({ - reducer: {alert: alertReducer}, + reducer: { alert: alertReducer, assignment: assignmentReducer }, }); -export default store; \ No newline at end of file +export default store;