-
Notifications
You must be signed in to change notification settings - Fork 304
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
General: Add course archive for old courses from previous semesters (#…
- Loading branch information
Showing
33 changed files
with
904 additions
and
90 deletions.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package de.tum.cit.aet.artemis.core.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
|
||
/** | ||
* DTO for representing archived courses from previous semesters. | ||
* | ||
* @param id The id of the course | ||
* @param title The title of the course | ||
* @param semester The semester in which the course was offered | ||
* @param color The background color of the course | ||
* @param icon The icon of the course | ||
*/ | ||
@JsonInclude(JsonInclude.Include.NON_EMPTY) | ||
public record CourseForArchiveDTO(long id, String title, String semester, String color, String icon) { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export class CourseForArchiveDTO { | ||
id: number; | ||
title: string; | ||
semester: string; | ||
color: string; | ||
icon: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/main/webapp/app/overview/course-archive/course-archive.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
@if (courses) { | ||
<div class="module-bg p-3 rounded-3 mb-3"> | ||
<div class="d-flex justify-content-between align-items-center"> | ||
<div class="d-flex justify-content-start gap-1"> | ||
<h3 class="fw-medium mb-0" jhiTranslate="artemisApp.course.archive.title"></h3> | ||
<fa-icon | ||
[icon]="faQuestionCircle" | ||
[size]="iconSize" | ||
class="text-secondary align-self-center" | ||
ngbTooltip="{{ 'artemisApp.course.archive.tip' | artemisTranslate }}" | ||
/> | ||
</div> | ||
@if (courses.length) { | ||
<div class="d-flex justify-content-between gap-3 align-items-center ms-1"> | ||
<div class="text-primary d-inline-flex"> | ||
<a id="sort-test" (click)="onSort()" class="d-inline-flex align-items-center"> | ||
<fa-icon id="icon-test-down" [icon]="isSortAscending ? faArrowDown19 : faArrowUp19" /> | ||
<span class="ms-1" jhiTranslate="artemisApp.course.archive.sort"></span> | ||
</a> | ||
</div> | ||
<jhi-search-filter (newSearchEvent)="setSearchValue($event)" class="my-0" /> | ||
</div> | ||
} | ||
</div> | ||
</div> | ||
<div class="module-bg py-3 rounded-3"> | ||
@if (courses.length) { | ||
<div class="mb-0"> | ||
@for (semester of semesters; track semester; let last = $last; let i = $index) { | ||
<div | ||
class="d-flex justify-content-between align-items-center px-3" | ||
(click)="isCourseFoundInSemester(semester) && toggleCollapseState(semester)" | ||
tabindex="0" | ||
role="button" | ||
id="semester-group-{{ i }}" | ||
[ngClass]="{ 'text-secondary': !(coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText).length }" | ||
> | ||
<span class="fw-bold" jhiTranslate="{{ fullFormOfSemesterStrings[semester] }}" [translateValues]="{ param: semester.slice(2) }"></span> | ||
<fa-icon [icon]="semesterCollapsed[semester] ? faAngleDown : faAngleUp" /> | ||
</div> | ||
@if (!semesterCollapsed[semester]) { | ||
<div class="container-fluid mt-2"> | ||
<div class="course-grid justify-content-center align-items-center"> | ||
@for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) { | ||
<div class="course-card-wrapper p-0"> | ||
<jhi-course-card-header | ||
class="col-2" | ||
[courseId]="course.id" | ||
[courseTitle]="course.title" | ||
[courseIcon]="course.icon" | ||
[courseColor]="course.color" | ||
[archiveMode]="true" | ||
/> | ||
</div> | ||
} | ||
</div> | ||
</div> | ||
} | ||
@if (!last) { | ||
<hr class="mx-3" /> | ||
} | ||
} | ||
</div> | ||
} @else { | ||
<div class="d-flex justify-content-center"> | ||
<h4 class="text-secondary text-center mb-0" jhiTranslate="artemisApp.course.archive.noCoursesPreviousSemester"></h4> | ||
</div> | ||
} | ||
</div> | ||
} |
17 changes: 17 additions & 0 deletions
17
src/main/webapp/app/overview/course-archive/course-archive.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
.course-grid { | ||
display: grid; | ||
// cards can shrink to 325px | ||
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); | ||
grid-gap: 1rem; | ||
justify-items: center; | ||
} | ||
|
||
.course-card-wrapper { | ||
width: 100%; | ||
max-width: 400px; | ||
} | ||
|
||
.container-fluid { | ||
// ensure that horizontal spacing in container is consistent | ||
--bs-gutter-x: 2rem; | ||
} |
151 changes: 151 additions & 0 deletions
151
src/main/webapp/app/overview/course-archive/course-archive.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'; | ||
import { Course } from 'app/entities/course.model'; | ||
import { CourseManagementService } from '../../course/manage/course-management.service'; | ||
import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; | ||
import { AlertService } from 'app/core/util/alert.service'; | ||
import { onError } from 'app/shared/util/global.utils'; | ||
import { Subscription } from 'rxjs'; | ||
import { faAngleDown, faAngleUp, faArrowDown19, faArrowUp19, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; | ||
import { SizeProp } from '@fortawesome/fontawesome-svg-core'; | ||
import { ArtemisSharedModule } from 'app/shared/shared.module'; | ||
import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; | ||
import { CourseForArchiveDTO } from 'app/course/manage/course-for-archive-dto'; | ||
import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; | ||
|
||
@Component({ | ||
selector: 'jhi-course-archive', | ||
templateUrl: './course-archive.component.html', | ||
styleUrls: ['./course-archive.component.scss'], | ||
standalone: true, | ||
imports: [ArtemisSharedModule, CourseCardHeaderComponent, SearchFilterComponent], | ||
}) | ||
export class CourseArchiveComponent implements OnInit, OnDestroy { | ||
private archiveCourseSubscription: Subscription; | ||
private courseService = inject(CourseManagementService); | ||
private alertService = inject(AlertService); | ||
|
||
courses: CourseForArchiveDTO[] = []; | ||
semesters: string[]; | ||
fullFormOfSemesterStrings: { [key: string]: string } = {}; | ||
semesterCollapsed: { [key: string]: boolean } = {}; | ||
coursesBySemester: { [key: string]: Course[] } = {}; | ||
searchCourseText = ''; | ||
isSortAscending = true; | ||
iconSize: SizeProp = 'lg'; | ||
|
||
//Icons | ||
readonly faAngleDown = faAngleDown; | ||
readonly faAngleUp = faAngleUp; | ||
readonly faArrowDown19 = faArrowDown19; | ||
readonly faArrowUp19 = faArrowUp19; | ||
readonly faQuestionCircle = faQuestionCircle; | ||
|
||
ngOnInit(): void { | ||
this.loadArchivedCourses(); | ||
this.courseService.enableCourseOverviewBackground(); | ||
} | ||
|
||
/** | ||
* Loads all courses that the student has been enrolled in from previous semesters | ||
*/ | ||
loadArchivedCourses(): void { | ||
this.archiveCourseSubscription = this.courseService.getCoursesForArchive().subscribe({ | ||
next: (res: HttpResponse<CourseForArchiveDTO[]>) => { | ||
if (res.body) { | ||
this.courses = res.body || []; | ||
this.courses = this.sortCoursesByTitle(this.courses); | ||
this.semesters = this.getUniqueSemesterNamesSorted(this.courses); | ||
this.mapCoursesIntoSemesters(); | ||
} | ||
}, | ||
error: (error: HttpErrorResponse) => onError(this.alertService, error), | ||
}); | ||
} | ||
|
||
/** | ||
* maps existing courses to each semester | ||
*/ | ||
mapCoursesIntoSemesters(): void { | ||
this.semesters.forEach((semester) => { | ||
this.semesterCollapsed[semester] = false; | ||
this.courseService.setSemesterCollapseState(semester, false); | ||
this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); | ||
this.fullFormOfSemesterStrings[semester] = semester.startsWith('WS') ? 'artemisApp.course.archive.winterSemester' : 'artemisApp.course.archive.summerSemester'; | ||
}); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this.archiveCourseSubscription.unsubscribe(); | ||
this.courseService.disableCourseOverviewBackground(); | ||
} | ||
|
||
setSearchValue(searchValue: string): void { | ||
this.searchCourseText = searchValue; | ||
if (searchValue !== '') { | ||
this.expandOrCollapseBasedOnSearchValue(); | ||
} else { | ||
this.getCollapseStateForSemesters(); | ||
} | ||
} | ||
|
||
onSort(): void { | ||
if (this.semesters) { | ||
this.semesters.reverse(); | ||
this.isSortAscending = !this.isSortAscending; | ||
} | ||
} | ||
/** | ||
* if the searched text is matched with a course title, expand the accordion, otherwise collapse | ||
*/ | ||
expandOrCollapseBasedOnSearchValue(): void { | ||
for (const semester of this.semesters) { | ||
const hasMatchingCourse = this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); | ||
this.semesterCollapsed[semester] = !hasMatchingCourse; | ||
} | ||
} | ||
|
||
getCollapseStateForSemesters(): void { | ||
for (const semester of this.semesters) { | ||
this.semesterCollapsed[semester] = this.courseService.getSemesterCollapseStateFromStorage(semester); | ||
} | ||
} | ||
|
||
toggleCollapseState(semester: string): void { | ||
this.semesterCollapsed[semester] = !this.semesterCollapsed[semester]; | ||
this.courseService.setSemesterCollapseState(semester, this.semesterCollapsed[semester]); | ||
} | ||
|
||
isCourseFoundInSemester(semester: string): boolean { | ||
return this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); | ||
} | ||
|
||
sortCoursesByTitle(courses: CourseForArchiveDTO[]): CourseForArchiveDTO[] { | ||
return courses.sort((courseA, courseB) => (courseA.title ?? '').localeCompare(courseB.title ?? '')); | ||
} | ||
|
||
getUniqueSemesterNamesSorted(courses: CourseForArchiveDTO[]): string[] { | ||
return ( | ||
courses | ||
.map((course) => course.semester ?? '') | ||
// filter down to unique values | ||
.filter((course, index, courses) => courses.indexOf(course) === index) | ||
.sort((semesterA, semesterB) => { | ||
// Parse years in base 10 by extracting the two digits after the WS or SS prefix | ||
const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); | ||
if (yearsCompared !== 0) { | ||
return yearsCompared; | ||
} | ||
|
||
// If years are the same, sort WS over SS | ||
const prefixA = semesterA.slice(0, 2); | ||
const prefixB = semesterB.slice(0, 2); | ||
|
||
if (prefixA === prefixB) { | ||
return 0; // Both semesters are the same (either both WS or both SS) | ||
} | ||
|
||
return prefixA === 'WS' ? -1 : 1; // WS should be placed above SS | ||
}) | ||
); | ||
} | ||
} |
Oops, something went wrong.