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 (
+
+ );
+ }}
+
+
+
+ );
+};
+
+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 |
+ Course |
+ Institution |
+ Created |
+ Updated |
+ Actions |
+
+
+
+ {assignments.map((assignment) => (
+ handleClick(assignment.id)}>
+ | {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]}
+
+ ))}
+
+
+ {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;