diff --git a/frontend/public/img/logo_app.png b/frontend/public/img/logo_app.png new file mode 100644 index 00000000..7a36d43c Binary files /dev/null and b/frontend/public/img/logo_app.png differ diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index b9cbb5e1..27bcab24 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -12,7 +12,9 @@ "home": { "home": "Home", "tag": "en", - "homepage": "Homepage" + "homepage": "Homepage", + "welcomeDescription": "Welcome to Peristerónas, the online submission platform of UGent", + "login": "Login" }, "courseForm": { "courseName": "Course Name", @@ -79,5 +81,19 @@ "noRegexPlaceholder": "No regex added yet", "clearSelected": "Clear Selection", "faultySubmission": "Some fields were left open or there is no valid runner/file combination" + }, + "student" : { + "myProjects": "My Projects", + "myCourses": "My Courses", + "deadlines": "Past deadlines", + "last_submission" : "Last submission", + "course": "Course", + "SUCCESS": "Success", + "FAIL": "Fail", + "deadlinesOnDay": "Deadlines on: ", + "noDeadline": "No deadlines", + "no_submission_yet" : "No submission yet", + "loading": "Loading...", + "no_projects": "There are no projects here." } -} \ No newline at end of file +} diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 92be172f..bdd4028f 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -1,7 +1,4 @@ { - "home": { - "title": "Homepagina" - }, "header": { "myProjects": "Mijn Projecten", "myCourses": "Mijn Vakken", @@ -12,16 +9,32 @@ "projectUploadForm": "Project uploaden" }, "home": { - "login": "Aanmelden", "home": "Home", "tag": "nl", - "homepage": "Homepage" + "homepage": "Homepagina", + "welcomeDescription": "Welkom bij Peristerónas, het online indieningsplatform van UGent", + "login": "Aanmelden" }, "courseForm": { "courseName": "Vak Naam", "submit": "Opslaan", "emptyCourseNameError": "Vak naam mag niet leeg zijn" }, + "student": { + "myProjects": "Mijn Projecten", + "myCourses": "Mijn Vakken", + "deadlines": "Verlopen Deadlines", + "course": "Vak", + "last_submission": "Laatste indiening", + "SUCCESS": "Geslaagd", + "FAIL": "Gefaald", + "deadlinesOnDay": "Deadlines op: ", + "noDeadline": "Geen deadlines", + "no_submission_yet" : "Nog geen indiening", + "loading": "Laden...", + "no_projects": "Er zijn hier geen projecten." + + }, "projectForm": { "projectTitle": "Titel", "projectDescription": "Beschrijving", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 881a3dff..b1038469 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,18 @@ import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import Layout from "./components/Header/Layout"; -import Home from "./pages/home/Home"; import LanguagePath from "./components/LanguagePath"; import ProjectView from "./pages/project/projectView/ProjectView"; import { ErrorBoundary } from "./pages/error/ErrorBoundary.tsx"; import ProjectCreateHome from "./pages/create_project/ProjectCreateHome.tsx"; +import {fetchProjectPage} from "./pages/project/FetchProjects.tsx"; +import HomePages from "./pages/home/HomePages.tsx"; const router = createBrowserRouter( createRoutesFromElements( } errorElement={}> - } /> + } loader={fetchProjectPage}/> }> - } /> + } loader={fetchProjectPage} /> }/> diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 53bce51d..54de654d 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -1,16 +1,50 @@ import { useTranslation } from "react-i18next"; -import { Title } from "../../components/Header/Title"; +import { Button, Container, Typography, Box } from "@mui/material"; +import {Link } from "react-router-dom"; + /** * This component is the home page component that will be rendered when on the index route. * @returns - The home page component */ export default function Home() { - const { t } = useTranslation("translation", { keyPrefix: "home" }); + const { t } = useTranslation('translation', { keyPrefix: 'home' }); + const login_redirect:string =import.meta.env.VITE_LOGIN_LINK return ( - <> - - <div> - </div> - </> - ); + <Container maxWidth="sm"> + <Box + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + textAlign: 'center', + gap: 3, + }} + > + <Box component="img"src="/img/logo_ugent.png" alt="University Logo" + sx={{ width: 100, height: 100 }} /> + + <Typography variant="h2" component="h1" gutterBottom > + <Box + component="img" + src="/img/logo_app.png" + alt="University Logo" + sx={{ + position: 'relative', + top: '14px', + width: 90, + height: 90, + }} + /> + Peristerónas + </Typography> + <Typography variant="h6" component="p" > + {t('welcomeDescription', 'Welcome to Peristeronas.')} + </Typography> + <Button variant="contained" color="primary" size="large" component={Link} to={login_redirect}> + {t('login', 'Login')} + </Button> + </Box> + </Container> ); } diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx new file mode 100644 index 00000000..0fbb9c45 --- /dev/null +++ b/frontend/src/pages/home/HomePage.tsx @@ -0,0 +1,197 @@ +import { useTranslation } from "react-i18next"; +import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/material'; +import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; +import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import React, {useState} from 'react'; +import dayjs, {Dayjs} from "dayjs"; +import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; +import {ProjectDeadlineCard} from "../project/projectDeadline/ProjectDeadlineCard.tsx"; +import {ProjectDeadline} from "../project/projectDeadline/ProjectDeadline.tsx"; +import {useLoaderData} from "react-router-dom"; + +interface DeadlineInfoProps { + selectedDay: Dayjs; + deadlines: ProjectDeadline[]; +} + +type ExtendedPickersDayProps = PickersDayProps<Dayjs> & { highlightedDays?: number[] }; + +/** + * Displays the deadlines on a given day + * @param selectedDay - The day of interest + * @param deadlines - All the deadlines to consider + * @returns Element + */ +const DeadlineInfo: React.FC<DeadlineInfoProps> = ({ selectedDay, deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const deadlinesOnSelectedDay = deadlines.filter( + project => (project.deadline && dayjs(project.deadline).isSame(selectedDay, 'day')) + ); + //list of the corresponding assignment + return ( + <div> + {deadlinesOnSelectedDay.length === 0 ? ( + <Card style={{margin: '10px 0'}}> + <CardContent> + <Typography variant="body1"> + {t('noDeadline')} + </Typography> + </CardContent> + </Card> + ) : <ProjectDeadlineCard deadlines={deadlinesOnSelectedDay}/>} + </div> + ); +}; + +/** + * + * @param props - The day and the deadlines + * @returns - The ServerDay component that displays a badge for specific days + */ +function ServerDay(props: PickersDayProps<Dayjs> & { highlightedDays?: number[] }) { + const { highlightedDays = [], day, outsideCurrentMonth, ...other } = props; + + const isSelected = + !props.outsideCurrentMonth && highlightedDays.indexOf(props.day.date()) >= 0; + + return ( + <Badge + key={props.day.toString()} + overlap="circular" + badgeContent={isSelected ? '🔴' : undefined} + sx={{ + '.MuiBadge-badge': { + fontSize: '0.5em', + top: 8, + right: 8, + }, + }} + > + <PickersDay {...other} outsideCurrentMonth={outsideCurrentMonth} day={day} /> + </Badge> + ); +} +const handleMonthChange =( + date: Dayjs, + projects:ProjectDeadline[], + setHighlightedDays: React.Dispatch<React.SetStateAction<number[]>>, +) => { + + setHighlightedDays([]); + // projects are now only fetched on page load + const hDays:number[] = [] + projects.map((project, ) => { + if(project.deadline && project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ + hDays.push(project.deadline.getDate()) + } + + } + ); + setHighlightedDays(hDays) + +}; + +/** + * This component is the home page component that will be rendered when on the index route. + * @returns - The home page component + */ +export default function HomePage() { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + + const [highlightedDays, setHighlightedDays] = React.useState<number[]>([]); + + const [selectedDay, setSelectedDay] = useState<Dayjs>(dayjs(Date.now())); + const loader = useLoaderData() as { + projects: ProjectDeadline[], + me: string + } + const projects = loader.projects + + // Update selectedDay state when a day is selected + const handleDaySelect = (day: Dayjs) => { + setSelectedDay(day); + }; + const futureProjects = projects + .filter((p) => (p.deadline && dayjs(dayjs()).isBefore(p.deadline))) + .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) + .slice(0, 3) // only show the first 3 + + const pastDeadlines = projects + .filter((p) => p.deadline && (dayjs()).isAfter(p.deadline)) + .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) + .slice(0, 3) // only show the first 3 + const noDeadlineProject = projects.filter((p) => p.deadline === undefined) + return ( + <Container style={{ paddingTop: '50px' }}> + <Grid container spacing={2} wrap="nowrap"> + <Grid item xs={6}> + <Card> + <CardContent> + <Typography variant="body1"> + {t('myProjects')} + </Typography> + {futureProjects.length + noDeadlineProject.length > 0? ( + <> + <ProjectDeadlineCard deadlines={futureProjects} /> + <ProjectDeadlineCard deadlines={noDeadlineProject}/> + </> + ) : ( + <Typography variant="body1"> + {t('no_projects')} + </Typography> + )} + </CardContent> + </Card> + </Grid> + + <Grid item xs={6}> + <Card> + + <CardContent> + <Typography variant="body1"> + {t('deadlines')} + </Typography> + {pastDeadlines.length > 0 ? ( + <ProjectDeadlineCard deadlines={pastDeadlines} /> + ) : ( + <Typography variant="body1"> + {t('no_projects')} + </Typography> + )} + </CardContent> + </Card> + </Grid> + + <Grid item xs={6}> + <Card> + <LocalizationProvider dateAdapter={AdapterDayjs}> + <DateCalendar + value={selectedDay} + onMonthChange={(date: Dayjs) => { handleMonthChange(date, projects, setHighlightedDays) }} + onChange={handleDaySelect} + renderLoading={() => <DayCalendarSkeleton />} + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as ExtendedPickersDayProps, + }} + /> + </LocalizationProvider> + <CardContent> + <Typography variant="body2"> + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + </Typography> + <DeadlineInfo selectedDay={selectedDay} deadlines={projects} /> + </CardContent> + + </Card> + </Grid> + + </Grid> + </Container> + ); +} diff --git a/frontend/src/pages/home/HomePages.tsx b/frontend/src/pages/home/HomePages.tsx new file mode 100644 index 00000000..140be48c --- /dev/null +++ b/frontend/src/pages/home/HomePages.tsx @@ -0,0 +1,21 @@ +import HomePage from './HomePage.tsx'; +import Home from "./Home.tsx"; +import {useLoaderData} from "react-router-dom"; +import {ProjectDeadline} from "../project/projectDeadline/ProjectDeadline.tsx"; + +/** + * Gives the requested home page based on the login status + * @returns - The home page component + */ +export default function HomePages() { + const loader = useLoaderData() as { + projects: ProjectDeadline[], + me: string + } + const me = loader.me + if (me === 'UNKNOWN') { + return <Home />; + } else { + return <HomePage />; + } +} diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx new file mode 100644 index 00000000..1b988825 --- /dev/null +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -0,0 +1,114 @@ +import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; +const API_URL = import.meta.env.VITE_APP_API_HOST +const header = { + "Authorization": "teacher2" +} +export const fetchProjectPage = async () => { + const projects = await fetchProjects() + const me = await fetchMe() + return {projects, me} +} + +export const fetchMe = async () => { + try { + const response = await fetch(`${API_URL}/me`, { + headers:header + }) + if(response.status == 200){ + const data = await response.json() + return data.role + }else { + return "UNKNOWN" + } + } catch (e){ + return "UNKNOWN" + } + +} +export const fetchProjects = async () => { + + try{ + const response = await fetch(`${API_URL}/projects`, { + headers:header + }) + const jsonData = await response.json(); + let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { + try{ + const url_split = item.project_id.split('/') + const project_id = url_split[url_split.length -1] + const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { + headers: header + })).json() + + //get the latest submission + const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ + submission_id: submission.submission_id,//this is the path + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status + } + )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + // fetch the course id of the project + const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { + headers:header + })).json() + + //fetch the course + const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { + headers: header + })).json() + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id + } + if(project_item.data.deadlines){ + return project_item.data.deadlines.map((d:string[]) => { + return { + project_id: project_id, + title: project_item.data.title, + description: project_item.data.description, + assignment_file: project_item.data.assignment_file, + deadline: new Date(d[1]), + deadline_description: d[0], + course_id: Number(project_item.data.course_id), + visible_for_students: Boolean(project_item.data.visible_for_students), + archived: Boolean(project_item.data.archived), + test_path: project_item.data.test_path, + script_name: project_item.data.script_name, + regex_expressions: project_item.data.regex_expressions, + short_submission: latest_submission, + course: course + } + }) + } + // contains no dealine: + return [{ + project_id: project_id, + title: project_item.data.title, + description: project_item.data.description, + assignment_file: project_item.data.assignment_file, + deadline: undefined, + deadline_description: undefined, + course_id: Number(project_item.data.course_id), + visible_for_students: Boolean(project_item.data.visible_for_students), + archived: Boolean(project_item.data.archived), + test_path: project_item.data.test_path, + script_name: project_item.data.script_name, + regex_expressions: project_item.data.regex_expressions, + short_submission: latest_submission, + course: course + }] + + }catch (e){ + return [] + } + } + + )); + formattedData = formattedData.flat() + return formattedData + } catch (e) { + return [] + } +} diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx new file mode 100644 index 00000000..50f16a60 --- /dev/null +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx @@ -0,0 +1,49 @@ +export interface ProjectDeadline { + project_id:string , + title :string, + description:string, + assignment_file:string, + deadline:Date|undefined, + deadline_description:string, + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission, + course:Course + +} +export interface Project { + project_id:string , + title :string, + description:string, + assignment_file:string, + deadlines:string[][], + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission, + course:Course + +} +export interface Deadline { + description: string; + deadline: Date; +} + +export interface Course { + course_id: string; + name: string; + teacher: string; + ufora_id: string; +} +export interface ShortSubmission { + submission_id:number, + submission_time:Date, + submission_status:string +} diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx new file mode 100644 index 00000000..56d2351d --- /dev/null +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -0,0 +1,72 @@ +import {CardActionArea, Card, CardContent, Typography, Box, Button} from '@mui/material'; +import {Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import dayjs from "dayjs"; +import {ProjectDeadline, Deadline} from "./ProjectDeadline.tsx"; +import React from "react"; +import { useNavigate } from 'react-router-dom'; + +interface ProjectCardProps{ + deadlines:ProjectDeadline[], + pred?: (deadline:Deadline) => boolean +} + +/** + * A clickable display of a project deadline + * @param deadlines - A list of all the deadlines + * @param pred - A predicate to filter the deadlines + * @returns Element + */ +export const ProjectDeadlineCard: React.FC<ProjectCardProps> = ({ deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const { i18n } = useTranslation(); + const navigate = useNavigate(); + + //list of the corresponding assignment + return ( + <Box> + {deadlines.map((project, index) => ( + + <Card key={index} style={{margin: '10px 0'}}> + <CardActionArea component={Link} to={`/${i18n.language}/projects/${project.project_id}`}> + <CardContent> + <Typography variant="h6" style={{color: project.short_submission ? + (project.short_submission.submission_status === 'SUCCESS' ? 'green' : 'red') : '#686868'}}> + {project.title} + </Typography> + <Typography variant="subtitle1"> + {t('course')}: + <Button + style={{ + color: 'inherit', + textTransform: 'none' + }} + onMouseDown={event => event.stopPropagation()} + onClick={(event) => { + event.stopPropagation(); // stops the event from reaching CardActionArea + event.preventDefault(); + navigate(`/${i18n.language}/courses/${project.course.course_id}`) + }} + > + {project.course.name} + </Button> + </Typography> + <Typography variant="body2" color="textSecondary"> + {t('last_submission')}: {project.short_submission ? + t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} + </Typography> + {project.deadline && ( + <Typography variant="body2" color="textSecondary"> + Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} + </Typography> + )} + + </CardContent> + </CardActionArea> + </Card> + )) + + } + </Box> + ); +};