diff --git a/.changeset/forty-eagles-cough.md b/.changeset/forty-eagles-cough.md new file mode 100644 index 00000000..6f364200 --- /dev/null +++ b/.changeset/forty-eagles-cough.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': patch +--- + +Add a way to hide action chips on list items diff --git a/.changeset/pretty-wombats-double.md b/.changeset/pretty-wombats-double.md new file mode 100644 index 00000000..6d836a01 --- /dev/null +++ b/.changeset/pretty-wombats-double.md @@ -0,0 +1,13 @@ +--- +'@openstapps/app': minor +--- + +Revamp "My Courses" section on profile page + +The "My Courses" section on the profile page has been improved + +- It will now show the upcoming courses for the next five days +- The section header is now consistent with the other sections +- The section now uses standard list items instead of the custom solution + +Additionally, the profile page component has been cleaned up. diff --git a/.changeset/wet-houses-provide.md b/.changeset/wet-houses-provide.md new file mode 100644 index 00000000..15bce2bf --- /dev/null +++ b/.changeset/wet-houses-provide.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': minor +--- + +Use event title for date series instead of the generic date series title diff --git a/frontend/app/src/app/modules/data/list/data-list-item.component.ts b/frontend/app/src/app/modules/data/list/data-list-item.component.ts index 36d41818..90ca5ffe 100644 --- a/frontend/app/src/app/modules/data/list/data-list-item.component.ts +++ b/frontend/app/src/app/modules/data/list/data-list-item.component.ts @@ -38,6 +38,8 @@ export class DataListItemComponent { @Input() listItemEndInteraction = true; + @Input() listItemChipInteraction = true; + @Input() lines = 'inset'; @Input() forceHeight = false; diff --git a/frontend/app/src/app/modules/data/list/data-list-item.html b/frontend/app/src/app/modules/data/list/data-list-item.html index d8f1f562..3d478f66 100644 --- a/frontend/app/src/app/modules/data/list/data-list-item.html +++ b/frontend/app/src/app/modules/data/list/data-list-item.html @@ -43,7 +43,7 @@
diff --git a/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html b/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html index 5629c02a..6824333e 100644 --- a/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html +++ b/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html @@ -17,7 +17,7 @@
- {{ 'name' | thingTranslate : item }} + {{ 'event.name' | thingTranslate : item }}

diff --git a/frontend/app/src/app/modules/profile/page/my-courses.component.ts b/frontend/app/src/app/modules/profile/page/my-courses.component.ts new file mode 100644 index 00000000..4aec8d72 --- /dev/null +++ b/frontend/app/src/app/modules/profile/page/my-courses.component.ts @@ -0,0 +1,70 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {mergeMap, ReplaySubject} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {SCDateSeries, SCISO8601Date} from '@openstapps/core'; +import moment from 'moment/moment'; +import {ScheduleProvider} from '../../calendar/schedule.provider'; + +interface MyCoursesTodayInterface { + startTime: string; + endTime: string; + course: SCDateSeries; +} + +type MyCoursesGroup = [SCISO8601Date, MyCoursesTodayInterface[]][]; + +/** + * Groups date series into a list of events happening in the next days + * @param dateSeries the date series to group + * @param visibleDays the number of days ahead to group + */ +function groupDays(dateSeries: SCDateSeries[], visibleDays: number): MyCoursesGroup { + const courses: [SCISO8601Date, MyCoursesTodayInterface[]][] = []; + const dates = Array.from({length: visibleDays}, (_, i) => moment().startOf('day').add(i, 'days')); + + for (const day of dates) { + const dayCourses: MyCoursesTodayInterface[] = []; + for (const course of dateSeries) { + for (const date of course.dates) { + if (moment(date).isSame(day, 'day')) { + dayCourses.push({ + startTime: moment(date).toISOString(), + endTime: moment(date).add(course.duration).toISOString(), + course, + }); + } + } + } + courses.push([day.toISOString(), dayCourses]); + } + + return courses; +} + +@Component({ + selector: 'my-courses', + templateUrl: 'my-courses.html', + styleUrls: ['my-courses.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyCoursesComponent { + /** + * The number of days from today to display + */ + @Input({required: true}) set visibleDays(value: number) { + this.visibleDays$.next(value); + } + + readonly visibleDays$ = new ReplaySubject(); + + myCourses = this.visibleDays$.pipe( + mergeMap(visibleDays => + this.scheduleProvider.uuids$.pipe( + mergeMap(uuids => this.scheduleProvider.getDateSeries(uuids)), + map(dateSeries => groupDays(dateSeries.dates, visibleDays)), + ), + ), + ); + + constructor(private scheduleProvider: ScheduleProvider) {} +} diff --git a/frontend/app/src/app/modules/profile/page/my-courses.html b/frontend/app/src/app/modules/profile/page/my-courses.html new file mode 100644 index 00000000..1766b631 --- /dev/null +++ b/frontend/app/src/app/modules/profile/page/my-courses.html @@ -0,0 +1,35 @@ + + + + + {{ myCoursesDay[0] | amDateFormat: 'dddd, ll' }} - {{ ('profile.courses.' + (myCoursesDay[1].length + === 0 ? 'NO' : myCoursesDay[1].length === 1 ? 'ONE' : 'MANY' ) + '_EVENT') | translate: {count: + myCoursesDay[1].length} }} + + + + +

{{ 'profile.courses.no_courses' | translate }}
+ + + + {{myCourse.startTime | amDateFormat: 'LT'}} - {{myCourse.endTime | amDateFormat: + 'LT'}} + + + + + + diff --git a/frontend/app/src/app/modules/profile/page/my-courses.scss b/frontend/app/src/app/modules/profile/page/my-courses.scss new file mode 100644 index 00000000..9e8c38f0 --- /dev/null +++ b/frontend/app/src/app/modules/profile/page/my-courses.scss @@ -0,0 +1,9 @@ +ion-accordion-group { + overflow: hidden; + border-radius: var(--border-radius-default); +} + +ion-item-divider { + --background: transparent; + --color: inherit; +} diff --git a/frontend/app/src/app/modules/profile/page/profile-page.component.ts b/frontend/app/src/app/modules/profile/page/profile-page.component.ts index 5bbe4068..d08a9983 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page.component.ts @@ -12,41 +12,20 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnInit} from '@angular/core'; -import {firstValueFrom, Observable, of, Subscription} from 'rxjs'; +import {Component} from '@angular/core'; import {AuthHelperService} from '../../auth/auth-helper.service'; -import {SCAuthorizationProviderType, SCDateSeries, SCUserConfiguration} from '@openstapps/core'; +import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core'; import {ActivatedRoute} from '@angular/router'; import {ScheduleProvider} from '../../calendar/schedule.provider'; -import moment from 'moment'; -import {SCIcon} from '../../../util/ion-icon/icon'; import {profilePageSections} from '../../../../config/profile-page-sections'; import {filter, map} from 'rxjs/operators'; -const CourseCard = { - collapsed: SCIcon`expand_more`, - expanded: SCIcon`expand_less`, -}; - -interface MyCoursesTodayInterface { - startTime: string; - endTime: string; - course: SCDateSeries; -} - @Component({ selector: 'app-home', templateUrl: 'profile-page.html', styleUrls: ['profile-page.scss'], }) -export class ProfilePageComponent implements OnInit { - data: { - [key in SCAuthorizationProviderType]: {loggedIn$: Observable}; - } = { - default: {loggedIn$: of(false)}, - paia: {loggedIn$: of(false)}, - }; - +export class ProfilePageComponent { user$ = this.authHelper.getProvider('default').user$.pipe( filter(user => user !== undefined), map(userInfo => { @@ -58,59 +37,18 @@ export class ProfilePageComponent implements OnInit { logins: SCAuthorizationProviderType[] = []; - originPath: string | null; - userInfo?: SCUserConfiguration; - courseCardEnum = CourseCard; - - courseCardState = CourseCard.expanded; - - todayDate = moment().startOf('day').add(0, 'day').format(); // moment().startOf('day').format(); '2022-05-03T00:00:00+02:00' - - myCoursesToday: MyCoursesTodayInterface[] = []; - - subscriptions: Subscription[] = []; - constructor( - private authHelper: AuthHelperService, - private route: ActivatedRoute, - protected readonly scheduleProvider: ScheduleProvider, + readonly authHelper: AuthHelperService, + readonly activatedRoute: ActivatedRoute, + readonly scheduleProvider: ScheduleProvider, ) {} - ngOnInit() { - this.data.default.loggedIn$ = this.authHelper.getProvider('default').isAuthenticated$; - this.data.paia.loggedIn$ = this.authHelper.getProvider('paia').isAuthenticated$; - - this.subscriptions.push( - this.route.queryParamMap.subscribe(queryParameters => { - this.originPath = queryParameters.get('origin_path'); - }), - ); - - this.getMyCourses(); - } - - async getMyCourses() { - const result = await firstValueFrom(this.scheduleProvider.uuids$); - const courses = await this.scheduleProvider.getDateSeries(result); - - for (const course of courses.dates) { - for (const date of course.dates) { - if (moment(date).startOf('day').format() === this.todayDate) { - this.myCoursesToday[this.myCoursesToday.length] = { - startTime: moment(date).format('LT'), - endTime: moment(date).add(course.duration).format('LT'), - course, - }; - } - } - } - } - async signIn(providerType: SCAuthorizationProviderType) { - await this.handleOriginPath(); - this.authHelper.getProvider(providerType).signIn(); + const originPath = this.activatedRoute.snapshot.queryParamMap.get('origin_path'); + await (originPath ? this.authHelper.setOriginPath(originPath) : this.authHelper.deleteOriginPath()); + await this.authHelper.getProvider(providerType).signIn(); } async signOut(providerType: SCAuthorizationProviderType) { @@ -118,25 +56,6 @@ export class ProfilePageComponent implements OnInit { this.userInfo = undefined; } - toggleCourseCardState() { - if (this.courseCardState === CourseCard.expanded) { - const card: HTMLElement | null = document.querySelector('.course-card'); - const height = card?.scrollHeight; - if (card && height) { - card.style.setProperty('--max-height', height + 'px'); - } - } - - this.courseCardState = - this.courseCardState === CourseCard.expanded ? CourseCard.collapsed : CourseCard.expanded; - } - - private async handleOriginPath() { - this.originPath - ? await this.authHelper.setOriginPath(this.originPath) - : await this.authHelper.deleteOriginPath(); - } - ionViewWillEnter() { this.authHelper .getProvider('default') diff --git a/frontend/app/src/app/modules/profile/page/profile-page.html b/frontend/app/src/app/modules/profile/page/profile-page.html index d40321c6..2e03e686 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.html +++ b/frontend/app/src/app/modules/profile/page/profile-page.html @@ -23,76 +23,53 @@ -
- - - - - {{ userInfo.role ? (userInfo?.role | titlecase) : ('profile.role_guest' | translate | titlecase) }} - - - - - - - - - - {{ userInfo?.name }} -
- {{ 'profile.userInfo.studentId' | translate | uppercase }} - {{ userInfo?.studentId }} -
-
- {{ 'profile.userInfo.username' | translate | uppercase }} - {{ userInfo?.id }} -
- -
+ + + + + {{ userInfo.role ? (userInfo?.role | titlecase) : ('profile.role_guest' | translate | titlecase) }} + + + + + + + + + + {{ userInfo?.name }} +
+ {{ 'profile.userInfo.studentId' | translate | uppercase }} + {{ userInfo?.studentId }} +
+
+ {{ 'profile.userInfo.username' | translate | uppercase }} + {{ userInfo?.id }} +
+ +
+
+ + + - - - - - -
-
-
-
-
+ + + + + -
- {{ 'profile.titleCourses' | translate | uppercase }} - - - {{ 'profile.courses.today' | translate | uppercase }} - - - - -
{{ 'profile.courses.no_courses' | translate }}
-
- -
-
{{ myCourse?.startTime }} - {{ myCourse?.endTime }}
-
{{ myCourse?.course.event?.originalCategory }}
-
{{ myCourse.course?.event?.name }}
-
- {{ myCourse.course?.inPlace.name }} -
-
-
-
-
-
+ + +
diff --git a/frontend/app/src/app/modules/profile/page/profile-page.scss b/frontend/app/src/app/modules/profile/page/profile-page.scss index 8a250e45..4945293c 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.scss +++ b/frontend/app/src/app/modules/profile/page/profile-page.scss @@ -12,216 +12,106 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -:host { - section { - margin-bottom: calc(2 * var(--spacing-lg) - var(--spacing-md)); - padding: var(--spacing-md); +// TODO: clean up this mess +.user-card { + position: relative; - &:last-of-type { - margin-bottom: 0; - } - } + max-width: 400px; + margin: var(--spacing-xl); - .section-headline { - margin-bottom: var(--spacing-md); - } + border-radius: var(--border-radius-default); + box-shadow: var(--shadow-profile-card); - .user-card-wrapper { - margin-bottom: 0; + ion-card-header { + --background: var(--ion-color-tertiary); - .user-card { - position: relative; - - max-width: 400px; - margin: 0; - - border-radius: var(--border-radius-default); - box-shadow: var(--shadow-profile-card); - - ion-card-header { - --background: var(--ion-color-tertiary); - - display: flex; - align-items: center; - padding-top: var(--spacing-sm); - padding-bottom: var(--spacing-sm); - - ion-img { - display: block; - height: 36px; - margin-right: auto; - object-position: left 50%; - } - - span { - padding-top: 3px; - - font-size: var(--font-size-lg); - font-weight: var(--font-weight-bold); - line-height: 1; - color: var(--ion-color-light); - } - } - - ion-card-content { - min-height: 15vh; - - .profile-card-img { - position: absolute; - - width: 50%; - height: 100%; - margin-left: calc(var(--spacing-md) * -4); - - opacity: 0.13; - object-position: left bottom; - } - - .main-info { - display: grid; - grid-template-areas: - 'fullName fullName' - 'matriculationNumber userName' - 'email email'; - - ion-label { - display: block; - font-size: var(--font-size-sm); - font-weight: var(--font-weight-bold); - color: var(--ion-color-medium-shade); - } - - ion-text { - display: block; - font-size: var(--font-size-md); - font-weight: var(--font-weight-bold); - color: var(--ion-color-text); - } - - .full-name { - display: block; - grid-area: fullName; - - margin-bottom: var(--spacing-sm); - - font-size: var(--font-size-lg); - font-weight: var(--font-weight-bold); - } - - .matriculation-number { - grid-area: matriculationNumber; - margin-bottom: var(--spacing-sm); - } - - .user-name { - grid-area: userName; - margin-bottom: var(--spacing-sm); - } - - .email { - grid-area: email; - } - } - - .log-in-prompt { - margin: auto 0; - font-size: var(--font-size-lg); - font-weight: var(--font-weight-semi-bold); - color: var(--ion-color-text); - } - } - } - } - - ion-thumbnail { + display: flex; align-items: center; + padding-top: var(--spacing-sm); + padding-bottom: var(--spacing-sm); - width: 80%; - height: 80%; - margin: 0; - padding: 10px; - - background: var(--placeholder-gray); - border-radius: var(--border-radius-default); - - ion-icon { + ion-img { display: block; - width: 100%; - height: 100%; - color: white; + height: 36px; + margin-right: auto; + object-position: left 50%; + } + + span { + padding-top: 3px; + + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + line-height: 1; + color: var(--ion-color-light); } } - ion-row.main-info { - margin-bottom: 2px; - font-weight: bold; - } + ion-card-content { + min-height: 15vh; - .courses { - .courses-card { - max-width: 800px; - margin: 0; + .profile-card-img { + position: absolute; - background-color: unset; - border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; - box-shadow: none; + width: 50%; + height: 100%; + margin-left: calc(var(--spacing-md) * -4); - ion-card-header { - display: flex; - align-items: center; - justify-content: space-between; + opacity: 0.13; + object-position: left bottom; + } - background-color: var(--ion-item-background); - border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; + .main-info { + display: grid; + grid-template-areas: + 'fullName fullName' + 'matriculationNumber userName' + 'email email'; - span { - font-size: var(--font-size-lg); - font-weight: var(--font-weight-bold); - color: var(--ion-item-background-color-contrast); - } - - ion-icon { - cursor: pointer; - color: var(--ion-color-light); - } + ion-label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + color: var(--ion-color-medium-shade); } - ion-card-content { - overflow: hidden; - - max-height: 0; - margin: 0; - padding: 0; - - background-color: var(--ion-item-background); - border-radius: var(--border-radius-default); - - transition: max-height 250ms ease-in-out, padding 250ms ease-in-out, margin 250ms ease-in-out; - - &.show-card { - display: block; - - height: 100%; - max-height: var(--max-height); - margin: var(--spacing-xxl); - padding: var(--spacing-md); - } - - div { - font-size: var(--font-size-md); - font-weight: var(--font-weight-black); - color: var(--ion-item-background-color-contrast); - text-align: center; - - &.no-course { - padding: var(--spacing-xxl) var(--spacing-lg); - } - - &.last { - margin-bottom: var(--spacing-xl); - } - } + ion-text { + display: block; + font-size: var(--font-size-md); + font-weight: var(--font-weight-bold); + color: var(--ion-color-text); } + + .full-name { + display: block; + grid-area: fullName; + + margin-bottom: var(--spacing-sm); + + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + } + + .matriculation-number { + grid-area: matriculationNumber; + margin-bottom: var(--spacing-sm); + } + + .user-name { + grid-area: userName; + margin-bottom: var(--spacing-sm); + } + + .email { + grid-area: email; + } + } + + .log-in-prompt { + margin: auto 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semi-bold); + color: var(--ion-color-text); } } } diff --git a/frontend/app/src/app/modules/profile/profile.module.ts b/frontend/app/src/app/modules/profile/profile.module.ts index fc09fc83..36d274af 100644 --- a/frontend/app/src/app/modules/profile/profile.module.ts +++ b/frontend/app/src/app/modules/profile/profile.module.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; @@ -25,6 +24,9 @@ import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {ProfilePageSectionComponent} from './page/profile-page-section.component'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; +import {DataModule} from '../data/data.module'; +import {MyCoursesComponent} from './page/my-courses.component'; +import {MomentModule} from 'ngx-moment'; const routes: Routes = [ { @@ -34,7 +36,7 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [ProfilePageComponent, ProfilePageSectionComponent], + declarations: [MyCoursesComponent, ProfilePageComponent, ProfilePageSectionComponent], imports: [ CommonModule, FormsModule, @@ -45,6 +47,8 @@ const routes: Routes = [ SwiperModule, UtilModule, ThingTranslateModule, + DataModule, + MomentModule, ], }) export class ProfilePageModule {} diff --git a/frontend/app/src/assets/i18n/de.json b/frontend/app/src/assets/i18n/de.json index 60831ff6..50bf50f4 100644 --- a/frontend/app/src/assets/i18n/de.json +++ b/frontend/app/src/assets/i18n/de.json @@ -501,8 +501,10 @@ "logInPrompt": "Bitte loggen Sie sich ein, um Ihre Nutzerdaten sehen zu können." }, "courses": { - "today": "Heute", - "no_courses": "Heute stehen keine Termine mehr an." + "no_courses": "Heute stehen keine Termine mehr an.", + "NO_EVENT": "Keine Termine", + "ONE_EVENT": "Ein Termin", + "MANY_EVENT": "{{count}} Termine" } }, "settings": { diff --git a/frontend/app/src/assets/i18n/en.json b/frontend/app/src/assets/i18n/en.json index 0d765a26..21b4f93a 100644 --- a/frontend/app/src/assets/i18n/en.json +++ b/frontend/app/src/assets/i18n/en.json @@ -501,8 +501,10 @@ "logInPrompt": "Please log in to view your user data." }, "courses": { - "today": "Today", - "no_courses": "There are no more appointments scheduled today." + "no_courses": "There are no more appointments scheduled today.", + "NO_EVENT": "no events", + "ONE_EVENT": "one event", + "MANY_EVENT": "{{count}} events" } }, "settings": { diff --git a/frontend/app/src/assets/icons.min.woff2 b/frontend/app/src/assets/icons.min.woff2 index 3b9e8368..3cc1beeb 100644 Binary files a/frontend/app/src/assets/icons.min.woff2 and b/frontend/app/src/assets/icons.min.woff2 differ