findDroppedBootcampStudents();
26 | }
27 |
--------------------------------------------------------------------------------
/back-end/src/main/resources/application-dev.properties:
--------------------------------------------------------------------------------
1 | #spring.datasource.url=jdbc:mysql://localhost:3306/assignment_submission_db
2 | #spring.datasource.username=example_user
3 | #spring.datasource.password=password123
4 |
5 | #spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
6 | #spring.jpa.show-sql=true
7 | #spring.jpa.hibernate.ddl-auto=update
8 |
9 | # coderscampus.com/bootcamp
10 |
11 | cookies.domain=localhost
12 | domainRoot=http://localhost:3000
13 |
14 | spring.security.filter.order=10
15 |
16 | server.port=8081
--------------------------------------------------------------------------------
/back-end/src/main/resources/application-local.properties:
--------------------------------------------------------------------------------
1 | #spring.datasource.url=jdbc:mysql://localhost:3306/assignment_submission_db
2 | #spring.datasource.username=example_user
3 | #spring.datasource.password=password123
4 |
5 | #spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
6 | #spring.jpa.show-sql=true
7 | #spring.jpa.properties.hibernate.format_sql=true
8 | #spring.jpa.hibernate.ddl-auto=update
9 |
10 | # coderscampus.com/bootcamp
11 |
12 | cookies.domain=localhost
13 | domainRoot=http://localhost:3000
14 |
15 | spring.security.filter.order=10
16 |
17 | server.port=8081
--------------------------------------------------------------------------------
/back-end/src/main/resources/application-prod.properties:
--------------------------------------------------------------------------------
1 | #spring.datasource.url=jdbc:mysql://localhost:3306/assignment_submission_db
2 | #spring.datasource.username=example_user
3 | #spring.datasource.password=password123
4 |
5 | #spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
6 | #spring.jpa.show-sql=true
7 | #spring.jpa.hibernate.ddl-auto=update
8 |
9 | # coderscampus.com/bootcamp
10 |
11 | cookies.domain=coderscampus.com
12 | domainRoot=https://assignments.coderscampus.com
13 |
14 | spring.security.filter.order=10
15 |
16 | server.port=8081
17 |
--------------------------------------------------------------------------------
/back-end/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 | %white(%d{ISO8601}) %highlight(%-5level) [%white(%t)] %yellow(%C{1.}): %msg%n%throwable
11 |
12 |
13 |
14 |
15 |
17 | ${LOGS}/spring-boot-logger.log
18 |
20 | %d %p %C{1.} [%t] %m%n
21 |
22 |
23 |
25 |
26 | ${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log
27 |
28 |
30 | 10MB
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/back-end/src/test/java/com/coderscampus/AssignmentSubmissionApp/service/NotificationServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.coderscampus.AssignmentSubmissionApp.service;
2 |
3 | import com.coderscampus.AssignmentSubmissionApp.domain.Assignment;
4 | import com.coderscampus.AssignmentSubmissionApp.domain.User;
5 | import org.junit.jupiter.api.Test;
6 |
7 |
8 | class NotificationServiceTest {
9 |
10 | NotificationService sut = new NotificationService();
11 |
12 | void test() {
13 | Assignment testAssignment = new Assignment();
14 |
15 | User user = new User();
16 | user.setName("Trevor Page");
17 |
18 | testAssignment.setUser(user);
19 | testAssignment.setNumber(4);
20 | sut.sendCongratsOnAssignmentSubmissionSlackMessage(testAssignment, "C05UGL8F42H");
21 |
22 | }
23 |
24 | // @Test
25 | void code_reviewer_channel_slack_test () {
26 | User user = new User();
27 | user.setName("Trevor Page");
28 | Assignment testAssignment = new Assignment();
29 |
30 | testAssignment.setUser(user);
31 | testAssignment.setNumber(4);
32 | testAssignment.setStatus("SUBMITTED");
33 | sut.sendAssignmentStatusUpdateCodeReviewer("PENDING_SUBMISSION", testAssignment);
34 | }
35 |
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | back-end:
3 | image: 851518566978.dkr.ecr.us-east-1.amazonaws.com/assignment-submission-backend-dev
4 | ports:
5 | - 8081:8081
6 | environment:
7 | DB_URL: jdbc:mysql://18.209.158.33:3306/assignment_submission_db
8 | DB_USERNAME: example_user
9 | DB_PASSWORD: password123
10 | front-end:
11 | image: 851518566978.dkr.ecr.us-east-1.amazonaws.com/assignment-submission-frontend-dev
12 | ports:
13 | - 3000:3000
14 |
--------------------------------------------------------------------------------
/front-end/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .dockerignore
4 | Dockerfile
5 | Dockerfile.prod
--------------------------------------------------------------------------------
/front-end/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/front-end/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:17-alpine3.15 AS builder
2 | # Set working directory
3 | WORKDIR /app
4 | # Copy all files from current directory to working dir in image
5 | COPY . .
6 | # install node modules and build assets
7 | RUN npm install && npm run build
8 |
9 | # nginx state for serving content
10 | FROM nginx:alpine
11 |
12 | RUN rm /etc/nginx/conf.d/default.conf
13 |
14 | COPY nginx.conf /etc/nginx/conf.d
15 |
16 | # Set working directory to nginx asset directory
17 | WORKDIR /usr/share/nginx/html
18 | # Remove default nginx static assets
19 | RUN rm -rf ./*
20 | # Copy static assets from builder stage
21 | COPY --from=builder /app/build .
22 | # Containers run nginx with global directives and daemon off
23 | ENTRYPOINT ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/front-end/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/front-end/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | include /etc/nginx/mime.types;
4 |
5 | location / {
6 | root /usr/share/nginx/html;
7 | index index.html index.htm;
8 | try_files $uri $uri/ /index.html;
9 | }
10 |
11 | location /api {
12 | try_files $uri @proxy_api;
13 | }
14 |
15 | location @proxy_api {
16 | proxy_set_header X-Forwarded-Proto https;
17 | proxy_set_header X-Url-Scheme $scheme;
18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
19 | proxy_set_header Host $http_host;
20 | proxy_redirect off;
21 | #proxy_pass http://host.docker.internal:8081; # for local
22 | proxy_pass http://localhost:8081; # for dev
23 | }
24 | }
--------------------------------------------------------------------------------
/front-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:8081/",
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.16.1",
8 | "@testing-library/react": "^12.1.2",
9 | "@testing-library/user-event": "^13.5.0",
10 | "bootstrap": "^5.1.3",
11 | "custom-react-step-progress-bar": "^1.0.8",
12 | "dayjs": "^1.10.7",
13 | "http-proxy-middleware": "^2.0.2",
14 | "js-cookie": "^3.0.1",
15 | "jwt-check-expiration": "^1.0.5",
16 | "moment": "^2.29.4",
17 | "react": "^17.0.2",
18 | "react-bootstrap": "^2.1.2",
19 | "react-dom": "^17.0.2",
20 | "react-router-dom": "^6.2.1",
21 | "react-scripts": "5.0.0",
22 | "react-step-progress-bar": "^1.0.3",
23 | "sass": "^1.49.8",
24 | "styled-components": "^5.3.3",
25 | "web-vitals": "^2.1.2"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "deploy": "aws s3 sync build/ s3://assignments.coderscampus.com --acl public-read",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/front-end/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/public/favicon.ico
--------------------------------------------------------------------------------
/front-end/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Assignment Submission
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/front-end/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/public/logo192.png
--------------------------------------------------------------------------------
/front-end/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/public/logo512.png
--------------------------------------------------------------------------------
/front-end/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/front-end/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/front-end/src/App.css:
--------------------------------------------------------------------------------
1 | .assignment-wrapper {
2 | border: 1px dashed lightgray;
3 | padding: 2em;
4 | border-radius: 1em;
5 | margin-top: 2em;
6 | }
7 |
8 | .assignment-wrapper-title {
9 | width: min-content !important;
10 | margin-top: -2em !important;
11 | margin-bottom: 1em !important;
12 | background-color: white !important;
13 | white-space: nowrap !important;
14 | }
15 |
16 | .error {
17 | color: red;
18 | }
19 |
20 | .link {
21 | text-decoration: none;
22 | cursor: pointer;
23 | color: black;
24 | }
25 | .blue-link {
26 | text-decoration: none;
27 | cursor: pointer;
28 | color: blue;
29 | }
30 | .comment-bubble {
31 | background: #eeeeee;
32 | border-radius: 1em;
33 | padding: 1em;
34 | margin: 1em 0em;
35 | }
36 |
37 | .logo {
38 | padding: 0.5em 0em;
39 | max-width: 10em;
40 | }
41 |
42 | .nav {
43 | min-height: 100px;
44 | border-bottom: 1px solid grey;
45 |
46 | width: 100%;
47 | }
48 |
49 | .NavBar {
50 | padding-top: 25px;
51 | }
52 |
--------------------------------------------------------------------------------
/front-end/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import jwt_decode from "jwt-decode";
3 | import { Route, Routes } from "react-router-dom";
4 | import "./App.css";
5 | import AssignmentView from "./AssignmentView";
6 | import Dashboard from "./Dashboard";
7 | import CodeReviewerDashboard from "./CodeReviewerDashboard";
8 | import Homepage from "./Homepage";
9 | import Login from "./Login";
10 | import PrivateRoute from "./PrivateRoute";
11 | import "./custom.scss";
12 | import CodeReviewerAssignmentView from "./CodeReviewAssignmentView";
13 | import { useUser } from "./UserProvider";
14 | import InstructorDashboard from "./InstructorDashboard";
15 |
16 | function App() {
17 | const [roles, setRoles] = useState([]);
18 | const user = useUser();
19 |
20 | useEffect(() => {
21 | setRoles(getRolesFromJWT());
22 | }, [user.jwt]);
23 |
24 | function getRolesFromJWT() {
25 | if (user.jwt) {
26 | const decodedJwt = jwt_decode(user.jwt);
27 | return decodedJwt.authorities;
28 | }
29 | return [];
30 | }
31 | return (
32 |
33 | role === "ROLE_CODE_REVIEWER") ? (
37 |
38 |
39 |
40 | ) : (
41 |
42 |
43 |
44 | )
45 | }
46 | />
47 | role === "ROLE_INSTRUCTOR") ? (
51 |
52 |
53 |
54 | ) : (
55 | You don't have the appropriate role. Talk to Trevor.
56 | )
57 | }
58 | />
59 | role === "ROLE_CODE_REVIEWER") ? (
63 |
64 |
65 |
66 | ) : (
67 |
68 |
69 |
70 | )
71 | }
72 | />
73 | } />
74 | } />
75 |
76 | );
77 | }
78 |
79 | export default App;
80 |
--------------------------------------------------------------------------------
/front-end/src/AssignmentView/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import {
3 | Button,
4 | ButtonGroup,
5 | Col,
6 | Container,
7 | Dropdown,
8 | DropdownButton,
9 | Form,
10 | Row,
11 | } from "react-bootstrap";
12 | import ajax from "../Services/fetchService";
13 | import StatusBadge from "../StatusBadge";
14 | import { useNavigate, useParams } from "react-router-dom";
15 | import { useUser } from "../UserProvider";
16 | import CommentContainer from "../CommentContainer";
17 | import NavBar from "../NavBar";
18 | import { getButtonsByStatusAndRole } from "../Services/statusService";
19 |
20 | const AssignmentView = () => {
21 | let navigate = useNavigate();
22 | const user = useUser();
23 | const { assignmentId } = useParams();
24 |
25 | // const assignmentId = window.location.href.split("/assignments/")[1];
26 | const [assignment, setAssignment] = useState({
27 | branch: "",
28 | githubUrl: "",
29 | number: null,
30 | status: null,
31 | });
32 |
33 | const [assignmentEnums, setAssignmentEnums] = useState([]);
34 | const [assignmentStatuses, setAssignmentStatuses] = useState([]);
35 |
36 | const prevAssignmentValue = useRef(assignment);
37 |
38 | function updateAssignment(prop, value) {
39 | const newAssignment = { ...assignment };
40 | newAssignment[prop] = value;
41 | setAssignment(newAssignment);
42 | }
43 |
44 | function save(status) {
45 | // this implies that the student is submitting the assignment for the first time
46 |
47 | if (status && assignment.status !== status) {
48 | updateAssignment("status", status);
49 | } else {
50 | persist();
51 | }
52 | }
53 |
54 | function persist() {
55 | ajax(`/api/assignments/${assignmentId}`, "PUT", user.jwt, assignment).then(
56 | (assignmentData) => {
57 | setAssignment(assignmentData);
58 | }
59 | );
60 | }
61 | useEffect(() => {
62 | if (prevAssignmentValue.current.status !== assignment.status) {
63 | persist();
64 | }
65 | prevAssignmentValue.current = assignment;
66 | }, [assignment]);
67 |
68 | useEffect(() => {
69 | ajax(`/api/assignments/${assignmentId}`, "GET", user.jwt).then(
70 | (assignmentResponse) => {
71 | let assignmentData = assignmentResponse.assignment;
72 | if (assignmentData.branch === null) assignmentData.branch = "";
73 | if (assignmentData.githubUrl === null) assignmentData.githubUrl = "";
74 | setAssignment(assignmentData);
75 | setAssignmentEnums(assignmentResponse.assignmentEnums);
76 | setAssignmentStatuses(assignmentResponse.statusEnums);
77 | }
78 | );
79 | }, []);
80 |
81 | return (
82 | <>
83 |
84 |
85 |
86 |
87 | {assignment && assignment.number && assignmentEnums.length > 0 ? (
88 | <>
89 | Assignment {assignment.number}
90 | {assignmentEnums[assignment.number - 1].assignmentName}
91 | >
92 | ) : (
93 | <>>
94 | )}
95 |
96 |
97 | {assignment ? : <>>}
98 |
99 |
100 | {assignment ? (
101 | <>
102 |
103 |
104 | Assignment Number:
105 |
106 |
107 | {
116 | updateAssignment("number", selectedElement);
117 | }}
118 | >
119 | {assignmentEnums.map((assignmentEnum) => (
120 |
124 | {assignmentEnum.assignmentNum}
125 |
126 | ))}
127 |
128 |
129 |
130 |
131 |
132 | GitHub URL:
133 |
134 |
135 |
137 | updateAssignment("githubUrl", e.target.value)
138 | }
139 | type="url"
140 | value={assignment.githubUrl}
141 | placeholder="https://github.com/username/repo-name"
142 | />
143 |
144 |
145 |
146 |
147 |
148 | Branch:
149 |
150 |
151 | updateAssignment("branch", e.target.value)}
155 | value={assignment.branch}
156 | />
157 |
158 |
159 |
160 |
161 | {getButtonsByStatusAndRole(assignment.status, "student").map(
162 | (btn) => (
163 | {
168 | if (btn.nextStatus === "Same") persist();
169 | else save(btn.nextStatus);
170 | }}
171 | >
172 | {btn.text}
173 |
174 | )
175 | )}
176 |
177 |
178 | >
179 | ) : (
180 | <>>
181 | )}
182 |
183 | >
184 | );
185 | };
186 |
187 | export default AssignmentView;
188 |
--------------------------------------------------------------------------------
/front-end/src/CodeReviewAssignmentView/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { Button, Col, Container, Form, Row } from "react-bootstrap";
3 | import CommentContainer from "../CommentContainer";
4 | import NavBar from "../NavBar";
5 | import ajax from "../Services/fetchService";
6 | import { getButtonsByStatusAndRole } from "../Services/statusService";
7 | import StatusBadge from "../StatusBadge";
8 | import { useUser } from "../UserProvider";
9 |
10 | const CodeReviewerAssignmentView = () => {
11 | const user = useUser();
12 | const assignmentId = window.location.href.split("/assignments/")[1];
13 | const [assignment, setAssignment] = useState({
14 | branch: "",
15 | githubUrl: "",
16 | number: null,
17 | status: null,
18 | codeReviewVideoUrl: null,
19 | });
20 | const [assignmentEnums, setAssignmentEnums] = useState([]);
21 | const [assignmentStatuses, setAssignmentStatuses] = useState([]);
22 | const [errorMsg, setErrorMsg] = useState("");
23 | const prevAssignmentValue = useRef(assignment);
24 |
25 | function updateAssignment(prop, value) {
26 | const newAssignment = { ...assignment };
27 | newAssignment[prop] = value;
28 | setAssignment(newAssignment);
29 | }
30 |
31 | function save(status) {
32 | setErrorMsg("");
33 | if (
34 | status === "Completed" &&
35 | (assignment.codeReviewVideoUrl === null ||
36 | assignment.codeReviewVideoUrl === "")
37 | ) {
38 | setErrorMsg(
39 | "Please insert the URL to the video review for the student to watch."
40 | );
41 | return;
42 | }
43 | if (status && assignment.status !== status) {
44 | updateAssignment("status", status);
45 | } else {
46 | persist();
47 | }
48 | }
49 |
50 | function persist() {
51 | ajax(`/api/assignments/${assignmentId}`, "PUT", user.jwt, assignment).then(
52 | (assignmentData) => {
53 | setAssignment(assignmentData);
54 | }
55 | );
56 | }
57 | useEffect(() => {
58 | if (
59 | assignment &&
60 | prevAssignmentValue.current.status !== assignment.status
61 | ) {
62 | persist();
63 | }
64 | prevAssignmentValue.current = assignment;
65 | }, [assignment]);
66 |
67 | useEffect(() => {
68 | ajax(`/api/assignments/${assignmentId}`, "GET", user.jwt).then(
69 | (assignmentResponse) => {
70 | let assignmentData = assignmentResponse.assignment;
71 | if (assignmentData.branch === null) assignmentData.branch = "";
72 | if (assignmentData.githubUrl === null) assignmentData.githubUrl = "";
73 |
74 | setAssignment(assignmentData);
75 | setAssignmentEnums(assignmentResponse.assignmentEnums);
76 | setAssignmentStatuses(assignmentResponse.statusEnums);
77 | }
78 | );
79 | }, []);
80 |
81 | return (
82 | <>
83 |
84 |
85 |
86 |
87 | {assignment && assignment.number ? (
88 | Assignment {assignment.number}
89 | ) : (
90 | <>>
91 | )}
92 |
93 |
94 | {assignment ? : <>>}
95 |
96 |
97 |
98 | {errorMsg}
99 |
100 |
101 | {assignment ? (
102 | <>
103 | {assignment.user ? (
104 |
105 |
106 | Student Name:
107 |
108 |
109 |
114 |
115 |
116 | ) : (
117 | <>>
118 | )}
119 |
120 |
121 | GitHub URL:
122 |
123 |
124 |
126 | updateAssignment("githubUrl", e.target.value)
127 | }
128 | type="url"
129 | readOnly
130 | value={assignment.githubUrl}
131 | placeholder="https://github.com/username/repo-name"
132 | />
133 |
134 |
135 |
136 |
137 |
138 | Branch:
139 |
140 |
141 | updateAssignment("branch", e.target.value)}
146 | value={assignment.branch}
147 | />
148 |
149 |
150 |
151 |
152 |
153 | Video Review URL:
154 |
155 |
156 |
158 | updateAssignment("codeReviewVideoUrl", e.target.value)
159 | }
160 | type="url"
161 | value={assignment.codeReviewVideoUrl}
162 | placeholder="https://screencast-o-matic.com/something"
163 | />
164 |
165 |
166 |
167 |
168 | {getButtonsByStatusAndRole(
169 | assignment.status,
170 | "code_reviewer"
171 | ).map((btn) => (
172 | save(btn.nextStatus)}
176 | >
177 | {btn.text}
178 |
179 | ))}
180 |
181 |
182 |
183 | >
184 | ) : (
185 | <>>
186 | )}
187 |
188 | >
189 | );
190 | };
191 |
192 | export default CodeReviewerAssignmentView;
193 |
--------------------------------------------------------------------------------
/front-end/src/Comment/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useUser } from "../UserProvider";
3 | import jwt_decode from "jwt-decode";
4 | import dayjs from "dayjs";
5 | import relativeTime from "dayjs/plugin/relativeTime";
6 |
7 | const Comment = (props) => {
8 | const user = useUser();
9 | const decodedJwt = jwt_decode(user.jwt);
10 | const { id, createdDate, createdBy, text } = props.commentData;
11 | const { emitEditComment, emitDeleteComment } = props;
12 | const [commentRelativeTime, setCommentRelativeTime] = useState("");
13 |
14 | useEffect(() => {
15 | updateCommentRelativeTime();
16 | }, [createdDate]);
17 |
18 | function updateCommentRelativeTime() {
19 | if (createdDate) {
20 | dayjs.extend(relativeTime);
21 |
22 | if (typeof createdDate === "string")
23 | setCommentRelativeTime(dayjs(createdDate).fromNow());
24 | else {
25 | setCommentRelativeTime(createdDate.fromNow());
26 | }
27 | }
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 |
{`${createdBy.name}`}
35 | {decodedJwt.sub === createdBy.username ? (
36 | <>
37 |
emitEditComment(id)}
39 | style={{ cursor: "pointer", color: "blue" }}
40 | >
41 | edit
42 |
43 |
emitDeleteComment(id)}
45 | style={{ cursor: "pointer", color: "red" }}
46 | >
47 | delete
48 |
49 | >
50 | ) : (
51 | <>>
52 | )}
53 |
54 |
{text}
55 |
56 |
57 |
60 | {commentRelativeTime ? `Posted ${commentRelativeTime}` : ""}
61 |
62 | >
63 | );
64 | };
65 |
66 | export default Comment;
67 |
--------------------------------------------------------------------------------
/front-end/src/CommentContainer/index.js:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import React, { useEffect, useState } from "react";
3 | import { Button, Col, Row } from "react-bootstrap";
4 | import Comment from "../Comment";
5 | import ajax from "../Services/fetchService";
6 | import { useUser } from "../UserProvider";
7 | import { useInterval } from "../util/useInterval";
8 |
9 | const CommentContainer = (props) => {
10 | const { assignmentId } = props;
11 | const user = useUser();
12 |
13 | const emptyComment = {
14 | id: null,
15 | text: "",
16 | assignmentId: assignmentId != null ? parseInt(assignmentId) : null,
17 | user: user.jwt,
18 | createdDate: null,
19 | };
20 |
21 | const [comment, setComment] = useState(emptyComment);
22 | const [comments, setComments] = useState([]);
23 |
24 | useInterval(() => {
25 | updateCommentTimeDisplay();
26 | }, 1000 * 5);
27 | function updateCommentTimeDisplay() {
28 | const commentsCopy = [...comments];
29 | commentsCopy.forEach(
30 | (comment) => (comment.createdDate = dayjs(comment.createdDate))
31 | );
32 | formatComments(commentsCopy);
33 | }
34 |
35 | function handleEditComment(commentId) {
36 | const i = comments.findIndex((comment) => comment.id === commentId);
37 | const commentCopy = {
38 | id: comments[i].id,
39 | text: comments[i].text,
40 | assignmentId: assignmentId != null ? parseInt(assignmentId) : null,
41 | user: user.jwt,
42 | createdDate: comments[i].createdDate,
43 | };
44 | setComment(commentCopy);
45 | }
46 |
47 | function handleDeleteComment(commentId) {
48 | // TODO: send DELETE request to server
49 | ajax(`/api/comments/${commentId}`, "delete", user.jwt).then((msg) => {
50 | const commentsCopy = [...comments];
51 | const i = commentsCopy.findIndex((comment) => comment.id === commentId);
52 | commentsCopy.splice(i, 1);
53 | formatComments(commentsCopy);
54 | });
55 | }
56 | function formatComments(commentsCopy) {
57 | commentsCopy.forEach((comment) => {
58 | if (typeof comment.createDate === "string") {
59 | comment.createDate = dayjs(comment.createDate);
60 | }
61 | });
62 | setComments(commentsCopy);
63 | }
64 |
65 | useEffect(() => {
66 | ajax(
67 | `/api/comments?assignmentId=${assignmentId}`,
68 | "get",
69 | user.jwt,
70 | null
71 | ).then((commentsData) => {
72 | formatComments(commentsData);
73 | });
74 | }, []);
75 |
76 | function updateComment(value) {
77 | const commentCopy = { ...comment };
78 | commentCopy.text = value;
79 | setComment(commentCopy);
80 | }
81 | function submitComment() {
82 | // if (
83 | // typeof comment.createdDate === "object" &&
84 | // comment.createdDate != null
85 | // ) {
86 | // comment.createdDate = comment.createdDate.toDate();
87 | // }
88 | if (comment.id) {
89 | ajax(`/api/comments/${comment.id}`, "put", user.jwt, comment).then(
90 | (d) => {
91 | const commentsCopy = [...comments];
92 | const i = commentsCopy.findIndex((comment) => comment.id === d.id);
93 | commentsCopy[i] = d;
94 | formatComments(commentsCopy);
95 |
96 | setComment(emptyComment);
97 | }
98 | );
99 | } else {
100 | ajax("/api/comments", "post", user.jwt, comment).then((d) => {
101 | const commentsCopy = [...comments];
102 | commentsCopy.push(d);
103 | formatComments(commentsCopy);
104 | setComment(emptyComment);
105 | });
106 | }
107 | }
108 | return (
109 | <>
110 |
111 |
Comments
112 |
113 |
114 |
115 |
120 |
121 |
122 | submitComment()}>Post Comment
123 |
124 | {comments.map((comment) => (
125 |
131 | ))}
132 |
133 | >
134 | );
135 | };
136 |
137 | export default CommentContainer;
138 |
--------------------------------------------------------------------------------
/front-end/src/Dashboard/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Button, Card, Col, Row } from "react-bootstrap";
3 | import { useNavigate } from "react-router-dom";
4 | import NavBar from "../NavBar";
5 | import ajax from "../Services/fetchService";
6 | import StatusBadge from "../StatusBadge";
7 | import { useUser } from "../UserProvider";
8 | import MultiColorProgressBar from "../MultiColorProgressBar";
9 | import jwt_decode from "jwt-decode";
10 | import { getDueDates } from "../Services/assignmentDueDatesService";
11 |
12 | const Dashboard = () => {
13 | const navigate = useNavigate();
14 | const user = useUser();
15 | const [assignments, setAssignments] = useState(null);
16 | const [userData, setUserData] = useState(null);
17 | const [asignmentDueDates, setAssignmentDueDates] = useState(null);
18 |
19 | useEffect(() => {
20 | const decodedJwt = jwt_decode(user.jwt);
21 | if (!userData && assignments) {
22 | ajax("api/users/" + decodedJwt.sub, "GET", user.jwt).then((data) => {
23 | setUserData(data);
24 | let dueDates = getDueDates(
25 | data.cohortStartDate,
26 | data.bootcampDurationInWeeks,
27 | assignments
28 | );
29 |
30 | setAssignmentDueDates(dueDates);
31 | });
32 | }
33 | }, [user, userData, assignments]);
34 |
35 | useEffect(() => {
36 | ajax("api/assignments", "GET", user.jwt).then((assignmentsData) => {
37 | setAssignments(assignmentsData);
38 | });
39 | if (!user.jwt) {
40 | console.warn("No valid jwt found, redirecting to login page");
41 | navigate("/login");
42 | }
43 | }, [user.jwt]);
44 |
45 | function createAssignment() {
46 | ajax("api/assignments", "POST", user.jwt).then((assignment) => {
47 | navigate(`/assignments/${assignment.id}`);
48 | // window.location.href = `/assignments/${assignment.id}`;
49 | });
50 | }
51 | return (
52 | <>
53 |
54 |
55 | {asignmentDueDates ? (
56 |
57 | ) : (
58 | <>>
59 | )}
60 |
61 |
62 |
66 | {assignments &&
67 | assignments.map((assignment) => (
68 | //
69 |
73 |
74 | Assignment #{assignment.number}
75 |
76 |
77 |
78 |
79 |
80 | GitHub URL : {assignment.githubUrl}
81 |
82 | Branch : {assignment.branch}
83 |
84 |
85 | {assignment && assignment.status === "Completed" ? (
86 | <>
87 | {
89 | window.open(assignment.codeReviewVideoUrl);
90 | }}
91 | className="mb-4"
92 | >
93 | Watch Review
94 |
95 | {
98 | navigate(`/assignments/${assignment.id}`);
99 | }}
100 | >
101 | View
102 |
103 | >
104 | ) : (
105 | {
108 | navigate(`/assignments/${assignment.id}`);
109 | }}
110 | >
111 | Edit
112 |
113 | )}
114 |
115 |
116 | //
117 | ))}
118 |
119 |
120 | createAssignment()}>
121 | Submit New Assignment
122 |
123 |
124 |
125 |
126 |
127 | >
128 | );
129 | };
130 |
131 | export default Dashboard;
132 |
--------------------------------------------------------------------------------
/front-end/src/Homepage/homepage.css:
--------------------------------------------------------------------------------
1 | .main-container{
2 | height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 | .wrapper{
7 | display:flex;
8 | flex-direction: column;
9 | flex-grow: 1;
10 | background: linear-gradient(to bottom, #8ad2d4 50% , #243f93 50%);
11 | }
12 | .top-half{
13 | padding-top: 3rem;
14 | display:inline-block;
15 | height:50%;
16 | }
17 | .welcome {
18 | color: #243f93;
19 | }
20 | .bottom-half{
21 | display:inline-block;
22 | height: 50%;
23 | margin-top: 3rem;
24 | }
25 | .bottom-container{
26 | display: flex;
27 | justify-content: space-between;
28 | }
29 | .welcome-text{
30 | color: white;
31 | max-width: 500px;
32 | }
33 | .assignment{
34 | max-width: 20em;
35 | margin-right: 20rem;
36 | }
37 |
--------------------------------------------------------------------------------
/front-end/src/Homepage/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container } from "react-bootstrap";
3 | import NavBar from "../NavBar";
4 | import assignment from "../Images/coders-campus-assignment-example.png";
5 |
6 | import './homepage.css';
7 |
8 | const Homepage = () => {
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | Welcome Fellow Coders
17 |
18 | Coders Campus Assignment Submission Form is a tool to make turning in assignments more convenient.
19 | Students can insert Github links of their code for each individual assignment so a reviewer can clone code.
20 |
21 |
22 |
23 |
24 |
25 | With these tools students can get personalized video feedback from coding experts. →
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Homepage;
35 |
--------------------------------------------------------------------------------
/front-end/src/Images/coders-campus-assignment-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/src/Images/coders-campus-assignment-example.png
--------------------------------------------------------------------------------
/front-end/src/Images/coders-campus-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/src/Images/coders-campus-logo-dark.png
--------------------------------------------------------------------------------
/front-end/src/Images/coders-campus-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tp02ga/AssignmentSubmissionApp/d76607ed82e5a43672c9ddb8af8947a4b21fa1bb/front-end/src/Images/coders-campus-logo.png
--------------------------------------------------------------------------------
/front-end/src/InstructorDashboard/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import NavBar from "../NavBar";
3 | import { Col, Container, Row } from "react-bootstrap";
4 | import ajax from "../Services/fetchService";
5 | import { useUser } from "../UserProvider";
6 | import { numAssignmentsThatShouldBeCompleted } from "../Services/assignmentDueDatesService";
7 | import dayjs from "dayjs";
8 | import "./instructor-dashboard.css";
9 | import InstructorStudentEditModal from "../InstructorStudentEditModal";
10 |
11 | const InstructorDashboard = () => {
12 | const user = useUser();
13 | const [userAssignments, setUserAssignments] = useState(null);
14 | const [nonConfiguredUsers, setNonConfiguredUsers] = useState(null);
15 | const [bootcampStudents, setBootcampStudents] = useState(null);
16 | const [studentEditModal, setStudentEditModal] = useState(<>>);
17 |
18 | const handleCloseStudentEdit = () => {
19 | setStudentEditModal(<>>);
20 | };
21 |
22 | const handleSaveStudent = (data) => {
23 | ajax(`/api/users/${data.email}`, "put", user.jwt, data).then(() => {
24 | handleCloseStudentEdit();
25 | });
26 | };
27 |
28 | useEffect(() => {
29 | ajax("/api/assignments/all", "get", user.jwt).then((data) => {
30 | setUserAssignments(data);
31 | });
32 |
33 | ajax("/api/users/bootcamp-students", "get", user.jwt).then((data) => {
34 | setBootcampStudents(data);
35 | });
36 | }, []);
37 |
38 | useEffect(() => {
39 | if (userAssignments && bootcampStudents) {
40 | const nonConfigured = bootcampStudents.filter((student) => {
41 | let found = false;
42 | Object.entries(userAssignments).forEach((entry) => {
43 | const key = JSON.parse(entry[0]);
44 | if (key.email === student.email) {
45 | found = true;
46 | }
47 | });
48 | return !found;
49 | });
50 | setNonConfiguredUsers(nonConfigured);
51 | }
52 | }, [userAssignments, bootcampStudents]);
53 |
54 | const getColor = (delta) => {
55 | if (delta <= -1) {
56 | return "#b2e0b2"; // green
57 | } else if (delta === 1) {
58 | return "yellow"; // yellow
59 | } else if (delta === 2) {
60 | return "orange"; // orange
61 | } else if (delta === 3) {
62 | return "#f65555"; // red
63 | } else if (delta >= 4) {
64 | return "#c70000";
65 | }
66 | };
67 | return (
68 | <>
69 |
70 |
71 | {studentEditModal}
72 |
73 |
74 | Instructor Dashboard
75 |
76 |
77 | {userAssignments && nonConfiguredUsers ? (
78 |
79 |
80 |
81 | Cohort
82 | Name
83 | Email
84 | Assignments Submitted
85 | Assignments Expected
86 |
87 |
88 |
89 | {nonConfiguredUsers
90 | .filter((student) => {
91 | return student.bootcampDurationInWeeks && student.startDate;
92 | })
93 | .map((student) => {
94 | let numAssignmentsToBeDone =
95 | numAssignmentsThatShouldBeCompleted(
96 | student.startDate,
97 | student.bootcampDurationInWeeks
98 | );
99 | let numAssignmentsDone = 0;
100 | return (
101 |
102 |
103 | {dayjs(student.startDate, "YYYY-M-D").format(
104 | "MMM YYYY"
105 | )}
106 |
107 |
114 | {student.name}
115 |
116 | {student.email}
117 | {numAssignmentsDone}
118 | {numAssignmentsToBeDone}
119 |
120 | );
121 | })}
122 | {Object.entries(userAssignments).map((entry) => {
123 | const [keyData, value] = entry;
124 | let key = JSON.parse(keyData);
125 | let numAssignmentsToBeDone =
126 | numAssignmentsThatShouldBeCompleted(
127 | key.startDate,
128 | key.bootcampDurationInWeeks
129 | );
130 | let numAssignmentsDone = value.length;
131 | return (
132 |
133 |
134 | {dayjs(key.startDate, "YYYY-M-D").format("MMM YYYY")}
135 |
136 |
143 | {key.name}
144 |
145 | {key.email}
146 | {numAssignmentsDone}
147 | {numAssignmentsToBeDone}
148 |
149 | );
150 | })}
151 |
152 |
153 | ) : (
154 | <>>
155 | )}
156 |
157 | {nonConfiguredUsers ? (
158 | <>
159 | Non Configured Users
160 |
161 |
162 |
163 | Name
164 | Email
165 |
166 |
167 |
168 |
169 | {nonConfiguredUsers
170 | .filter((u) => !u.bootcampDurationInWeeks || !u.startDate)
171 | .map((u) => (
172 |
173 | {u.name}
174 | {u.email}
175 |
176 | {
179 | setStudentEditModal(
180 |
185 | );
186 | }}
187 | >
188 | configure
189 |
190 |
191 |
192 | ))}
193 |
194 |
195 | >
196 | ) : (
197 | <>>
198 | )}
199 |
200 | >
201 | );
202 | };
203 |
204 | export default InstructorDashboard;
205 |
--------------------------------------------------------------------------------
/front-end/src/InstructorDashboard/instructor-dashboard.css:
--------------------------------------------------------------------------------
1 | td,
2 | th {
3 | padding: 3px 15px;
4 | }
5 | tr:nth-child(even) {
6 | background: #eee;
7 | }
8 | tr:nth-child(odd) {
9 | background: #fff;
10 | }
11 |
--------------------------------------------------------------------------------
/front-end/src/InstructorStudentEditModal/index.js:
--------------------------------------------------------------------------------
1 | import { Form, Button, Modal } from "react-bootstrap";
2 | import React, { useEffect, useState } from "react";
3 | import { useUser } from "../UserProvider";
4 | import ajax from "../Services/fetchService";
5 |
6 | const InstructorStudentEditModal = (props) => {
7 | const { emitClose, emitSave, studentEmail } = props;
8 | const user = useUser();
9 | const [student, setStudent] = useState(null);
10 | useEffect(() => {
11 | ajax(`/api/users/${studentEmail}`, "get", user.jwt).then((data) => {
12 | if (!data) {
13 | let yesNo = window.confirm(
14 | "User doesn't exist in the Assignment Submission app, do you want to create the user?"
15 | );
16 | if (yesNo) {
17 | ajax(`/api/users/${studentEmail}`, "put", user.jwt, {
18 | email: studentEmail,
19 | }).then((data) => {
20 | emitClose();
21 | });
22 | } else {
23 | emitClose();
24 | }
25 | }
26 | data.email = data.username;
27 | setStudent(data);
28 | });
29 | }, []);
30 |
31 | const updateStudentData = (field, value) => {
32 | const studentCopy = { ...student };
33 | studentCopy[field] = value;
34 | setStudent(studentCopy);
35 | };
36 | return (
37 | <>
38 |
39 |
40 | Edit Student Info
41 |
42 | {student ? (
43 | <>
44 |
45 |
47 | Name
48 | updateStudentData("name", e.target.value)}
52 | >
53 |
54 |
55 | Email
56 |
61 |
62 |
63 | Start Date
64 |
68 | updateStudentData("startDate", e.target.value)
69 | }
70 | >
71 |
72 |
73 | Bootcamp Duration (Weeks)
74 |
78 | updateStudentData(
79 | "bootcampDurationInWeeks",
80 | e.target.value
81 | )
82 | }
83 | >
84 |
85 |
86 |
87 |
88 |
89 | Close
90 |
91 | {
94 | if (
95 | student.bootcampDurationInWeeks == 24 ||
96 | student.bootcampDurationInWeeks == 36
97 | ) {
98 | emitSave(student);
99 | } else {
100 | alert("Bootcamp Duration in Weeks must be 24 or 36");
101 | }
102 | }}
103 | >
104 | Save Changes
105 |
106 |
107 | >
108 | ) : (
109 | Loading...
110 | )}
111 |
112 | >
113 | );
114 | };
115 |
116 | export default InstructorStudentEditModal;
117 |
--------------------------------------------------------------------------------
/front-end/src/Login/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Col, Container, Row, Form } from "react-bootstrap";
3 | import { useNavigate } from "react-router-dom";
4 | import NavBar from "../NavBar";
5 | import { useUser } from "../UserProvider";
6 |
7 | const Login = () => {
8 | const user = useUser();
9 | const navigate = useNavigate();
10 | const [username, setUsername] = useState("");
11 | const [password, setPassword] = useState("");
12 | const [errorMsg, setErrorMsg] = useState(null);
13 |
14 | // useEffect(() => {
15 | // if (user.jwt) navigate("/dashboard");
16 | // }, [user]);
17 |
18 | function sendLoginRequest() {
19 | setErrorMsg("");
20 | const reqBody = {
21 | username: username,
22 | password: password,
23 | };
24 |
25 | fetch("api/auth/login", {
26 | headers: {
27 | "Content-Type": "application/json",
28 | },
29 | method: "post",
30 | body: JSON.stringify(reqBody),
31 | })
32 | .then((response) => {
33 | if (response.status === 200) return response.text();
34 | else if (response.status === 401 || response.status === 403) {
35 | setErrorMsg("Invalid username or password");
36 | } else {
37 | setErrorMsg(
38 | "Something went wrong, try again later or reach out to trevor@coderscampus.com"
39 | );
40 | }
41 | })
42 | .then((data) => {
43 | if (data) {
44 | user.setJwt(data);
45 | navigate("/dashboard");
46 | }
47 | });
48 | }
49 | return (
50 | <>
51 |
52 |
53 |
54 |
55 |
56 | Username
57 | setUsername(e.target.value)}
63 | />
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Password
72 | setPassword(e.target.value)}
78 | />
79 |
80 |
81 |
82 | {errorMsg ? (
83 |
84 |
85 |
86 | {errorMsg}
87 |
88 |
89 |
90 | ) : (
91 | <>>
92 | )}
93 |
94 |
99 | sendLoginRequest()}
104 | >
105 | Login
106 |
107 | {
112 | navigate("/");
113 | }}
114 | >
115 | Exit
116 |
117 |
118 |
119 |
120 | >
121 | );
122 | };
123 |
124 | export default Login;
125 |
--------------------------------------------------------------------------------
/front-end/src/MultiColorProgressBar/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./multicolor-progress-bar.css";
3 |
4 | class MultiColorProgressBar extends React.Component {
5 | render() {
6 | const parent = this.props;
7 |
8 | let values =
9 | parent.readings &&
10 | parent.readings.length &&
11 | parent.readings.map(function (item, i) {
12 | if (item.value > 0) {
13 | return (
14 |
19 |
{item.name}
20 |
{item.dueDate}
21 |
22 | );
23 | }
24 | }, this);
25 |
26 | let calibrations =
27 | parent.readings &&
28 | parent.readings.length &&
29 | parent.readings.map(function (item, i) {
30 | if (item.value > 0) {
31 | return (
32 |
37 | |
38 |
39 | );
40 | }
41 | }, this);
42 |
43 | let bars =
44 | parent.readings &&
45 | parent.readings.length &&
46 | parent.readings.map(function (item, i) {
47 | if (item.value > 0) {
48 | return (
49 |
54 | );
55 | }
56 | }, this);
57 |
58 | return (
59 |
60 |
{values == "" ? "" : values}
61 |
{calibrations == "" ? "" : calibrations}
62 |
{bars == "" ? "" : bars}
63 |
64 | );
65 | }
66 | }
67 |
68 | export default MultiColorProgressBar;
69 |
--------------------------------------------------------------------------------
/front-end/src/MultiColorProgressBar/multicolor-progress-bar.css:
--------------------------------------------------------------------------------
1 | .multicolor-bar {
2 | margin: 20px 20%;
3 | }
4 |
5 | .multicolor-bar .values .value {
6 | float: left;
7 | text-align: center;
8 | }
9 |
10 | .multicolor-bar .scale .graduation {
11 | float: left;
12 | text-align: center;
13 | }
14 |
15 | .multicolor-bar .bars .bar {
16 | float: left;
17 | height: 10px;
18 | }
19 |
20 | .multicolor-bar .bars div.bar:first-of-type {
21 | border-top-left-radius: 5px;
22 | border-bottom-left-radius: 5px;
23 | }
24 |
25 | .multicolor-bar .bars div.bar:last-of-type {
26 | border-top-right-radius: 5px;
27 | border-bottom-right-radius: 5px;
28 | }
29 |
30 | .multicolor-bar .legends {
31 | text-align: center;
32 | }
33 |
34 | .multicolor-bar .legends .legend {
35 | display: inline-block;
36 | margin: 0 5px;
37 | text-align: center;
38 | }
39 |
40 | .multicolor-bar .legends .legend .dot {
41 | font-size: 25px;
42 | vertical-align: middle;
43 | }
44 |
45 | .multicolor-bar .legends .legend .label {
46 | margin-left: 2px;
47 | vertical-align: middle;
48 | }
49 |
--------------------------------------------------------------------------------
/front-end/src/NavBar/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Link, useNavigate, useLocation } from "react-router-dom";
3 | import { Button, Image } from "react-bootstrap";
4 | import logo from "../Images/coders-campus-logo.png";
5 | import { useUser } from "../UserProvider";
6 | import jwt_decode from "jwt-decode";
7 |
8 | function NavBar() {
9 | const navigate = useNavigate();
10 | const { pathname } = useLocation();
11 | const user = useUser();
12 | const [authorities, setAuthorities] = useState(null);
13 |
14 | useEffect(() => {
15 | if (user && user.jwt) {
16 | const decodedJwt = jwt_decode(user.jwt);
17 | setAuthorities(decodedJwt.authorities);
18 | }
19 | }, [user, user.jwt]);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {user && user.jwt ? (
30 | {
33 | // TODO: have this delete cookie on server side
34 | fetch("/api/auth/logout").then((response) => {
35 | if (response.status === 200) {
36 | user.setJwt(null);
37 | navigate("/");
38 | }
39 | });
40 | }}
41 | >
42 | Logout
43 |
44 | ) : pathname !== "/login" ? (
45 | {
49 | navigate("/login");
50 | }}
51 | >
52 | Login
53 |
54 | ) : (
55 | <>>
56 | )}
57 |
58 | {authorities &&
59 | authorities.filter((auth) => auth === "ROLE_INSTRUCTOR").length > 0 ? (
60 |
64 | Instructors
65 |
66 | ) : (
67 | <>>
68 | )}
69 |
70 | {user && user.jwt ? (
71 | {
74 | navigate("/dashboard");
75 | }}
76 | >
77 | Dashboard
78 |
79 | ) : (
80 | <>>
81 | )}
82 |
83 |
84 | );
85 | }
86 |
87 | export default NavBar;
88 |
--------------------------------------------------------------------------------
/front-end/src/PrivateRoute/index.js:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 | import { useUser } from "../UserProvider";
3 | import { useState, useEffect } from "react";
4 | import ajax from "../Services/fetchService";
5 |
6 | const PrivateRoute = (props) => {
7 | const user = useUser();
8 | const [isLoading, setIsLoading] = useState(true);
9 | const [isValid, setIsValid] = useState(null);
10 | const { children } = props;
11 |
12 | if (user && user.jwt) {
13 | ajax(`/api/auth/validate`, "get", user.jwt).then((isValid) => {
14 | setIsValid(isValid);
15 | setIsLoading(false);
16 | });
17 | } else {
18 | return ;
19 | }
20 |
21 | return isLoading ? (
22 | Loading...
23 | ) : isValid === true ? (
24 | children
25 | ) : (
26 |
27 | );
28 | };
29 |
30 | export default PrivateRoute;
31 |
--------------------------------------------------------------------------------
/front-end/src/Register/index.js:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 | import React, { useState, useEffect } from "react";
3 | import { Button, Col, Container, Row, Form } from "react-bootstrap";
4 | import { useNavigate } from "react-router-dom";
5 |
6 | import { useUser } from "../UserProvider";
7 |
8 | const Register = () => {
9 | const user = useUser();
10 | const navigate = useNavigate();
11 | const [username, setUsername] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [name, setName] = useState("");
14 |
15 | useEffect(() => {
16 | if (user.jwt) navigate("/dashboard");
17 | }, [user]);
18 |
19 | function createAndLoginUser() {
20 | const reqBody = {
21 | username: username,
22 | password: password,
23 | name: name,
24 | };
25 |
26 | fetch("api/users/register", {
27 | headers: {
28 | "Content-Type": "application/json",
29 | },
30 | method: "post",
31 | body: JSON.stringify(reqBody),
32 | })
33 | .then((response) => {
34 | if (response.status === 200)
35 | return Promise.all([response.json(), response.headers]);
36 | else return Promise.reject("Invalid login attempt");
37 | })
38 | .then(([body, headers]) => {
39 | user.setJwt(Cookies.get("jwt"));
40 | })
41 | .catch((message) => {
42 | alert(message);
43 | });
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 | Full Name
53 | setName(e.target.value)}
61 | />
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Username
70 | setUsername(e.target.value)}
76 | />
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Password
85 | setPassword(e.target.value)}
91 | />
92 |
93 |
94 |
95 |
96 |
101 | createAndLoginUser()}
107 |
108 | >
109 | Register
110 |
111 | {
116 | navigate("/login");
117 | }}
118 | >
119 | Exit
120 |
121 |
122 |
123 |
124 |
125 | );
126 | };
127 |
128 | export default Register;
129 |
--------------------------------------------------------------------------------
/front-end/src/Services/assignmentDueDatesService.js:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import { isValidValue } from "./validate";
3 |
4 | const nineMonthDueDatesInWeeks = [
5 | 2, 7, 10, 16, 18, 20, 21, 22, 23, 24, 25, 27, 29, 32, 36,
6 | ];
7 | const sixMonthDueDatesinWeeks = [
8 | 3, 6, 7, 10, 11, 12, 14, 15, 16, 17, 18, 20, 22, 24, 26,
9 | ];
10 |
11 | function getDueDates(sd, courseDurationInWeeks, assignments) {
12 | isValidValue(courseDurationInWeeks, [24, 36]);
13 | const startDate = dayjs(sd);
14 |
15 | if (courseDurationInWeeks === 36) {
16 | return nineMonthDueDatesInWeeks.map((weekDue, i) => {
17 | return {
18 | name: `#${i + 1}`,
19 | dueDate: startDate.add(weekDue, "week").format("MMM-DD"),
20 | value: 6.6,
21 | color: getColor(assignments, i + 1, startDate.add(weekDue, "week")),
22 | };
23 | });
24 | } else {
25 | return sixMonthDueDatesinWeeks.map((weekDue, i) => {
26 | return {
27 | name: `#${i + 1}`,
28 | dueDate: startDate.add(weekDue, "week").format("MMM-DD"),
29 | value: 6.6,
30 | color: getColor(assignments, i + 1, startDate.add(weekDue, "week")),
31 | };
32 | });
33 | }
34 | }
35 |
36 | const getNumDaysSinceLastSubmission = (assignments) => {
37 | const latestAssignment = assignments
38 | .sort((a1, a2) => {
39 | if (a2.submittedDate && a1.submittedDate) {
40 | return dayjs(a2.submittedDate).diff(dayjs(a1.submittedDate), "second");
41 | } else if (a2.lastModified && a1.lastModified) {
42 | return new Date(a2.lastModified) - new Date(a1.lastModified);
43 | } else if (!a2.submittedDate && a1.submittedDate) {
44 | return -1;
45 | } else if (!a1.submittedDate && a2.submittedDate) {
46 | return 1;
47 | }
48 | return 0;
49 | })
50 | .splice(0, 1);
51 |
52 | if (latestAssignment.length > 0 && latestAssignment[0].submittedDate) {
53 | const submittedDay = dayjs(latestAssignment[0].submittedDate);
54 | console.log("We have a submitted date: ", submittedDay);
55 | const dayDiff = dayjs().diff(submittedDay, "day");
56 | console.log("Day diff: ", dayDiff);
57 | return dayDiff;
58 | } else if (latestAssignment.lastModified) {
59 | return dayjs().diff(dayjs(latestAssignment.lastModified), "day");
60 | } else {
61 | return -1;
62 | }
63 | };
64 |
65 | function numAssignmentsThatShouldBeCompleted(sd, courseDurationInWeeks) {
66 | const startDate = dayjs(sd);
67 | const now = dayjs();
68 | const weekDiff = now.diff(startDate, "week");
69 | let assignmentNumber = 15;
70 |
71 | if (courseDurationInWeeks === 36) {
72 | for (let i = 0; i < nineMonthDueDatesInWeeks.length; i++) {
73 | if (weekDiff < nineMonthDueDatesInWeeks[i]) {
74 | assignmentNumber = i + 1;
75 | break;
76 | }
77 | }
78 | return assignmentNumber;
79 | } else {
80 | for (let i = 0; i < sixMonthDueDatesinWeeks.length; i++) {
81 | if (weekDiff < sixMonthDueDatesinWeeks[i]) {
82 | assignmentNumber = i + 1;
83 | break;
84 | }
85 | }
86 | return assignmentNumber;
87 | }
88 | }
89 |
90 | function getColor(assignments, num, dueDate) {
91 | const assignment = assignments.filter((a) => a.number === num);
92 | const now = dayjs();
93 | if (assignment.length > 0) {
94 | if (assignment[0].status === "Completed") return "green";
95 |
96 | if (now.isAfter(dueDate)) {
97 | if (assignment[0].status === "Submitted") {
98 | return "rgb(255, 193, 7)";
99 | } else if (assignment[0].status === "Needs Update") {
100 | return "orange";
101 | } else {
102 | return "rgb(220, 53, 69)";
103 | }
104 | } else return "grey";
105 | } else {
106 | if (now.isAfter(dueDate)) return "rgb(220, 53, 69)";
107 | else return "grey";
108 | }
109 | }
110 | export {
111 | getDueDates,
112 | numAssignmentsThatShouldBeCompleted,
113 | getNumDaysSinceLastSubmission,
114 | };
115 |
--------------------------------------------------------------------------------
/front-end/src/Services/fetchService.js:
--------------------------------------------------------------------------------
1 | function ajax(url, requestMethod, jwt, requestBody) {
2 | const fetchData = {
3 | headers: {
4 | "Content-Type": "application/json",
5 | },
6 | method: requestMethod,
7 | };
8 |
9 | if (jwt) {
10 | fetchData.headers.Authorization = `Bearer ${jwt}`;
11 | }
12 |
13 | if (requestBody) {
14 | fetchData.body = JSON.stringify(requestBody);
15 | }
16 |
17 | return fetch(url, fetchData).then((response) => {
18 | if (response.status === 200) {
19 | const contentType = response.headers.get("content-type");
20 | if (contentType && contentType.indexOf("application/json") !== -1) {
21 | return response.json();
22 | } else {
23 | return response.text();
24 | }
25 | }
26 | });
27 | }
28 |
29 | export default ajax;
30 |
--------------------------------------------------------------------------------
/front-end/src/Services/statusService.js:
--------------------------------------------------------------------------------
1 | function getButtonsByStatusAndRole(currentStatus, role) {
2 | let buttons = [];
3 | if (currentStatus === "Pending Submission" && role === "student") {
4 | buttons.push(getButton("Save"));
5 | buttons.push(getButton("Submit"));
6 | } else if (currentStatus === "Submitted" && role === "student") {
7 | buttons.push(getButton("Un-submit"));
8 | } else if (currentStatus === "Needs Update" && role === "student") {
9 | buttons.push(getButton("Resubmit Assignment"));
10 | } else if (currentStatus === "In Review" && role === "code_reviewer") {
11 | buttons.push(getButton("Complete Review"));
12 | buttons.push(getButton("Reject Assignment"));
13 | } else if (currentStatus === "Completed" && role === "code_reviewer") {
14 | buttons.push(getButton("Re-claim"));
15 | } else if (currentStatus === "Needs Update" && role === "code_reviewer") {
16 | buttons.push(getButton("Re-claim"));
17 | } else if (currentStatus === "Resubmitted" && role === "student") {
18 | buttons.push(getButton("Un-submit"));
19 | }
20 |
21 | return buttons;
22 | }
23 |
24 | function getButton(type, currentStatus) {
25 | let button = {};
26 | button.text = type;
27 | switch (type) {
28 | case "Save":
29 | button.variant = "secondary";
30 | button.nextStatus = "Same";
31 | break;
32 | case "Submit":
33 | button.variant = "primary";
34 | button.nextStatus = "Submitted";
35 | break;
36 | case "Un-submit":
37 | button.variant = "secondary";
38 | button.nextStatus = "Pending Submission";
39 | break;
40 | case "Resubmit Assignment":
41 | button.variant = "primary";
42 | button.nextStatus = "Resubmitted";
43 | break;
44 | case "Complete Review":
45 | button.variant = "primary";
46 | button.nextStatus = "Completed";
47 | break;
48 | case "Reject Assignment":
49 | button.variant = "danger";
50 | button.nextStatus = "Needs Update";
51 | break;
52 | case "Re-claim":
53 | if (currentStatus === "Submitted") {
54 | button.nextStatus = "Pending Submission";
55 | } else if (currentStatus === "Needs Update" || "Completed") {
56 | button.nextStatus = "In Review";
57 | }
58 | button.variant = "secondary";
59 | break;
60 | default:
61 | button.variant = "info";
62 | break;
63 | }
64 | return button;
65 | }
66 |
67 | export { getButtonsByStatusAndRole };
68 |
--------------------------------------------------------------------------------
/front-end/src/Services/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Is the `input` value inside the set of `validValues`?
3 | * @param {*} input the input value that we're trying to validate
4 | * @param {*} validValues an array of valid values for the input
5 | */
6 | function isValidValue(input, validValues) {
7 | const valid =
8 | validValues.filter((validValue) => validValue == input).length > 0;
9 | if (!valid) {
10 | throw new Error(
11 | `Input value ${input} not in set of valid values ${validValues}`
12 | );
13 | }
14 | }
15 |
16 | export { isValidValue };
17 |
--------------------------------------------------------------------------------
/front-end/src/StatusBadge/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Badge } from "react-bootstrap";
3 |
4 | const StatusBadge = (props) => {
5 | const { text } = props;
6 | function getColorOfBadge() {
7 | if (text === "Completed") return "success";
8 | else if (text === "Needs Update") return "danger";
9 | else if (text === "Pending Submission") return "warning";
10 | else if (text === "Resubmitted") return "primary";
11 | else return "info";
12 | }
13 | return (
14 |
21 | {text}
22 |
23 | );
24 | };
25 |
26 | export default StatusBadge;
27 |
--------------------------------------------------------------------------------
/front-end/src/UserProvider/index.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState } from "react";
2 | import Cookies from "js-cookie";
3 | const UserContext = createContext();
4 |
5 | const UserProvider = ({ children }) => {
6 | const [jwt, setJwt] = useState(Cookies.get("jwt"));
7 |
8 | const value = { jwt, setJwt };
9 | return {children} ;
10 | };
11 |
12 | function useUser() {
13 | const context = useContext(UserContext);
14 | if (context === undefined) {
15 | throw new Error("useUser must be used within a UserProvider");
16 | }
17 |
18 | return context;
19 | }
20 |
21 | export { useUser, UserProvider };
22 |
--------------------------------------------------------------------------------
/front-end/src/custom.scss:
--------------------------------------------------------------------------------
1 | $theme-colors: (
2 | "primary": #233f93,
3 | "secondary": #6c757d,
4 | "success": #28a745,
5 | "danger": #dc3545,
6 | "warning": #ffc107,
7 | "info": #8ad2d4,
8 | "light": #f8f9fa,
9 | "dark": #343a40,
10 | );
11 | // Import Bootstrap and its default variables
12 | @import "~bootstrap/scss/bootstrap.scss";
13 |
14 | a {
15 | text-decoration: none !important;
16 | }
17 |
--------------------------------------------------------------------------------
/front-end/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/front-end/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 | import { BrowserRouter } from "react-router-dom";
7 | import { UserProvider } from "./UserProvider";
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById("root")
18 | );
19 |
20 | // If you want to start measuring performance in your app, pass a function
21 | // to log results (for example: reportWebVitals(console.log))
22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
23 | reportWebVitals();
24 |
--------------------------------------------------------------------------------
/front-end/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/front-end/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/front-end/src/util/useInterval.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | function useInterval(callback, delay) {
4 | const savedCallback = useRef();
5 |
6 | // Remember the latest callback.
7 | useEffect(() => {
8 | savedCallback.current = callback;
9 | }, [callback]);
10 |
11 | // Set up the interval.
12 | useEffect(() => {
13 | function tick() {
14 | savedCallback.current();
15 | }
16 | if (delay !== null) {
17 | let id = setInterval(tick, delay);
18 | return () => clearInterval(id);
19 | }
20 | }, [delay]);
21 | }
22 |
23 | export { useInterval };
24 |
--------------------------------------------------------------------------------
/front-end/src/util/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | function useLocalState(defaultValue, key) {
4 | const [value, setValue] = useState(() => {
5 | const localStorageValue = localStorage.getItem(key);
6 |
7 | return localStorageValue !== null
8 | ? JSON.parse(localStorageValue)
9 | : defaultValue;
10 | });
11 |
12 | useEffect(() => {
13 | localStorage.setItem(key, JSON.stringify(value));
14 | }, [key, value]);
15 |
16 | return [value, setValue];
17 | }
18 |
19 | export { useLocalState };
20 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AssignmentSubmissionApp",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------