mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-02-04 07:52:47 +00:00
feat: backend-supplied id cards on profile page
feat: SCIdCard thing
This commit is contained in:
6
.changeset/old-bananas-live.md
Normal file
6
.changeset/old-bananas-live.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/core': minor
|
||||||
|
'@openstapps/app': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for web-service-provided id cards on the profile page
|
||||||
5
.changeset/unlucky-pillows-thank.md
Normal file
5
.changeset/unlucky-pillows-thank.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/app': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make section swiper buttons reactive
|
||||||
@@ -70,6 +70,10 @@ const config = {
|
|||||||
authProvider: 'paia',
|
authProvider: 'paia',
|
||||||
url: 'https://hds.hebis.de/paia/core',
|
url: 'https://hds.hebis.de/paia/core',
|
||||||
},
|
},
|
||||||
|
/** TODO: idCards: {
|
||||||
|
authProvider: 'default',
|
||||||
|
url: 'TODO',
|
||||||
|
} */
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aboutPages,
|
aboutPages,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const DataIcons: Record<SCThingType, string> = {
|
|||||||
'dish': SCIcon`lunch_dining`,
|
'dish': SCIcon`lunch_dining`,
|
||||||
'favorite': SCIcon`favorite`,
|
'favorite': SCIcon`favorite`,
|
||||||
'floor': SCIcon`foundation`,
|
'floor': SCIcon`foundation`,
|
||||||
|
'id card': SCIcon`badge`,
|
||||||
'message': SCIcon`newspaper`,
|
'message': SCIcon`newspaper`,
|
||||||
'organization': SCIcon`business_center`,
|
'organization': SCIcon`business_center`,
|
||||||
'periodical': SCIcon`feed`,
|
'periodical': SCIcon`feed`,
|
||||||
|
|||||||
29
frontend/app/src/app/modules/profile/id-card.component.ts
Normal file
29
frontend/app/src/app/modules/profile/id-card.component.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
||||||
|
import {SCIdCard} from '@openstapps/core';
|
||||||
|
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
|
||||||
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
|
import {AsyncPipe, NgIf, TitleCasePipe} from '@angular/common';
|
||||||
|
import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'stapps-id-card',
|
||||||
|
templateUrl: 'id-card.html',
|
||||||
|
styleUrls: ['id-card.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
hostDirectives: [FullScreenImageDirective],
|
||||||
|
imports: [
|
||||||
|
FullScreenImageDirective,
|
||||||
|
ThingTranslateModule,
|
||||||
|
NgIf,
|
||||||
|
InRangeNowPipe,
|
||||||
|
ToDateRangePipe,
|
||||||
|
AsyncPipe,
|
||||||
|
TranslateModule,
|
||||||
|
TitleCasePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class IdCardComponent {
|
||||||
|
@Input({required: true}) item: SCIdCard;
|
||||||
|
}
|
||||||
4
frontend/app/src/app/modules/profile/id-card.html
Normal file
4
frontend/app/src/app/modules/profile/id-card.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<img [src]="item.image" [alt]="'name' | thingTranslate : item" draggable="false" />
|
||||||
|
<div *ngIf="item.validity && (item.validity | toDateRange | isInRangeNow | async) === false" class="expired">
|
||||||
|
{{ 'profile.userInfo.expired' | translate | titlecase }}
|
||||||
|
</div>
|
||||||
34
frontend/app/src/app/modules/profile/id-card.scss
Normal file
34
frontend/app/src/app/modules/profile/id-card.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host:fullscreen {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired {
|
||||||
|
position: absolute;
|
||||||
|
top: 48px;
|
||||||
|
left: 48px;
|
||||||
|
transform-origin: center;
|
||||||
|
translate: -50% -50%;
|
||||||
|
rotate: -45deg;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 256px;
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--ion-color-danger-contrast);
|
||||||
|
|
||||||
|
background: var(--ion-color-danger);
|
||||||
|
}
|
||||||
51
frontend/app/src/app/modules/profile/id-cards.component.ts
Normal file
51
frontend/app/src/app/modules/profile/id-cards.component.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||||
|
import {IdCardsProvider} from './id-cards.provider';
|
||||||
|
import {SCIdCard} from '@openstapps/core';
|
||||||
|
import {IonicModule} from '@ionic/angular';
|
||||||
|
import {AsyncPipe, NgForOf, NgIf, TitleCasePipe} from '@angular/common';
|
||||||
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
|
import {UtilModule} from '../../util/util.module';
|
||||||
|
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
|
||||||
|
import {IdCardComponent} from './id-card.component';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'stapps-id-cards',
|
||||||
|
templateUrl: 'id-cards.html',
|
||||||
|
styleUrls: ['id-cards.scss'],
|
||||||
|
providers: [IdCardsProvider],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
IonicModule,
|
||||||
|
NgForOf,
|
||||||
|
NgIf,
|
||||||
|
AsyncPipe,
|
||||||
|
ThingTranslateModule,
|
||||||
|
UtilModule,
|
||||||
|
FullScreenImageDirective,
|
||||||
|
IdCardComponent,
|
||||||
|
TranslateModule,
|
||||||
|
TitleCasePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class IdCardsComponent {
|
||||||
|
idCards: Observable<SCIdCard[]> = this.idCardsProvider.getIdCards();
|
||||||
|
|
||||||
|
constructor(readonly idCardsProvider: IdCardsProvider) {}
|
||||||
|
}
|
||||||
25
frontend/app/src/app/modules/profile/id-cards.html
Normal file
25
frontend/app/src/app/modules/profile/id-cards.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2023 StApps
|
||||||
|
~ This program is free software: you can redistribute it and/or modify it
|
||||||
|
~ under the terms of the GNU General Public License as published by the Free
|
||||||
|
~ Software Foundation, version 3.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
~ more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with
|
||||||
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<stapps-section buttonColor="primary-contrast">
|
||||||
|
<simple-swiper *ngIf="idCards | async as idCards">
|
||||||
|
<div *ngIf="idCards.length === 0">
|
||||||
|
<div class="log-in">
|
||||||
|
{{'profile.userInfo.logInPrompt' | translate | sentencecase}}
|
||||||
|
<ion-icon name="person" fill="true"></ion-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<stapps-id-card *ngFor="let idCard of idCards" [item]="idCard"></stapps-id-card>
|
||||||
|
</simple-swiper>
|
||||||
|
</stapps-section>
|
||||||
74
frontend/app/src/app/modules/profile/id-cards.provider.ts
Normal file
74
frontend/app/src/app/modules/profile/id-cards.provider.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core';
|
||||||
|
import {from, Observable, of} from 'rxjs';
|
||||||
|
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||||
|
import {mergeMap, filter, map, startWith} from 'rxjs/operators';
|
||||||
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class IdCardsProvider {
|
||||||
|
constructor(
|
||||||
|
private authHelper: AuthHelperService,
|
||||||
|
private config: ConfigProvider,
|
||||||
|
private httpClient: HttpClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getIdCards(): Observable<SCIdCard[]> {
|
||||||
|
const feature = this.config.config.app.features.extern?.['idCards'];
|
||||||
|
const auth = this.authHelper.getProvider(feature?.authProvider ?? 'default');
|
||||||
|
|
||||||
|
return auth.isAuthenticated$.pipe(
|
||||||
|
mergeMap(isAuthenticated =>
|
||||||
|
isAuthenticated
|
||||||
|
? feature
|
||||||
|
? from(auth.getValidToken()).pipe(
|
||||||
|
mergeMap(token => this.fetchIdCards(feature.url, token.accessToken)),
|
||||||
|
)
|
||||||
|
: auth.user$.pipe(
|
||||||
|
filter(user => user !== undefined),
|
||||||
|
map(userInfo => this.authHelper.getUserFromUserInfo(userInfo as object)),
|
||||||
|
mergeMap(user => this.fetchFallbackIdCards(user)),
|
||||||
|
startWith([]),
|
||||||
|
)
|
||||||
|
: // TODO: find a better solution here (async pipe stuff...)
|
||||||
|
of([]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchIdCards(url: string, token: string): Observable<SCIdCard[]> {
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
return this.httpClient.post(url, null, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer: ${token}`,
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
}) as Observable<SCIdCard[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchFallbackIdCards(user: SCUserConfiguration): Observable<SCIdCard[]> {
|
||||||
|
return this.httpClient.get('/assets/examples/student-id.sample.svg', {responseType: 'text'}).pipe(
|
||||||
|
map(svg => {
|
||||||
|
let result = svg;
|
||||||
|
for (const key in user) {
|
||||||
|
result = result.replaceAll(`{{${key}}`, (user as unknown as Record<string, string>)[key]);
|
||||||
|
}
|
||||||
|
return `data:image/svg+xml;base64,${Buffer.from(result, 'base64').toString('base64')}`;
|
||||||
|
}),
|
||||||
|
map(image => [
|
||||||
|
{
|
||||||
|
name: 'Student ID',
|
||||||
|
image,
|
||||||
|
type: SCThingType.IdCard,
|
||||||
|
uid: '1234',
|
||||||
|
origin: {
|
||||||
|
name: 'Sample Origin',
|
||||||
|
type: SCThingOriginType.Remote,
|
||||||
|
indexed: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
frontend/app/src/app/modules/profile/id-cards.scss
Normal file
52
frontend/app/src/app/modules/profile/id-cards.scss
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
simple-swiper {
|
||||||
|
--swiper-slide-width: calc(min(90cqw, 12cm));
|
||||||
|
|
||||||
|
filter: drop-shadow(0 0 16px rgba(0 0 0 / 10%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-in {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: calc(min(90cqw, 12cm) * (53.98 / 85.6));
|
||||||
|
padding: var(--spacing-xxl);
|
||||||
|
|
||||||
|
color: var(--ion-color-medium);
|
||||||
|
|
||||||
|
background: var(--ion-color-light);
|
||||||
|
border-radius: 3mm;
|
||||||
|
outline: 1px dashed var(--ion-color-medium);
|
||||||
|
outline-offset: calc(-1 * var(--spacing-sm));
|
||||||
|
|
||||||
|
> ion-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
font-size: calc(min(90cqw, 12cm) / 2);
|
||||||
|
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
frontend/app/src/app/modules/profile/id-cards.spec.ts
Normal file
65
frontend/app/src/app/modules/profile/id-cards.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
|
import {IdCardsProvider} from './id-cards.provider';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||||
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
|
import {SCAuthorizationProviderType} from '@openstapps/core';
|
||||||
|
|
||||||
|
class FakeAuth {
|
||||||
|
isAuthenticated$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
getValidToken() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IdCards', () => {
|
||||||
|
let configProvider: ConfigProvider;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let authHelper: AuthHelperService;
|
||||||
|
let fakeAuth: FakeAuth;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configProvider = jasmine.createSpyObj('ConfigProvider', ['config']);
|
||||||
|
configProvider.config = {
|
||||||
|
app: {features: {extern: {idCards: {url: 'http://id-cards.local', authProvider: 'fakeAuth'}}}},
|
||||||
|
} as never;
|
||||||
|
httpClient = jasmine.createSpyObj('HttpClient', ['post']);
|
||||||
|
fakeAuth = new FakeAuth();
|
||||||
|
authHelper = jasmine.createSpyObj('AuthHelperService', ['getProvider']);
|
||||||
|
authHelper.getProvider = jasmine.createSpy().and.returnValue(fakeAuth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit undefined if not logged in', async () => {
|
||||||
|
const provider = new IdCardsProvider(authHelper, configProvider, httpClient);
|
||||||
|
expect(await firstValueFrom(provider.getIdCards())).toEqual([]);
|
||||||
|
expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit network result when logged in', async () => {
|
||||||
|
fakeAuth.isAuthenticated$.next(true);
|
||||||
|
httpClient.post = jasmine.createSpy().and.returnValue(of(['abc']));
|
||||||
|
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
|
||||||
|
const provider = new IdCardsProvider(authHelper, configProvider, httpClient);
|
||||||
|
expect(await firstValueFrom(provider.getIdCards())).toEqual(['abc' as never]);
|
||||||
|
expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType);
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
expect(httpClient.post).toHaveBeenCalledOnceWith('http://id-cards.local', null, {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer: fake-token',
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to logins', async () => {
|
||||||
|
const provider = new IdCardsProvider(authHelper, configProvider, httpClient);
|
||||||
|
const observable = provider.getIdCards();
|
||||||
|
expect(await firstValueFrom(observable)).toEqual([]);
|
||||||
|
httpClient.post = jasmine.createSpy().and.returnValue(of(['abc']));
|
||||||
|
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
|
||||||
|
fakeAuth.isAuthenticated$.next(true);
|
||||||
|
// this is counter-intuitive, but because we unsubscribed above the first value
|
||||||
|
// will now contain the network result.
|
||||||
|
expect(await firstValueFrom(observable)).toEqual(['abc' as never]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2023 StApps
|
||||||
|
~ This program is free software: you can redistribute it and/or modify it
|
||||||
|
~ under the terms of the GNU General Public License as published by the Free
|
||||||
|
~ Software Foundation, version 3.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
~ more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with
|
||||||
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
<ion-accordion-group *ngIf="myCourses | async as myCourses" [value]="myCourses[0][0]">
|
<ion-accordion-group *ngIf="myCourses | async as myCourses" [value]="myCourses[0][0]">
|
||||||
<ion-accordion
|
<ion-accordion
|
||||||
*ngFor="let myCoursesDay of myCourses"
|
*ngFor="let myCoursesDay of myCourses"
|
||||||
|
|||||||
@@ -14,11 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {AuthHelperService} from '../../auth/auth-helper.service';
|
import {AuthHelperService} from '../../auth/auth-helper.service';
|
||||||
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
|
import {SCAuthorizationProviderType} from '@openstapps/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {ScheduleProvider} from '../../calendar/schedule.provider';
|
import {ScheduleProvider} from '../../calendar/schedule.provider';
|
||||||
import {profilePageSections} from '../../../../config/profile-page-sections';
|
import {profilePageSections} from '../../../../config/profile-page-sections';
|
||||||
import {filter, map} from 'rxjs/operators';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -26,19 +25,8 @@ import {filter, map} from 'rxjs/operators';
|
|||||||
styleUrls: ['profile-page.scss'],
|
styleUrls: ['profile-page.scss'],
|
||||||
})
|
})
|
||||||
export class ProfilePageComponent {
|
export class ProfilePageComponent {
|
||||||
user$ = this.authHelper.getProvider('default').user$.pipe(
|
|
||||||
filter(user => user !== undefined),
|
|
||||||
map(userInfo => {
|
|
||||||
return this.authHelper.getUserFromUserInfo(userInfo as object);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
sections = profilePageSections;
|
sections = profilePageSections;
|
||||||
|
|
||||||
logins: SCAuthorizationProviderType[] = [];
|
|
||||||
|
|
||||||
userInfo?: SCUserConfiguration;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly authHelper: AuthHelperService,
|
readonly authHelper: AuthHelperService,
|
||||||
readonly activatedRoute: ActivatedRoute,
|
readonly activatedRoute: ActivatedRoute,
|
||||||
@@ -53,7 +41,6 @@ export class ProfilePageComponent {
|
|||||||
|
|
||||||
async signOut(providerType: SCAuthorizationProviderType) {
|
async signOut(providerType: SCAuthorizationProviderType) {
|
||||||
await this.authHelper.getProvider(providerType).signOut();
|
await this.authHelper.getProvider(providerType).signOut();
|
||||||
this.userInfo = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewWillEnter() {
|
ionViewWillEnter() {
|
||||||
|
|||||||
@@ -23,48 +23,7 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content color="light" parallax [parallaxSize]="130">
|
<ion-content color="light" parallax [parallaxSize]="130">
|
||||||
<ion-card class="user-card">
|
<stapps-id-cards></stapps-id-cards>
|
||||||
<ion-card-header>
|
|
||||||
<ion-img src="assets/imgs/header.svg"></ion-img>
|
|
||||||
<span *ngIf="user$ | async as userInfo">
|
|
||||||
{{ userInfo.role ? (userInfo?.role | titlecase) : ('profile.role_guest' | translate | titlecase) }}
|
|
||||||
</span>
|
|
||||||
</ion-card-header>
|
|
||||||
<ion-card-content>
|
|
||||||
<ion-img class="profile-card-img" src="assets/imgs/profile-card-head.svg"></ion-img>
|
|
||||||
<ion-grid>
|
|
||||||
<ion-row>
|
|
||||||
<ion-col size="3"></ion-col>
|
|
||||||
<ion-col
|
|
||||||
*ngIf="authHelper.getProvider('default').isAuthenticated$ | async as loggedIn; else logInPrompt"
|
|
||||||
size="9"
|
|
||||||
class="main-info"
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="user$ | async as userInfo">
|
|
||||||
<ion-text class="full-name"> {{ userInfo?.name }} </ion-text>
|
|
||||||
<div class="matriculation-number">
|
|
||||||
<ion-label> {{ 'profile.userInfo.studentId' | translate | uppercase }} </ion-label>
|
|
||||||
<ion-text> {{ userInfo?.studentId }} </ion-text>
|
|
||||||
</div>
|
|
||||||
<div class="user-name">
|
|
||||||
<ion-label> {{ 'profile.userInfo.username' | translate | uppercase }} </ion-label>
|
|
||||||
<ion-text>{{ userInfo?.id }}</ion-text>
|
|
||||||
</div>
|
|
||||||
<div class="email">
|
|
||||||
<ion-label> {{ 'profile.userInfo.email' | translate | uppercase }} </ion-label>
|
|
||||||
<ion-text> {{ userInfo?.email }} </ion-text>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ion-col>
|
|
||||||
<ng-template #logInPrompt>
|
|
||||||
<ion-col size="9">
|
|
||||||
<ion-text class="log-in-prompt"> {{ 'profile.userInfo.logInPrompt' | translate }} </ion-text>
|
|
||||||
</ion-col>
|
|
||||||
</ng-template>
|
|
||||||
</ion-row>
|
|
||||||
</ion-grid>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-card>
|
|
||||||
<stapps-profile-page-section
|
<stapps-profile-page-section
|
||||||
*ngFor="let section of sections"
|
*ngFor="let section of sections"
|
||||||
[item]="section"
|
[item]="section"
|
||||||
|
|||||||
@@ -12,106 +12,3 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
// TODO: clean up this mess
|
|
||||||
.user-card {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
max-width: 400px;
|
|
||||||
margin: var(--spacing-xl);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
|||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {MyCoursesComponent} from './page/my-courses.component';
|
import {MyCoursesComponent} from './page/my-courses.component';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
|
import {IdCardsComponent} from './id-cards.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -49,6 +50,7 @@ const routes: Routes = [
|
|||||||
ThingTranslateModule,
|
ThingTranslateModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
MomentModule,
|
MomentModule,
|
||||||
|
IdCardsComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ProfilePageModule {}
|
export class ProfilePageModule {}
|
||||||
|
|||||||
24
frontend/app/src/app/util/full-screen-image.directive.ts
Normal file
24
frontend/app/src/app/util/full-screen-image.directive.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {Directive, ElementRef, HostListener} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'img[fullScreenImage]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class FullScreenImageDirective {
|
||||||
|
constructor(private host: ElementRef) {}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
async onClick() {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
await this.host.nativeElement.requestFullscreen();
|
||||||
|
if (
|
||||||
|
Math.sign(screen.width - screen.height) ===
|
||||||
|
Math.sign(this.host.nativeElement.width - this.host.nativeElement.height)
|
||||||
|
) {
|
||||||
|
await screen.orientation.lock('landscape');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
frontend/app/src/app/util/in-range.pipe.ts
Normal file
41
frontend/app/src/app/util/in-range.pipe.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {SCRange, isInRange, SCISO8601DateRange} from '@openstapps/core';
|
||||||
|
import {merge, Observable, timer} from 'rxjs';
|
||||||
|
import {distinctUntilChanged, map, startWith} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'isInRange',
|
||||||
|
pure: true,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class InRangePipe implements PipeTransform {
|
||||||
|
transform<T>(value: T, range: SCRange<T>): boolean {
|
||||||
|
return isInRange(value, range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toDateRange',
|
||||||
|
pure: true,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ToDateRangePipe implements PipeTransform {
|
||||||
|
transform(value: SCISO8601DateRange): SCRange<Date> {
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, new Date(value)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'isInRangeNow',
|
||||||
|
pure: true,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class InRangeNowPipe implements PipeTransform {
|
||||||
|
transform(value: SCRange<Date>): Observable<boolean> {
|
||||||
|
return merge(timer(value.lte || value.lt || 0), timer(value.gte || value.gt || 0)).pipe(
|
||||||
|
startWith(0),
|
||||||
|
map(() => isInRange(new Date(), value)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col>
|
<ion-col *ngIf="title || item">
|
||||||
<a *ngIf="item; else titleTemplate" [routerLink]="['/data-detail', item.uid]" [state]="{item}">
|
<a *ngIf="item; else titleTemplate" [routerLink]="['/data-detail', item.uid]" [state]="{item}">
|
||||||
<ng-container *ngTemplateOutlet="titleTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="titleTemplate"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<ion-col size="auto" class="swiper-button">
|
<ion-col size="auto" class="swiper-button">
|
||||||
<ion-button
|
<ion-button
|
||||||
fill="clear"
|
fill="clear"
|
||||||
color="medium"
|
[color]="buttonColor"
|
||||||
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
|
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
|
||||||
[disabled]="false"
|
[disabled]="false"
|
||||||
>
|
>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<ion-col size="auto" class="swiper-button">
|
<ion-col size="auto" class="swiper-button">
|
||||||
<ion-button
|
<ion-button
|
||||||
fill="clear"
|
fill="clear"
|
||||||
color="medium"
|
[color]="buttonColor"
|
||||||
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
|
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
|
||||||
[disabled]="false"
|
[disabled]="false"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export class SectionComponent implements AfterContentInit {
|
|||||||
|
|
||||||
@Input() item?: SCThings;
|
@Input() item?: SCThings;
|
||||||
|
|
||||||
|
@Input() buttonColor = 'medium';
|
||||||
|
|
||||||
nativeElement = new ReplaySubject<HTMLElement>(1);
|
nativeElement = new ReplaySubject<HTMLElement>(1);
|
||||||
|
|
||||||
swiper = this.nativeElement.pipe(
|
swiper = this.nativeElement.pipe(
|
||||||
|
|||||||
28
frontend/app/src/assets/examples/student-id.sample.svg
Normal file
28
frontend/app/src/assets/examples/student-id.sample.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 68 KiB |
@@ -512,6 +512,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userInfo": {
|
"userInfo": {
|
||||||
|
"expired": "Abgelaufen",
|
||||||
"studentId": "Matrikelnr.",
|
"studentId": "Matrikelnr.",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -512,6 +512,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userInfo": {
|
"userInfo": {
|
||||||
|
"expired": "expired",
|
||||||
"studentId": "Matriculation Nr.",
|
"studentId": "Matriculation Nr.",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export * from './things/diff.js';
|
|||||||
export * from './things/dish.js';
|
export * from './things/dish.js';
|
||||||
export * from './things/favorite.js';
|
export * from './things/favorite.js';
|
||||||
export * from './things/floor.js';
|
export * from './things/floor.js';
|
||||||
|
export * from './things/id-card.js';
|
||||||
export * from './things/message.js';
|
export * from './things/message.js';
|
||||||
export * from './things/organization.js';
|
export * from './things/organization.js';
|
||||||
export * from './things/periodical.js';
|
export * from './things/periodical.js';
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {SCTicket, SCTicketMeta, SCTicketWithoutReferences} from './things/ticket
|
|||||||
import {SCToDo, SCToDoMeta, SCToDoWithoutReferences} from './things/todo.js';
|
import {SCToDo, SCToDoMeta, SCToDoWithoutReferences} from './things/todo.js';
|
||||||
import {SCTour, SCTourMeta, SCTourWithoutReferences} from './things/tour.js';
|
import {SCTour, SCTourMeta, SCTourWithoutReferences} from './things/tour.js';
|
||||||
import {SCVideo, SCVideoMeta, SCVideoWithoutReferences} from './things/video.js';
|
import {SCVideo, SCVideoMeta, SCVideoWithoutReferences} from './things/video.js';
|
||||||
|
import {SCIdCard, SCIdCardMeta, SCIdCardWithoutReferences} from './things/id-card.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of things, from type to meta data
|
* A map of things, from type to meta data
|
||||||
@@ -80,6 +81,7 @@ export const SCClasses: {[K in SCThingType]: object} = {
|
|||||||
'dish': SCDishMeta,
|
'dish': SCDishMeta,
|
||||||
'favorite': SCFavoriteMeta,
|
'favorite': SCFavoriteMeta,
|
||||||
'floor': SCFloorMeta,
|
'floor': SCFloorMeta,
|
||||||
|
'id card': SCIdCardMeta,
|
||||||
'message': SCMessageMeta,
|
'message': SCMessageMeta,
|
||||||
'organization': SCOrganizationMeta,
|
'organization': SCOrganizationMeta,
|
||||||
'periodical': SCPeriodicalMeta,
|
'periodical': SCPeriodicalMeta,
|
||||||
@@ -111,6 +113,7 @@ export type SCIndexableThings =
|
|||||||
| SCDateSeries
|
| SCDateSeries
|
||||||
| SCDish
|
| SCDish
|
||||||
| SCFloor
|
| SCFloor
|
||||||
|
| SCIdCard
|
||||||
| SCMessage
|
| SCMessage
|
||||||
| SCOrganization
|
| SCOrganization
|
||||||
| SCPeriodical
|
| SCPeriodical
|
||||||
@@ -167,6 +170,8 @@ export type SCAssociatedThingWithoutReferences<THING extends SCThings> = THING e
|
|||||||
? SCFavoriteWithoutReferences
|
? SCFavoriteWithoutReferences
|
||||||
: THING extends SCFloor
|
: THING extends SCFloor
|
||||||
? SCFloorWithoutReferences
|
? SCFloorWithoutReferences
|
||||||
|
: THING extends SCIdCard
|
||||||
|
? SCIdCardWithoutReferences
|
||||||
: THING extends SCMessage
|
: THING extends SCMessage
|
||||||
? SCMessageWithoutReferences
|
? SCMessageWithoutReferences
|
||||||
: THING extends SCOrganization
|
: THING extends SCOrganization
|
||||||
@@ -230,6 +235,8 @@ export type SCAssociatedThing<THING extends SCThings> = THING extends SCAssessme
|
|||||||
? SCFavorite
|
? SCFavorite
|
||||||
: THING extends SCFloorWithoutReferences
|
: THING extends SCFloorWithoutReferences
|
||||||
? SCFloor
|
? SCFloor
|
||||||
|
: THING extends SCIdCardWithoutReferences
|
||||||
|
? SCIdCard
|
||||||
: THING extends SCMessageWithoutReferences
|
: THING extends SCMessageWithoutReferences
|
||||||
? SCMessage
|
? SCMessage
|
||||||
: THING extends SCOrganizationWithoutReferences
|
: THING extends SCOrganizationWithoutReferences
|
||||||
|
|||||||
@@ -21,6 +21,30 @@ import {SCISO8601Date} from '../../general/time.js';
|
|||||||
*/
|
*/
|
||||||
export type SCISO8601DateRange = SCRange<SCISO8601Date>;
|
export type SCISO8601DateRange = SCRange<SCISO8601Date>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a value is inside a range
|
||||||
|
* @param value the value to check
|
||||||
|
* @param range the range
|
||||||
|
*/
|
||||||
|
export function isInRange<T>(value: T, range: SCRange<T>): boolean {
|
||||||
|
return (
|
||||||
|
(range.lt == undefined ? (range.lte == undefined ? true : range.lte >= value) : range.lt > value) &&
|
||||||
|
(range.gt == undefined ? (range.gte == undefined ? true : range.gte <= value) : range.gt < value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a range
|
||||||
|
* @example '0..4'
|
||||||
|
* @example '1=..=3'
|
||||||
|
* @example '0..=3'
|
||||||
|
*/
|
||||||
|
export function formatRange<T>(range: SCRange<T>): string {
|
||||||
|
return `${range.gt ?? range.gte}${range.gte == undefined ? '' : '='}..${range.lte == undefined ? '' : '='}${
|
||||||
|
range.lt ?? range.lte
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic range type
|
* Generic range type
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export enum SCThingType {
|
|||||||
Dish = 'dish',
|
Dish = 'dish',
|
||||||
Favorite = 'favorite',
|
Favorite = 'favorite',
|
||||||
Floor = 'floor',
|
Floor = 'floor',
|
||||||
|
IdCard = 'id card',
|
||||||
Message = 'message',
|
Message = 'message',
|
||||||
Organization = 'organization',
|
Organization = 'organization',
|
||||||
Person = 'person',
|
Person = 'person',
|
||||||
|
|||||||
93
packages/core/src/things/id-card.ts
Normal file
93
packages/core/src/things/id-card.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2022 Open StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {SCMetaTranslations, SCTranslations} from '../general/i18n.js';
|
||||||
|
import {
|
||||||
|
SCThing,
|
||||||
|
SCThingMeta,
|
||||||
|
SCThingTranslatableProperties,
|
||||||
|
SCThingType,
|
||||||
|
SCThingWithoutReferences,
|
||||||
|
} from './abstract/thing.js';
|
||||||
|
import {SCISO8601DateRange} from './abstract/range.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ID-Card without references
|
||||||
|
*/
|
||||||
|
export interface SCIdCardWithoutReferences extends SCThingWithoutReferences {
|
||||||
|
/**
|
||||||
|
* Validity range
|
||||||
|
*/
|
||||||
|
validity?: SCISO8601DateRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type
|
||||||
|
*/
|
||||||
|
type: SCThingType.IdCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message
|
||||||
|
* @validatable
|
||||||
|
* @indexable
|
||||||
|
*/
|
||||||
|
export interface SCIdCard extends SCIdCardWithoutReferences, SCThing {
|
||||||
|
/**
|
||||||
|
* Translated fields of a message
|
||||||
|
*/
|
||||||
|
translations?: SCTranslations<SCIdCardTranslatableProperties>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type
|
||||||
|
*/
|
||||||
|
type: SCThingType.IdCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translatable properties of a message
|
||||||
|
*/
|
||||||
|
export interface SCIdCardTranslatableProperties extends SCThingTranslatableProperties {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta information about messages
|
||||||
|
*/
|
||||||
|
export class SCIdCardMeta extends SCThingMeta implements SCMetaTranslations<SCIdCard> {
|
||||||
|
/**
|
||||||
|
* Translations of fields
|
||||||
|
*/
|
||||||
|
fieldTranslations = {
|
||||||
|
de: {
|
||||||
|
...new SCThingMeta().fieldTranslations.de,
|
||||||
|
validity: 'Gültigkeit',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
...new SCThingMeta().fieldTranslations.en,
|
||||||
|
validity: 'validity',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translations of values of fields
|
||||||
|
*/
|
||||||
|
fieldValueTranslations = {
|
||||||
|
de: {
|
||||||
|
...new SCThingMeta().fieldValueTranslations.de,
|
||||||
|
type: 'Ausweis',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
...new SCThingMeta().fieldValueTranslations.en,
|
||||||
|
type: SCThingType.Message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
41
packages/core/test/range.spec.ts
Normal file
41
packages/core/test/range.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {expect} from 'chai';
|
||||||
|
import {formatRange, isInRange} from '../src/index.js';
|
||||||
|
import {SCRange} from '../lib/index.js';
|
||||||
|
|
||||||
|
const cases: Record<'accept' | 'reject', [number, SCRange<number>][]> = {
|
||||||
|
accept: [
|
||||||
|
[4, {gt: 3, lt: 5}],
|
||||||
|
[4, {gte: 4, lte: 4}],
|
||||||
|
[3, {gt: 2, lt: 4}],
|
||||||
|
[5, {gte: 3, lte: 5}],
|
||||||
|
[10, {gt: 5, lt: 15}],
|
||||||
|
[0, {gte: 0, lte: 10}],
|
||||||
|
],
|
||||||
|
reject: [
|
||||||
|
[4, {gt: 3, lt: 4}],
|
||||||
|
[4, {gte: 5, lte: 6}],
|
||||||
|
[2, {gt: 5, lt: 10}],
|
||||||
|
[6, {gte: 7, lte: 8}],
|
||||||
|
[-1, {gt: 0, lt: 5}],
|
||||||
|
[20, {gte: 10, lte: 15}],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Range', () => {
|
||||||
|
for (const constructor of ['Number', 'Date'] as const) {
|
||||||
|
describe(`${constructor} range`, () => {
|
||||||
|
for (const [accept, [value, range]] of Object.entries(cases).flatMap(([accept, cases]) =>
|
||||||
|
cases.map(it => [accept, it] as const),
|
||||||
|
)) {
|
||||||
|
it(`should ${accept} ${value} in the range ${formatRange(range)}`, () => {
|
||||||
|
const result = isInRange(constructor === 'Number' ? value : new Date(value), range);
|
||||||
|
if (accept === 'accept') {
|
||||||
|
expect(result).to.be.true;
|
||||||
|
} else {
|
||||||
|
expect(result).to.be.false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user