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" + } +}