import React, {useState, useEffect} from "react";
import "../styles/addcourse.css";
import "../styles/modal.css";
import "../styles/course.css";
import {Select} from "../Components/Select";
/**
* The AddCourse component displays a form for adding a new course to the system.
* @param handleAddCourseModal
* @param getCourses
* @returns {Element}
* @constructor
*/
const AddCourse = ({handleAddCourseModal, getCourses}) => {
const [courseCode, setCourseCode] = useState(""); // State for storing the course code
const [courseName, setCourseName] = useState(""); // State for storing the course name
const [file, setFile] = useState(null); // State for storing the file
const [semester, setSemester] = useState(""); // State for storing the semester
const [year, setYear] = useState(null); // State for storing the year
const [rosterFileError, setRosterFileError] = useState([]); // State for storing the roster file error
const [duplicateError, setDuplicateError] = useState(""); // State for storing the duplicate error
const [showModal, setShowModal] = useState(false); // State for showing the modal
const [instructors, setInstructors] = useState([]); // array of instructor objects selected including their id and name
const [allInstructors, setAllInstructors] = useState([]); // array of all instructors in the database
const [selectedInstructors, setSelectedInstructors] = useState([]); // array of selected instructor ids to send to backend
const formData = new FormData();
/**
* Determines the current year based on the current date.
* @returns {number}
*/
const getCurrentYear = () => {
const date = new Date();
return date.getFullYear();
};
// Using 2023-2024 course schedule
/**
* Determines the current semester based on the current date.
* @returns {number}
*/
const getCurrentSemester = () => {
const date = new Date();
const month = date.getMonth(); // 0 for January, 1 for February, etc.
const day = date.getDate();
// Summer Sessions (May 30 to Aug 18)
if (
(month === 4 && day >= 30) ||
(month > 4 && month < 7) ||
(month === 7 && day <= 18)
) {
return 3; // Summer
}
// Fall Semester (Aug 28 to Dec 20)
if (
(month === 7 && day >= 28) ||
(month > 7 && month < 11) ||
(month === 11 && day <= 20)
) {
return 4; // Fall
}
// Winter Session (Dec 28 to Jan 19)
if ((month === 11 && day >= 28) || (month === 0 && day <= 19)) {
return 1; // Winter
}
// If none of the above conditions are met, it must be Spring (Jan 24 to May 19)
return 2; // Spring
};
/**
* Converts semester names to their corresponding integer codes.
*/
const getFutureSemesters = () => {
const date = new Date();
const currentYear = getCurrentYear();
const currentSemester = getCurrentSemester();
const futureSemesters = [];
let startSem;
if (currentSemester === 1) startSem = "winter";
if (currentSemester === 2) startSem = "spring";
if (currentSemester === 3) startSem = "summer";
if (currentSemester === 4) startSem = "fall";
let year = currentYear;
// Include 4 semesters from the current one
for (let i = 0; i < 4; i++) {
futureSemesters.push({
value: `${startSem}_${year}`,
text: `${startSem.charAt(0).toUpperCase() + startSem.slice(1)} ${year}`,
});
startSem = getNextSemester(startSem);
if (startSem === "winter") {
year++;
}
}
return futureSemesters;
};
/**
* Returns the next semester given the current semester.
* @param currentSemester
* @returns {string}
*/
const getNextSemester = (currentSemester) => {
switch (currentSemester) {
case "winter":
return "spring";
case "spring":
return "summer";
case "summer":
return "fall";
case "fall":
return "winter";
default:
return "";
}
};
const futureSemesters = getFutureSemesters(); // Array of future semesters
const [semesters, setSemesters] = useState(futureSemesters); // State for storing the future semesters
// fetch the courses to display on the sidebar
useEffect(() => {
setYear(getCurrentYear());
let currSem = getCurrentSemester();
if (currSem === 1) setSemester("winter");
if (currSem === 2) setSemester("spring");
if (currSem === 3) setSemester("summer");
if (currSem === 4) setSemester("fall");
// Fetch all instructors
fetch(process.env.REACT_APP_API_URL + "getInstructors.php", {
method: "GET",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then((res) => res.json())
.then((result) => {
let fetchedInstructors = [];
result.map((instructor) => {
let currentInstructor = {
label: instructor[1],
value: instructor[0],
};
fetchedInstructors.push(currentInstructor);
});
setAllInstructors(fetchedInstructors);
})
.catch((err) => {
console.log(err);
});
}, []);
// Everytime an instructor is selected/deselected the selectedInstructors state updates
useEffect(() => {
let instructorIds = [];
instructors.map((instructor) => {
instructorIds.push(+instructor.value);
});
setSelectedInstructors(instructorIds);
}, [instructors]);
/**
* Handles the change of the semester.
*/
const handleSemesterChange = (e) => {
const selectedValue = e.target.value; // For example, "fall_2024"
const [newSemester, newYear] = selectedValue.split("_"); // Splits to ["fall", "2024"]
// Update the states
setSemester(newSemester);
setYear(parseInt(newYear));
};
/**
* Formats the roster file error.
* @param input
*/
function formatRosterError(input) {
// Split the string into an array on the "Line" pattern, then filter out empty strings
const lines = input
.split(/(Line \d+)/)
.filter((line) => line.trim() !== "");
// Combine adjacent elements so that each "Line #" and its message are in the same element
const combinedLines = [];
for (let i = 0; i < lines.length; i += 2) {
combinedLines.push(lines[i] + (lines[i + 1] || ""));
}
return combinedLines
}
/**
* Handles the form submission.
* @param e
*/
const handleSubmit = (e) => {
e.preventDefault();
formData.append("course-code", courseCode);
formData.append("course-name", courseName);
formData.append("course-year", year);
formData.append("roster-file", file); // Assuming `file` is a File object
formData.append("semester", semester);
formData.append("additional-instructors", selectedInstructors);
// Send the form data to the API
fetch(process.env.REACT_APP_API_URL + "courseAdd.php", {
method: "POST",
body: formData,
})
// Parse the response to JSON format
.then((res) => res.text())
.then((result) => {
// If the result is a string and not empty, it means there is an error
if (typeof result === "string" && result !== "") {
try {
const parsedResult = JSON.parse(result);
console.log(parsedResult);
if (parsedResult["roster-file"]) {
setShowModal(true);
const updatedError = formatRosterError(
parsedResult["roster-file"]
);
setRosterFileError(updatedError);
} else if (parsedResult["duplicate"]) {
setDuplicateError(parsedResult["duplicate"]);
}
} catch (e) {
console.log("Failed to parse JSON: ", e);
}
} else {
// Class is valid, so we can just navigate to the home page
handleAddCourseModal();
getCourses();
setRosterFileError([]);
setDuplicateError("");
}
})
.catch((err) => {
console.log(err);
});
};
const handleModalClose = () => {
setShowModal(false); // Close the modal
};
// The AddCourse component renders a form to add a new course.
return (
<>
<div className="formContainer">
<div className="formContent">
<h2 className="add-header">Add Course</h2>
{/*The form element where the onSubmit event is handled by the handleSubmit function.*/}
<form
className="add__form"
onSubmit={handleSubmit}
encType="multipart/form-data"
>
<div className="addcourse-form__item--container">
{/* Section for course code and name with potential duplicate error messages. */}
<div className="addcourse--namecode-error">
{/* Input fields for course code and name. */}
<div className="addcourse--name-code">
<div className="name-code--item form__item">
<label className="form__item--label">
Course Code
<input
type="text"
id="course-code"
value={courseCode}
onChange={(e) => setCourseCode(e.target.value)}
placeholder="CSE 115"
required
className={
duplicateError && "addcourse-duplicate-error"
}
/>
</label>
</div>
<div className="name-code--item form__item">
<label className=" form__item--label">
Course Name
<input
type="text"
id="course-name"
value={courseName}
onChange={(e) => setCourseName(e.target.value)}
placeholder="Introduction to Computer Science"
required
className={
duplicateError && "addcourse-duplicate-error"
}
/>
</label>
</div>
</div>
{/* Displays error message if the course being added is a duplicate. */}
{duplicateError && (
<p className="add-course--error">
This course already exists
</p>
)}
</div>
{/* File input for course roster CSV file with specific requirements. */}
<div className="form__item file-input-wrapper">
<label className="form__item--label form__item--file">
Roster (CSV File) - Requires Emails in Columns 1, First Names
in Columns 2 and Last Names in Columns 3
<input
type="file"
id="addcourse-file-input"
className={`addcourse-file-input`}
onChange={(e) => setFile(e.target.files[0])}
required
/>
</label>
</div>
<div className="sem-year--additional-instructor--container">
{/* Dropdown for selecting the course's semester and year. */}
<div className="form__item form__item--select">
<label className="form__item--label">
Course Semester and Year
</label>
<select
value={`${semester}_${year}`}
className="add-course--select"
onChange={handleSemesterChange}
id="semester"
name="semester"
required
>
{semesters.map((sem) => {
return (
<option
key={sem.value}
value={sem.value}
selected={sem.value === `${semester}_${year}`}
>
{sem.text}
</option>
);
})}
</select>
</div>
{/* Select component for choosing additional instructors. */}
<div className="form__item additional-instructors--item">
<label className="form__item--label">
Additional Instructor(s)
</label>
<Select
multiple
options={allInstructors}
value={instructors}
onChange={(o) => setInstructors(o)}
/>
</div>
</div>
</div>
{/* Submission button for the form. */}
<div className="add-form__submit--container">
<button type="submit" className="form__submit">
+ Add Course
</button>
</div>
</form>
</div>
</div>
{/* Conditional rendering of a modal dialog for roster file errors. */}
{showModal && (
<div className="modal">
<div className="modal-content">
<h2>Roster File Error</h2>
{
rosterFileError.length > 0 && rosterFileError.map((err) => (
<p>{err}</p>
))
}
<button className="roster-file--error-btn" onClick={handleModalClose}>OK</button>
</div>
</div>
)}
</>
);
};
export default AddCourse;