diff --git a/devU-api/package.json b/devU-api/package.json
index e908de1..9fbe875 100644
--- a/devU-api/package.json
+++ b/devU-api/package.json
@@ -53,7 +53,7 @@
"prettier": "^2.3.0",
"rimraf": "^3.0.2",
"ts-jest": "^27.0.2",
- "ts-node": "^10.0.0",
+ "ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^4.3.2"
},
diff --git a/devU-api/scripts/populate-db.ts b/devU-api/scripts/populate-db.ts
index cdb1b7a..6ed62df 100644
--- a/devU-api/scripts/populate-db.ts
+++ b/devU-api/scripts/populate-db.ts
@@ -55,16 +55,23 @@ async function SendPOST(path: string, requestBody: string | FormData, requesterE
return responseBody
}
-async function CreateCourse(name: string, number: string, semester: string) {
+async function CreateCourse(
+ name: string,
+ number: string,
+ semester: string,
+ isPublic: boolean
+) {
const courseData = {
- name: name,
- semester: semester,
- number: number,
- startDate: '2024-01-24T00:00:00-0500',
- endDate: '2024-05-10T23:59:59-0500',
- }
- console.log('Creating course: ', courseData.name)
- return await SendPOST('/courses/instructor', JSON.stringify(courseData), 'admin')
+ name: name,
+ semester: semester,
+ number: number,
+ startDate: '2024-01-24T00:00:00-0500',
+ endDate: '2024-05-10T23:59:59-0500',
+ is_public: isPublic // Include the public property
+ };
+
+ console.log('Creating course: ', courseData.name);
+ return await SendPOST('/courses/instructor', JSON.stringify(courseData), 'admin');
}
async function joinCourse(courseId: number, userId: number, role: string) {
@@ -208,8 +215,8 @@ async function runCourseAndSubmission() {
const jones = await fetchToken('jones@buffalo.edu', 'jones')
//Create courses
- const courseId1 = (await CreateCourse('Testing Course Name1', 'CSE101', 's2024')).id
- const courseId2 = (await CreateCourse('Testing Course Name2', 'CSE102', 's2024')).id
+ const courseId1 = (await CreateCourse('Testing Course Name1', 'CSE101', 's2024',true)).id
+ const courseId2 = (await CreateCourse('Testing Course Name2', 'CSE102', 's2024',true)).id
//Enroll students
await joinCourse(courseId1, billy, 'student')
diff --git a/devU-api/src/entities/course/course.model.ts b/devU-api/src/entities/course/course.model.ts
index 3ab6c5a..dd657d2 100644
--- a/devU-api/src/entities/course/course.model.ts
+++ b/devU-api/src/entities/course/course.model.ts
@@ -58,4 +58,11 @@ export default class CourseModel {
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date
+
+ @Column({ type: 'boolean', name: 'is_public', default: false })
+ isPublic: boolean;
+
+ @Column({ name: 'private_data', type: 'timestamp', default: () => 'now()' })
+ private_data?: Date;
+
}
diff --git a/devU-api/src/entities/course/course.serializer.ts b/devU-api/src/entities/course/course.serializer.ts
index 8462cff..7094c1c 100644
--- a/devU-api/src/entities/course/course.serializer.ts
+++ b/devU-api/src/entities/course/course.serializer.ts
@@ -12,5 +12,7 @@ export function serialize(course: CourseModel): Course {
endDate: course.endDate.toISOString(),
createdAt: course.createdAt.toISOString(),
updatedAt: course.updatedAt.toISOString(),
+ isPublic: course.isPublic,
+ private_data: course.private_data ? course.private_data.toISOString() : undefined
}
}
diff --git a/devU-api/src/migration/1626719306608-addAssignmentsAndCourses.ts b/devU-api/src/migration/1626719306608-addAssignmentsAndCourses.ts
index f761ab3..332ac2a 100644
--- a/devU-api/src/migration/1626719306608-addAssignmentsAndCourses.ts
+++ b/devU-api/src/migration/1626719306608-addAssignmentsAndCourses.ts
@@ -15,6 +15,8 @@ export class addAssignmentsAndCourses1626719306608 implements MigrationInterface
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
"deleted_at" TIMESTAMP,
+ "is_public" boolean NOT NULL DEFAULT false,
+ "private_data" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "courses_primary_key_constraint" PRIMARY KEY ("id")
)`
)
diff --git a/devU-client/src/components/listItems/courseListItem.scss b/devU-client/src/components/listItems/courseListItem.scss
index c5d73f6..e8449c5 100644
--- a/devU-client/src/components/listItems/courseListItem.scss
+++ b/devU-client/src/components/listItems/courseListItem.scss
@@ -60,13 +60,29 @@
color: $text-color;
-
+ &.enrolled {
+ background-color: #dff0d8; // Light green background for enrolled courses
+ border-color: #3c763d; // Darker green border for enrolled courses
+ color: #3c763d; // Change text color to darker green
+ }
&:hover,
&:focus {
background: $list-item-background-hover;
}
}
+.enrollmentStatus {
+ margin-left: 10px;
+ font-weight: bold; // Make it bold for emphasis
+}
+
+.enrolled {
+ color: green; // Change color for enrolled courses
+}
+
+.notEnrolled {
+ color: red; // Change color for not enrolled courses
+}
@media (max-width: $medium) {
.subText {
diff --git a/devU-client/src/components/listItems/courseListItem.tsx b/devU-client/src/components/listItems/courseListItem.tsx
index 368ac2f..db096d6 100644
--- a/devU-client/src/components/listItems/courseListItem.tsx
+++ b/devU-client/src/components/listItems/courseListItem.tsx
@@ -44,7 +44,17 @@ const CourseListItem = ({course, isOpen}: Props) => {
{infoSection("Course Number", course.number)}
{infoSection("Semester", prettyPrintSemester(course.semester))}
{infoSection("Start/End Date", prettyPrintDate(course.startDate), prettyPrintDate(course.endDate))}
+
+ {course && (
+ course.isPublic ? (
+ Public Course
+ ) : (
+ Private Course
+ )
+ )}
+
+
}
)
diff --git a/devU-client/src/components/misc/globalToolbar.tsx b/devU-client/src/components/misc/globalToolbar.tsx
index 7149e5e..29a894e 100644
--- a/devU-client/src/components/misc/globalToolbar.tsx
+++ b/devU-client/src/components/misc/globalToolbar.tsx
@@ -29,7 +29,7 @@ const GlobalToolbar = () => {
{
- My Courses
+ Join a Course
}
{/**/}
diff --git a/devU-client/src/components/pages/forms/courses/courseUpdatePage.tsx b/devU-client/src/components/pages/forms/courses/courseUpdatePage.tsx
index 065df76..5b5ff33 100644
--- a/devU-client/src/components/pages/forms/courses/courseUpdatePage.tsx
+++ b/devU-client/src/components/pages/forms/courses/courseUpdatePage.tsx
@@ -44,12 +44,14 @@ const CourseUpdatePage = ({ }) => {
name: '',
number: '',
semester: '',
+ isPublic: false
})
const [startDate, setStartDate] = useState(new Date().toISOString())
const [endDate, setEndDate] = useState(new Date().toISOString())
const [studentEmail, setStudentEmail] = useState("")
const [emails, setEmails] = useState([])
const [invalidFields, setInvalidFields] = useState(new Map())
+ const [privateDate, setPrivateDate] = useState(new Date().toISOString().split("T")[0]);
const { courseId } = useParams() as UrlParams
useEffect(() => {
@@ -60,9 +62,11 @@ const CourseUpdatePage = ({ }) => {
name: res.name,
number: res.number,
semester: res.semester,
+ isPublic: res.isPublic
});
setStartDate(new Date(res.startDate).toISOString().split("T")[0]);
setEndDate(new Date(res.endDate).toISOString().split("T")[0]);
+ setPrivateDate(new Date(res.privateDate).toISOString().split("T")[0]);
isMounted = true;
});
}
@@ -80,10 +84,15 @@ const CourseUpdatePage = ({ }) => {
setFormData(prevState => ({ ...prevState, [key]: value }))
}
}
-
+ const handleCheckboxChange = (e: React.ChangeEvent) => {
+ setFormData(prevState => ({ ...prevState, isPublic: e.target.checked }));
+ };
const handleStartDateChange = (event: React.ChangeEvent) => { setStartDate(event.target.value) }
const handleEndDateChange = (event: React.ChangeEvent) => { setEndDate(event.target.value) }
+ const handlePrivateDateChange = (event: React.ChangeEvent) => {
+ setPrivateDate(event.target.value);
+ };
const handleCourseUpdate = () => {
const finalFormData = {
name: formData.name,
@@ -91,6 +100,8 @@ const CourseUpdatePage = ({ }) => {
semester: formData.semester,
startDate: startDate + "T16:02:41.849Z",
endDate: endDate + "T16:02:41.849Z",
+ isPublic: formData.isPublic,
+ privateDate: privateDate + "T16:02:41.849Z",
}
RequestService.put(`/api/courses/${courseId}`, finalFormData)
@@ -267,6 +278,20 @@ const CourseUpdatePage = ({ }) => {
+
+
+
+
+
+
+
diff --git a/devU-client/src/components/pages/forms/courses/coursesFormPage.tsx b/devU-client/src/components/pages/forms/courses/coursesFormPage.tsx
index e5814ce..ca101c8 100644
--- a/devU-client/src/components/pages/forms/courses/coursesFormPage.tsx
+++ b/devU-client/src/components/pages/forms/courses/coursesFormPage.tsx
@@ -1,102 +1,131 @@
-import React, { useState } from 'react'
-import { useHistory } from 'react-router-dom'
-import { ExpressValidationError } from 'devu-shared-modules'
-
-import PageWrapper from 'components/shared/layouts/pageWrapper'
-
-import RequestService from 'services/request.service'
-
-import { useActionless } from 'redux/hooks'
-import TextField from 'components/shared/inputs/textField'
-import { SET_ALERT } from 'redux/types/active.types'
-import formStyles from './coursesFormPage.scss'
-import { applyMessageToErrorFields, removeClassFromField } from "../../../../utils/textField.utils";
-
+import React, { useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import RequestService from 'services/request.service';
+import { useActionless } from 'redux/hooks';
+import TextField from 'components/shared/inputs/textField';
+import { SET_ALERT } from 'redux/types/active.types';
+import formStyles from './coursesFormPage.scss';
+import PageWrapper from 'components/shared/layouts/pageWrapper';
const EditCourseFormPage = () => {
- const [setAlert] = useActionless(SET_ALERT)
+ const [setAlert] = useActionless(SET_ALERT);
const history = useHistory();
const [formData, setFormData] = useState({
name: '',
number: '',
semester: '',
- })
+ isPublic: false
+ });
- const [startDate, setStartDate] = useState(new Date().toISOString().split("T")[0])
- const [endDate, setEndDate] = useState(new Date().toISOString().split("T")[0])
- const [invalidFields, setInvalidFields] = useState(new Map
())
+ const [startDate, setStartDate] = useState(new Date().toISOString().split("T")[0]);
+ const [endDate, setEndDate] = useState(new Date().toISOString().split("T")[0]);
+ const [privateDate, setPrivateDate] = useState(new Date().toISOString().split("T")[0]);
- const handleChange = (value: String, e: React.ChangeEvent) => {
- const key = e.target.id
- setFormData(prevState => ({ ...prevState, [key]: value }))
+ const handleChange = (value: string, e: React.ChangeEvent) => {
+ const key = e.target.id;
+ setFormData(prevState => ({ ...prevState, [key]: value }));
+ };
- const newInvalidFields = removeClassFromField(invalidFields, key)
- setInvalidFields(newInvalidFields)
- }
+ const handleCheckboxChange = (e: React.ChangeEvent) => {
+ setFormData(prevState => ({ ...prevState, isPublic: e.target.checked }));
+ };
- const handleStartDateChange = (event: React.ChangeEvent) => { setStartDate(event.target.value) }
- const handleEndDateChange = (event: React.ChangeEvent) => { setEndDate(event.target.value) }
+ const handleStartDateChange = (event: React.ChangeEvent) => {
+ setStartDate(event.target.value);
+ };
+ const handleEndDateChange = (event: React.ChangeEvent) => {
+ setEndDate(event.target.value);
+ };
+
+ const handlePrivateDateChange = (event: React.ChangeEvent) => {
+ setPrivateDate(event.target.value);
+ };
+
+ const formatDateForSubmission = (date: string) => {
+ return new Date(date).toISOString();
+ };
+
+ const isFormValid = () => {
+ return formData.name && formData.number && formData.semester && startDate && endDate;
+ };
const handleSubmit = () => {
const finalFormData = {
name: formData.name,
number: formData.number,
semester: formData.semester,
- startDate: startDate + "T16:02:41.849Z",
- endDate: endDate + "T16:02:41.849Z",
- }
+ startDate: formatDateForSubmission(startDate),
+ endDate: formatDateForSubmission(endDate),
+ isPublic: formData.isPublic,
+ privateDate: formatDateForSubmission(privateDate)
+ };
RequestService.post('/api/courses/instructor', finalFormData)
.then(() => {
- setAlert({ autoDelete: true, type: 'success', message: 'Course Added' })
- history.goBack()
+ setAlert({ autoDelete: true, type: 'success', message: 'Course Added' });
+ history.goBack();
})
- .catch((err: ExpressValidationError[] | Error) => {
- const message = Array.isArray(err) ? err.map((e) => `${e.param} ${e.msg}`).join(', ') : err.message
-
- const newFields = new Map()
- Array.isArray(err) ? err.map((e) => applyMessageToErrorFields(newFields, e.param, e.msg)) : newFields
- setInvalidFields(newFields);
- setAlert({ autoDelete: false, type: 'error', message })
- })
- .finally(() => {
- })
- }
+ .catch((err) => {
+ setAlert({ autoDelete: false, type: 'error', message: err.message });
+ });
+ };
return (
Create Course
-
-
-
-
+
+
+
-
+
-
+
+
-
+
- )
-
-}
-
+ );
+};
-export default EditCourseFormPage
\ No newline at end of file
+export default EditCourseFormPage;
\ No newline at end of file
diff --git a/devU-client/src/components/pages/listPages/courses/coursesListPage.tsx b/devU-client/src/components/pages/listPages/courses/coursesListPage.tsx
index b1e0026..2184be9 100644
--- a/devU-client/src/components/pages/listPages/courses/coursesListPage.tsx
+++ b/devU-client/src/components/pages/listPages/courses/coursesListPage.tsx
@@ -1,70 +1,81 @@
-import React, {useEffect, useState} from 'react'
-import {Course, UserCourse} from 'devu-shared-modules'
-import LoadingOverlay from 'components/shared/loaders/loadingOverlay'
-import PageWrapper from 'components/shared/layouts/pageWrapper'
-import Dropdown, {Option} from 'components/shared/inputs/dropdown'
-import ErrorPage from '../../errorPage/errorPage'
-import RequestService from 'services/request.service'
-import styles from './coursesListPage.scss'
+import React, { useEffect, useState } from 'react';
+import { Course } from 'devu-shared-modules';
+import LoadingOverlay from 'components/shared/loaders/loadingOverlay';
+import PageWrapper from 'components/shared/layouts/pageWrapper';
+import Dropdown, { Option } from 'components/shared/inputs/dropdown';
+import ErrorPage from '../../errorPage/errorPage';
+import RequestService from 'services/request.service';
+import styles from './coursesListPage.scss';
import CourseListItem from "../../../listItems/courseListItem";
-// import {useAppSelector} from "../../../../redux/hooks";
import Button from "@mui/material/Button";
-import {useHistory} from "react-router-dom";
+import { useHistory } from "react-router-dom";
+import { useAppSelector } from "../../../../redux/hooks";
-type Filter = true | false
+type Filter = true | false;
const filterOptions: Option
[] = [
- {label: 'Expand All', value: true},
- {label: 'Collapse All', value: false},
-]
+ { label: 'Expand All', value: true },
+ { label: 'Collapse All', value: false },
+];
const UserCoursesListPage = () => {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [allCourses, setAllCourses] = useState([]);
+ const [filter, setFilter] = useState(false);
+ const history = useHistory();
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
- const [userCourses, setUserCourses] = useState(new Array())
- const [filter, setFilter] = useState(false )
- const history = useHistory()
-
- //Temporary place to store state for all courses
- const [allCourses, setAllCourses] = useState(new Array())
+ // Get userId from Redux store
+ const userId = useAppSelector((store) => store.user.id);
useEffect(() => {
- fetchData()
- }, [])
+ fetchData();
+ }, []);
const fetchData = async () => {
try {
- // const userCourses = await RequestService.get(`/api/user-courses?filterBy=${filter}`)
- const courseRequests = userCourses.map((u) => RequestService.get(`/api/courses/${u.courseId}`))
- const courses = await Promise.all(courseRequests)
-
- // Mapify course ids so we can look them up more easilly via their id
- const courseMap: Record = {}
- for (const course of courses) courseMap[course.id || ''] = course
-
- // Temporary place to grab and display all courses
- const allCourses = await RequestService.get('/api/courses')
- setAllCourses(allCourses)
-
- setUserCourses(userCourses)
+ // Fetch user-specific courses
+ const userCourseData = await RequestService.get<{
+ instructorCourses: Course[];
+ activeCourses: Course[];
+ pastCourses: Course[];
+ upcomingCourses: Course[];
+ }>(`/api/courses/user/${userId}`);
+
+ // Flatten and combine user course data into a single array
+ const userCoursesList = [
+ ...userCourseData.instructorCourses,
+ ...userCourseData.activeCourses,
+ ...userCourseData.pastCourses,
+ ...userCourseData.upcomingCourses,
+ ];
+
+ // Fetch all courses
+ const allCourseData = await RequestService.get(`/api/courses`);
+
+ // Filter to get courses the user is not enrolled in
+ const unenrolledCourses = allCourseData.filter(
+ (course) => !userCoursesList.some((userCourse) => userCourse.id === course.id)
+ );
+
+
+ setAllCourses(unenrolledCourses);
} catch (error: any) {
- setError(error)
+ setError(error);
} finally {
- setLoading(false)
+ setLoading(false);
}
- }
+ };
const handleFilterChange = (updatedFilter: Filter) => {
- setFilter(updatedFilter)
- }
+ setFilter(updatedFilter);
+ };
- if (loading) return
- if (error) return
-
- const defaultOption = filterOptions.find((o) => o.value === filter)
+ if (loading) return ;
+ if (error) return ;
+ const defaultOption = filterOptions.find((o) => o.value === filter);
return (
@@ -73,9 +84,8 @@ const UserCoursesListPage = () => {
All Courses
-
- {allCourses.map(course => (
-
+ {allCourses.map((course) => (
+
))}
- )
-
-
-}
+ );
+};
-export default UserCoursesListPage
+export default UserCoursesListPage;
\ No newline at end of file
diff --git a/devU-shared/src/types/course.types.ts b/devU-shared/src/types/course.types.ts
index 0e03d98..cc21135 100644
--- a/devU-shared/src/types/course.types.ts
+++ b/devU-shared/src/types/course.types.ts
@@ -7,4 +7,8 @@ export type Course = {
endDate: string
createdAt?: string
updatedAt?: string
+ isPublic?: boolean;
+ private_data?: string;
+ allowlist?: string[];
+ blocklist?: string[];
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..84cd429
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,34 @@
+{
+ "name": "devU",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@types/react": "^18.3.12"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.13",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
+ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.12",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
+ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ab744a6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "@types/react": "^18.3.12"
+ }
+}