From d0a9a55692918ee7271d684f419bae77e7f7225c Mon Sep 17 00:00:00 2001 From: Aron Buzogany Date: Sat, 6 Apr 2024 15:05:46 +0200 Subject: [PATCH] Fix #175 --- frontend/public/locales/en/translation.json | 23 +++ frontend/public/locales/nl/translation.json | 23 +++ frontend/src/App.tsx | 2 +- .../components/FolderUpload/FolderUpload.tsx | 2 +- .../project/{ => projectView}/ProjectView.tsx | 38 ++++- .../project/projectView/SubmissionCard.tsx | 150 ++++++++++++++++++ .../project/projectView/SubmissionsGrid.tsx | 92 +++++++++++ frontend/src/types/course.ts | 4 + frontend/src/types/submission.ts | 5 + frontend/src/utils/date-utils.ts | 27 ++++ 10 files changed, 357 insertions(+), 9 deletions(-) rename frontend/src/pages/project/{ => projectView}/ProjectView.tsx (65%) create mode 100644 frontend/src/pages/project/projectView/SubmissionCard.tsx create mode 100644 frontend/src/pages/project/projectView/SubmissionsGrid.tsx create mode 100644 frontend/src/types/course.ts create mode 100644 frontend/src/types/submission.ts create mode 100644 frontend/src/utils/date-utils.ts diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 6c8a3568..013a5fb3 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -11,5 +11,28 @@ "courseName": "Course Name", "submit": "Submit", "emptyCourseNameError": "Course name should not be empty" + }, + "projectView": { + "submitNetworkError": "Failed to upload file, please try again.", + "selected": "Selected", + "submit": "Submit", + "previousSubmissions": "Previous Submissions", + "noFileSelected": "No file selected", + "submissionGrid": { + "late": "Late", + "fail": "Fail", + "success": "Success", + "running": "Running", + "submitTime": "Time submitted", + "status": "Status" + } + }, + "time": { + "yearsAgo": "years ago", + "monthsAgo": "months ago", + "daysAgo": "days ago", + "hoursAgo": "hours ago", + "minutesAgo": "minutes ago", + "justNow": "just now" } } \ No newline at end of file diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index e62cf43d..faa36d15 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -11,5 +11,28 @@ "courseName": "Vak Naam", "submit": "Opslaan", "emptyCourseNameError": "Vak naam mag niet leeg zijn" + }, + "projectView": { + "submitNetworkError": "Er is iets mislopen bij het opslaan van uw indiening. Probeer het later opnieuw.", + "selected": "Geslecteerd", + "submit": "Indienen", + "previousSubmissions": "Vorige indieningen", + "noFileSelected": "Er is geen bestand geselecteerd", + "submissionGrid": { + "late": "Te laat", + "fail": "Gefaald", + "success": "Successvol", + "running": "Aan het uitvoeren", + "submitTime": "Indientijd", + "status": "Status" + } + }, + "time": { + "yearsAgo": "jaren geleden", + "monthsAgo": "maanden geleden", + "daysAgo": "dagen geleden", + "hoursAgo": "uur geleden", + "minutesAgo": "minuten geleden", + "justNow": "Zonet" } } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bc097c2e..eb541cd4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import { BrowserRouter,Route,Routes } from "react-router-dom"; import { Header } from "./components/Header/Header"; import Home from "./pages/home/Home"; import LanguagePath from "./components/LanguagePath"; -import ProjectView from "./pages/project/ProjectView"; +import ProjectView from "./pages/project/projectView/ProjectView"; /** * This component is the main application component that will be rendered by the ReactDOM. diff --git a/frontend/src/components/FolderUpload/FolderUpload.tsx b/frontend/src/components/FolderUpload/FolderUpload.tsx index 463ddb97..8a94de78 100644 --- a/frontend/src/components/FolderUpload/FolderUpload.tsx +++ b/frontend/src/components/FolderUpload/FolderUpload.tsx @@ -99,7 +99,7 @@ const FolderDragDrop: React.FC = ({ } return ( - + (); const [projectData, setProjectData] = useState(null); - const [courseData, setCourseData] = useState(null); + const [courseData, setCourseData] = useState(null); + const [assignmentRawText, setAssignmentRawText] = useState(""); useEffect(() => { fetch(`${API_URL}/projects/${projectId}`, { @@ -35,7 +45,6 @@ export default function ProjectView() { }).then((response) => { if (response.ok) { response.json().then((data) => { - console.log(data); setCourseData(data["data"]); }); } @@ -43,21 +52,32 @@ export default function ProjectView() { }); } }); + + fetch(`${API_URL}/projects/${projectId}/assignment`, { + headers: { Authorization: "teacher" }, + }).then((response) => { + if (response.ok) { + response.text().then((data) => setAssignmentRawText(data)); + } + }); }, [projectId]); + if (!projectId) return null; + return ( {projectData && ( @@ -65,7 +85,7 @@ export default function ProjectView() { {projectData.description} {courseData && ( - + {courseData.name} )} @@ -73,15 +93,19 @@ export default function ProjectView() { } /> + + {assignmentRawText} + )} - - - + diff --git a/frontend/src/pages/project/projectView/SubmissionCard.tsx b/frontend/src/pages/project/projectView/SubmissionCard.tsx new file mode 100644 index 00000000..c223aa27 --- /dev/null +++ b/frontend/src/pages/project/projectView/SubmissionCard.tsx @@ -0,0 +1,150 @@ +import { + Alert, + Button, + Card, + CardContent, + CardHeader, + Grid, + IconButton, + LinearProgress, + Typography, +} from "@mui/material"; +import SendIcon from "@mui/icons-material/Send"; +import { useEffect, useState } from "react"; +import FolderDragDrop from "../../../components/FolderUpload/FolderUpload"; +import axios from "axios"; +import { useTranslation } from "react-i18next"; +import SubmissionsGrid from "./SubmissionsGrid"; +import { Submission } from "../../../types/submission"; + +interface SubmissionCardProps { + regexRequirements?: string[]; + submissionUrl: string; + projectId: string; +} + +/** + * + * @param params - regexRequirements, submissionUrl, projectId + * @returns - SubmissionCard component which allows the user to submit files + * and view previous submissions + */ +export default function SubmissionCard({ + regexRequirements, + submissionUrl, + projectId, +}: SubmissionCardProps) { + const { t } = useTranslation('translation', { keyPrefix: 'projectView' }); + const [activeTab, setActiveTab] = useState("submit"); + const [selectedFile, setSelectedFile] = useState(null); + const [uploadProgress, setUploadProgress] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const [previousSubmissions, setPreviousSubmissions] = useState([]); + const handleFileDrop = (file: File) => { + setSelectedFile(file); + }; + + useEffect(() => { + + fetch(`${submissionUrl}?project_id=${projectId}`, { + headers: { Authorization: "teacher" } + }).then((response) => { + if (response.ok) { + response.json().then((data) => { + setPreviousSubmissions(data["data"]); + }); + } + }) + }, [projectId, submissionUrl]); + + const handleSubmit = async () => { + const form = new FormData(); + if (!selectedFile) { + setErrorMessage(t("noFileSelected")); + return; + } + form.append("files", selectedFile); + form.append("project_id", projectId); + form.append("uid", "teacher"); + try { + const response = await axios.post(submissionUrl, form, { + headers: { + "Content-Type": "multipart/form-data", + Authorization: "teacher", + }, + onUploadProgress: (progressEvent) => { + if (progressEvent.total) { + setUploadProgress( + Math.round((progressEvent.loaded * 100) / progressEvent.total) + ); + } + }, + }); + + if (response.status === 201) { + setSelectedFile(null); + setPreviousSubmissions((prev) => [...prev, response.data["data"]]); + setActiveTab("submissions"); + } else { + setErrorMessage(t("submitNetworkError")); + } + } catch (error) { + setErrorMessage(t("submitNetworkError")); + } + + setUploadProgress(null); + }; + + return ( + + + + + + } + /> + + {activeTab === "submit" ? ( + + + setErrorMessage(message)} + /> + + + {selectedFile && ( + {`${t("selected")}: ${selectedFile.name}`} + )} + + + + + + + + {uploadProgress && ( + + )} + + + {errorMessage && ( + setErrorMessage(null)}> + {errorMessage} + + )} + + + ) : ( + + )} + + + ); +} diff --git a/frontend/src/pages/project/projectView/SubmissionsGrid.tsx b/frontend/src/pages/project/projectView/SubmissionsGrid.tsx new file mode 100644 index 00000000..fd8ff9ff --- /dev/null +++ b/frontend/src/pages/project/projectView/SubmissionsGrid.tsx @@ -0,0 +1,92 @@ +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import DownloadIcon from "@mui/icons-material/Download"; +import { IconButton } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { timeDifference } from "../../../utils/date-utils"; +import { Submission } from "../../../types/submission"; + +interface SubmissionsGridProps { + submissionUrl: string; + rows: Submission[]; +} + +/** + * + * @param param - submissionUrl, rows + * @returns - SubmissionsGrid component which displays the submissions of the current user + */ +export default function SubmissionsGrid({ + submissionUrl, + rows, +}: SubmissionsGridProps) { + const { t } = useTranslation("translation", { + keyPrefix: "projectView.submissionGrid", + }); + + const stateMapper = { + LATE: t("late"), + FAIL: t("fail"), + RUNNING: t("running"), + SUCCESS: t("success"), + }; + + const columns: GridColDef[] = [ + { + field: "id", + type: "string", + width: 50, + headerName: "", + }, + { + field: "submission_time", + headerName: t("submitTime"), + type: "string", + flex: 1, + valueFormatter: (value) => timeDifference(value), + }, + { + field: "submission_status", + headerName: t("status"), + type: "string", + flex: 1, + valueFormatter: (value) => stateMapper[value], + }, + { + field: "actions", + type: "actions", + width: 50, + getActions: (props) => [ + + + , + ], + }, + ]; + + return ( + { + const urlTags = row.submission_id.split("/"); + return urlTags[urlTags.length - 1]; + }} + rows={rows} + /> + ); +} diff --git a/frontend/src/types/course.ts b/frontend/src/types/course.ts new file mode 100644 index 00000000..6b365ff9 --- /dev/null +++ b/frontend/src/types/course.ts @@ -0,0 +1,4 @@ +export interface Course { + name: string; + course_id: string; +} diff --git a/frontend/src/types/submission.ts b/frontend/src/types/submission.ts new file mode 100644 index 00000000..4522dbac --- /dev/null +++ b/frontend/src/types/submission.ts @@ -0,0 +1,5 @@ +export interface Submission { + submission_id: string; + submission_time: string; + submission_status: string; +} diff --git a/frontend/src/utils/date-utils.ts b/frontend/src/utils/date-utils.ts new file mode 100644 index 00000000..59fe2db9 --- /dev/null +++ b/frontend/src/utils/date-utils.ts @@ -0,0 +1,27 @@ +import i18next from "i18next"; + +/** + * + * @param date - date string to be converted to time difference + * @returns - time difference between the current date and the given date + */ +export function timeDifference(date: string) { + const t = (key: string) => { + return i18next.t(`time.${key}`); + }; + + const current = new Date(); + const previous = new Date(date); + const diff = current.getTime() - previous.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + if (years > 0) return `${years} ${t("yearsAgo")}`; + if (months > 0) return `${months} ${t("monthsAgo")}`; + if (days > 0) return `${days} ${t("daysAgo")}`; + if (hours > 0) return `${hours} ${t("hoursAgo")}`; + if (minutes > 0) return `${minutes} ${t("minutesAgo")}`; + return t("justNow"); +}