summaryrefslogtreecommitdiff
path: root/overlays/spm/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'overlays/spm/frontend')
-rw-r--r--overlays/spm/frontend/angular.json2
-rw-r--r--overlays/spm/frontend/package.json5
-rw-r--r--overlays/spm/frontend/src/app/app-routing.module.ts11
-rw-r--r--overlays/spm/frontend/src/app/app.component.html28
-rw-r--r--overlays/spm/frontend/src/app/app.component.sass2
-rw-r--r--overlays/spm/frontend/src/app/app.component.ts89
-rw-r--r--overlays/spm/frontend/src/app/app.module.ts14
-rw-r--r--overlays/spm/frontend/src/app/color-scheme.service.ts62
-rw-r--r--overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts16
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.html34
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.sass13
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.ts181
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.module.ts31
-rw-r--r--overlays/spm/frontend/src/custom-theme.scss2
-rw-r--r--overlays/spm/frontend/src/styles.sass1
-rw-r--r--overlays/spm/frontend/yarn-project.nix8
-rw-r--r--overlays/spm/frontend/yarn.lock41
17 files changed, 441 insertions, 99 deletions
diff --git a/overlays/spm/frontend/angular.json b/overlays/spm/frontend/angular.json
index cc08d665..a25bfd0b 100644
--- a/overlays/spm/frontend/angular.json
+++ b/overlays/spm/frontend/angular.json
@@ -54,7 +54,7 @@
54 "assets": [ 54 "assets": [
55 "src/assets", 55 "src/assets",
56 { 56 {
57 "glob": "@(thumb_up)/baseline.svg", 57 "glob": "@(add|check|delete_forever)/baseline.svg",
58 "input": "./node_modules/@material-icons/svg/svg", 58 "input": "./node_modules/@material-icons/svg/svg",
59 "output": "/icons/" 59 "output": "/icons/"
60 } 60 }
diff --git a/overlays/spm/frontend/package.json b/overlays/spm/frontend/package.json
index 74010466..54a301a0 100644
--- a/overlays/spm/frontend/package.json
+++ b/overlays/spm/frontend/package.json
@@ -15,16 +15,21 @@
15 "@angular/common": "~13.3.0", 15 "@angular/common": "~13.3.0",
16 "@angular/compiler": "~13.3.0", 16 "@angular/compiler": "~13.3.0",
17 "@angular/core": "~13.3.0", 17 "@angular/core": "~13.3.0",
18 "@angular/flex-layout": "^13.0.0-beta.38",
18 "@angular/forms": "~13.3.0", 19 "@angular/forms": "~13.3.0",
19 "@angular/material": "13.3.7", 20 "@angular/material": "13.3.7",
20 "@angular/platform-browser": "~13.3.0", 21 "@angular/platform-browser": "~13.3.0",
21 "@angular/platform-browser-dynamic": "~13.3.0", 22 "@angular/platform-browser-dynamic": "~13.3.0",
22 "@angular/router": "~13.3.0", 23 "@angular/router": "~13.3.0",
23 "@fontsource/roboto": "^4.5.7", 24 "@fontsource/roboto": "^4.5.7",
25 "@fontsource/source-sans-pro": "^4.5.10",
24 "@material-icons/svg": "^1.0.28", 26 "@material-icons/svg": "^1.0.28",
27 "@types/uuid": "^8.3.4",
28 "jwt-decode": "^3.1.2",
25 "ngx-webstorage": "9.0.0", 29 "ngx-webstorage": "9.0.0",
26 "rxjs": "~7.5.0", 30 "rxjs": "~7.5.0",
27 "tslib": "^2.3.0", 31 "tslib": "^2.3.0",
32 "uuid": "^8.3.2",
28 "zone.js": "~0.11.4" 33 "zone.js": "~0.11.4"
29 }, 34 },
30 "devDependencies": { 35 "devDependencies": {
diff --git a/overlays/spm/frontend/src/app/app-routing.module.ts b/overlays/spm/frontend/src/app/app-routing.module.ts
index 2da97cea..bffe8c4d 100644
--- a/overlays/spm/frontend/src/app/app-routing.module.ts
+++ b/overlays/spm/frontend/src/app/app-routing.module.ts
@@ -1,17 +1,14 @@
1import { NgModule } from '@angular/core'; 1import { NgModule } from '@angular/core';
2import { RouterModule, Routes } from '@angular/router'; 2import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
3
4import { SpmComponent } from './spm/spm.component';
5import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
6 3
7const routes: Routes = [ 4const routes: Routes = [
8 { path: 'spm', component: SpmComponent }, 5 { path: 'spm', loadChildren: () => import('./spm/spm.module').then(m => m.SpmModule) },
9 { path: '', redirectTo: '/spm', pathMatch: 'full' }, 6 { path: '', redirectTo: '/spm', pathMatch: 'full' },
10 { path: '**', component: PageNotFoundComponent } 7 { path: '**', loadChildren: () => import('./page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }
11]; 8];
12 9
13@NgModule({ 10@NgModule({
14 imports: [RouterModule.forRoot(routes)], 11 imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
15 exports: [RouterModule] 12 exports: [RouterModule]
16}) 13})
17export class AppRoutingModule { } 14export class AppRoutingModule { }
diff --git a/overlays/spm/frontend/src/app/app.component.html b/overlays/spm/frontend/src/app/app.component.html
index f9b5b8fc..1a2f88b0 100644
--- a/overlays/spm/frontend/src/app/app.component.html
+++ b/overlays/spm/frontend/src/app/app.component.html
@@ -1,7 +1,21 @@
1<mat-toolbar color="primary"> 1<div gdAreas="nav | content" gdRows="min-content 1fr" style="min-height: 100vh">
2 <div> 2 <mat-toolbar color="primary" gdArea="nav" fxLayout="flex" fxLayoutGap="16px">
3 <a mat-button routerLink="/spm" routerLinkActive="active">spm</a> 3 <div>
4 </div> 4 <span class="mono" [innerHTML]="apiDomain$ | async"></span>
5</mat-toolbar> 5 </div>
6<!-- The routed views render in the <router-outlet> --> 6 <div>
7<router-outlet></router-outlet> 7 <a mat-button routerLink="/spm" routerLinkActive="active">spm</a>
8 </div>
9 </mat-toolbar>
10 <!-- The routed views render in the <router-outlet> -->
11 <ng-template #router_loading_indicator>
12 <div gdArea="content" fxLayout="column" fxLayoutAlign="center stretch">
13 <div fxFlex="none" fxFlexAlign="center">
14 <mat-spinner></mat-spinner>
15 </div>
16 </div>
17 </ng-template>
18 <div class="content" gdArea="content" *ngIf="!(routerLoading$ | async); else router_loading_indicator">
19 <router-outlet></router-outlet>
20 </div>
21</div>
diff --git a/overlays/spm/frontend/src/app/app.component.sass b/overlays/spm/frontend/src/app/app.component.sass
index e69de29b..4732ea49 100644
--- a/overlays/spm/frontend/src/app/app.component.sass
+++ b/overlays/spm/frontend/src/app/app.component.sass
@@ -0,0 +1,2 @@
1.content
2 padding: 16px
diff --git a/overlays/spm/frontend/src/app/app.component.ts b/overlays/spm/frontend/src/app/app.component.ts
index 7c231203..11c0f772 100644
--- a/overlays/spm/frontend/src/app/app.component.ts
+++ b/overlays/spm/frontend/src/app/app.component.ts
@@ -1,6 +1,18 @@
1import { Component, OnInit, OnDestroy } from '@angular/core'; 1import { Component, OnInit, OnDestroy, Inject, Renderer2, RendererFactory2 } from '@angular/core';
2import { DOCUMENT } from '@angular/common';
3import { MediaMatcher } from '@angular/cdk/layout';
4import { LocalStorage } from 'ngx-webstorage';
5import { BehaviorSubject, Subscription, Subject } from 'rxjs';
6import { Router, RouterEvent, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
7import { HttpClient, HttpHeaders } from '@angular/common/http';
2 8
3import { ColorSchemeService } from './color-scheme.service'; 9import { map, filter } from 'rxjs/operators';
10
11type ColorScheme = 'dark' | 'light';
12
13export interface DomainResponse {
14 domain: string;
15}
4 16
5@Component({ 17@Component({
6 selector: 'app-root', 18 selector: 'app-root',
@@ -8,14 +20,81 @@ import { ColorSchemeService } from './color-scheme.service';
8 styleUrls: ['./app.component.sass'] 20 styleUrls: ['./app.component.sass']
9}) 21})
10export class AppComponent implements OnInit, OnDestroy { 22export class AppComponent implements OnInit, OnDestroy {
23 private renderer: Renderer2;
24 colorSchemeMatcher: MediaQueryList;
25
11 title = 'spm-frontend'; 26 title = 'spm-frontend';
12 27
13 constructor(private colorSchemeService: ColorSchemeService) {} 28 public activeColorScheme$: BehaviorSubject<ColorScheme> = new BehaviorSubject<ColorScheme>('dark');
29
30 @LocalStorage('prefers-color-scheme')
31 public colorSchemeOverride?: ColorScheme;
32
33 colorSchemeSubscription?: Subscription;
34
35 router: Router;
36 public routerLoading$: Subject<boolean> = new Subject<boolean>();
37 routerEventSubscription?: Subscription;
38
39 http: HttpClient;
40 apiDomain$: Subject<string> = new Subject<string>();
41
42 constructor( rendererFactory: RendererFactory2,
43 mediaMatcher: MediaMatcher,
44 @Inject(DOCUMENT) private document: Document,
45 router: Router,
46 http: HttpClient,
47 ) {
48 this.renderer = rendererFactory.createRenderer(null, null);
49 this.colorSchemeMatcher = mediaMatcher.matchMedia('(prefers-color-scheme: dark)');
50 this.document = document;
51
52 this.colorSchemeListener = this.colorSchemeListener.bind(this);
53 this.colorSchemeMatcher.addEventListener('change', this.colorSchemeListener);
54 this.activeColorScheme$.next(this.getColorScheme());
55
56 this.router = router;
57 this.http = http;
58 }
59
60 private colorSchemeListener(event: { matches: boolean; }) {
61 this.activeColorScheme$.next(this.getColorScheme(event));
62 }
63
64 private getColorScheme(event?: { matches: boolean; }): ColorScheme {
65 if (this.colorSchemeOverride) {
66 return this.colorSchemeOverride;
67 } else if (event) {
68 return event.matches ? 'dark' : 'light';
69 } else {
70 return this.colorSchemeMatcher.matches ? 'dark' : 'light';
71 }
72 }
73
74 updateColorScheme(scheme: ColorScheme) {
75 this.colorSchemeOverride = scheme;
76 this.activeColorScheme$.next(scheme);
77 }
14 78
15 ngOnInit() { 79 ngOnInit() {
16 this.colorSchemeService.init(); 80 this.colorSchemeSubscription = this.activeColorScheme$.subscribe(scheme => { this.renderer.setAttribute(this.document.body, 'data-color-scheme', scheme); });
81
82 this.routerEventSubscription = this.router.events.pipe(
83 filter(event =>
84 event instanceof NavigationStart ||
85 event instanceof NavigationEnd ||
86 event instanceof NavigationCancel ||
87 event instanceof NavigationError
88 ),
89 map(event => event instanceof NavigationStart)
90 ).subscribe(this.routerLoading$);
91
92 this.http.get<DomainResponse>('/domain', { headers: new HttpHeaders({ 'Accept': 'application/json' }) }).pipe(map((resp: DomainResponse) => resp.domain)).subscribe(this.apiDomain$);
17 } 93 }
18 ngOnDestroy() { 94 ngOnDestroy() {
19 this.colorSchemeService.destroy(); 95 this.colorSchemeMatcher.removeEventListener('change', this.colorSchemeListener);
96
97 this.colorSchemeSubscription?.unsubscribe();
98 this.routerEventSubscription?.unsubscribe();
20 } 99 }
21} 100}
diff --git a/overlays/spm/frontend/src/app/app.module.ts b/overlays/spm/frontend/src/app/app.module.ts
index 914f73a2..98e5106e 100644
--- a/overlays/spm/frontend/src/app/app.module.ts
+++ b/overlays/spm/frontend/src/app/app.module.ts
@@ -1,30 +1,30 @@
1import { NgModule } from '@angular/core'; 1import { NgModule } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser'; 2import { BrowserModule } from '@angular/platform-browser';
3import { NgxWebstorageModule } from 'ngx-webstorage';
3 4
4import { AppRoutingModule } from './app-routing.module'; 5import { AppRoutingModule } from './app-routing.module';
5import { AppComponent } from './app.component'; 6import { AppComponent } from './app.component';
6import { SpmComponent } from './spm/spm.component';
7import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
8import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9import { MatIconModule } from '@angular/material/icon';
10import { HttpClientModule } from "@angular/common/http"; 8import { HttpClientModule } from "@angular/common/http";
11import { MatToolbarModule } from '@angular/material/toolbar'; 9import { MatToolbarModule } from '@angular/material/toolbar';
12import { MatButtonModule } from '@angular/material/button'; 10import { MatButtonModule } from '@angular/material/button';
11import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
12import { FlexLayoutModule } from "@angular/flex-layout";
13 13
14@NgModule({ 14@NgModule({
15 declarations: [ 15 declarations: [
16 AppComponent, 16 AppComponent,
17 SpmComponent,
18 PageNotFoundComponent
19 ], 17 ],
20 imports: [ 18 imports: [
21 BrowserModule, 19 BrowserModule,
20 NgxWebstorageModule.forRoot(),
22 AppRoutingModule, 21 AppRoutingModule,
23 BrowserAnimationsModule, 22 BrowserAnimationsModule,
24 MatIconModule,
25 HttpClientModule, 23 HttpClientModule,
26 MatToolbarModule, 24 MatToolbarModule,
27 MatButtonModule 25 MatButtonModule,
26 MatProgressSpinnerModule,
27 FlexLayoutModule,
28 ], 28 ],
29 providers: [], 29 providers: [],
30 bootstrap: [AppComponent] 30 bootstrap: [AppComponent]
diff --git a/overlays/spm/frontend/src/app/color-scheme.service.ts b/overlays/spm/frontend/src/app/color-scheme.service.ts
deleted file mode 100644
index c56e9c23..00000000
--- a/overlays/spm/frontend/src/app/color-scheme.service.ts
+++ /dev/null
@@ -1,62 +0,0 @@
1import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
2import { MediaMatcher } from '@angular/cdk/layout';
3import { LocalStorage } from 'ngx-webstorage';
4
5type ColorScheme = "dark" | "light";
6
7@Injectable({
8 providedIn: 'root'
9})
10export class ColorSchemeService {
11 private renderer: Renderer2;
12 matcher!: MediaQueryList;
13
14 @LocalStorage('prefers-color-scheme')
15 public colorSchemeOverride: ColorScheme;
16
17 public activeColorScheme: BehaviorSubject<ColorScheme> = new BehaviorSubject("light");
18
19 constructor(rendererFactory: RendererFactory2,
20 mediaMatcher: MediaMatcher
21 ) {
22 // Create new renderer from renderFactory, to make it possible to use renderer2 in a service
23 this.renderer = rendererFactory.createRenderer(null, null);
24 this.matcher = mediaMatcher.matchMedia('(prefers-color-scheme: dark)');
25
26 this._listener = this._listener.bind(this);
27 }
28
29 init() {
30 this.matcher.addEventListener('change', this._listener);
31 this.load();
32 }
33
34 _listener(event: { matches: any; }) {
35 this.activeColorScheme.next(event.matches ? 'dark' : 'light');
36 }
37
38 destroy() {
39 this.matcher.removeEventListener('change', this._listener);
40 }
41
42 _getColorScheme() {
43 if (this.colorSchemeOverride) {
44 this.colorSchemeOverride
45 } else {
46 this.matcher.matches ? 'dark' : 'light'
47 }
48 }
49
50 load() {
51 this.activeColorScheme.next(this._getColorScheme());
52 }
53
54 update(scheme: ColorScheme) {
55 this.colorSchemeOverride = scheme;
56 this.activeColorScheme.next(scheme);
57 }
58
59 currentActive() {
60 return this.colorScheme;
61 }
62}
diff --git a/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts b/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts
new file mode 100644
index 00000000..b9d8945c
--- /dev/null
+++ b/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts
@@ -0,0 +1,16 @@
1import { NgModule } from '@angular/core';
2import { PageNotFoundComponent } from './page-not-found.component';
3import { RouterModule } from '@angular/router';
4
5@NgModule({
6 declarations: [PageNotFoundComponent],
7 exports: [PageNotFoundComponent],
8 imports: [
9 RouterModule.forChild([{
10 path: '',
11 pathMatch: 'full',
12 component: PageNotFoundComponent
13 }]),
14 ]
15})
16export class PageNotFoundModule {}
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.html b/overlays/spm/frontend/src/app/spm/spm.component.html
index d1339d95..5d0e625a 100644
--- a/overlays/spm/frontend/src/app/spm/spm.component.html
+++ b/overlays/spm/frontend/src/app/spm/spm.component.html
@@ -1,3 +1,33 @@
1<p>spm works!</p> 1<div id="mail-panel-container" fxLayout="row wrap" style="gap: 16px">
2 <ng-template ngFor [ngForOf]="spmMails$ | async | keyvalue: asIsOrder" let-entry>
3 <mat-card>
4 <mat-card-title class="mono" *ngIf="entry.value.state !== 'loading'">{{entry.value.local}}</mat-card-title>
5 <mat-card-subtitle class="mono" *ngIf="entry.value.state !== 'loading'">@{{entry.value.domain}}</mat-card-subtitle>
6 <mat-card-content *ngIf="entry.value.state === 'loading'">
7 <mat-spinner style="margin: auto"></mat-spinner>
8 </mat-card-content>
9 <mat-card-actions>
10 <button mat-raised-button color="primary" (click)="claim(entry.key)" [disabled]="entry.value.state === 'loaded' || entry.value.state === 'claiming' ? null : true">
11 <mat-icon aria-hidden="false" aria-label="Claim" svgIcon="check"></mat-icon>
12 <span *ngIf="entry.value.state !== 'claimed'">Claim</span>
13 <span *ngIf="entry.value.state === 'claimed'">Claimed</span>
14 </button>
15 <button mat-stroked-button color="warn" (click)="forget(entry.key)">
16 <mat-icon aria-hidden="false" aria-label="Forget" svgIcon="delete_forever"></mat-icon>
17 <span *ngIf="entry.value.state !== 'claimed'">Forget</span>
18 <span *ngIf="entry.value.state === 'claimed'">Hide</span>
19 </button>
20 </mat-card-actions>
21 <mat-card-footer *ngIf="entry.value.state === 'loaded' && entry.value.percent_expiration">
22 <mat-progress-bar mode="determinate" [value]="entry.value.percent_expiration"></mat-progress-bar>
23 </mat-card-footer>
24 <mat-card-footer *ngIf="entry.value.state === 'claiming'">
25 <mat-progress-bar mode="indeterminate"></mat-progress-bar>
26 </mat-card-footer>
27 </mat-card>
28 </ng-template>
29</div>
2 30
3<mat-icon aria-hidden="false" aria-label="Thumbs up" svgIcon="thumb_up"></mat-icon> 31<button id="add-button" mat-fab color="accent" (click)="add()">
32 <mat-icon aria-hidden="false" aria-label="Add" svgIcon="add"></mat-icon>
33</button>
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.sass b/overlays/spm/frontend/src/app/spm/spm.component.sass
index e69de29b..bda4dd4a 100644
--- a/overlays/spm/frontend/src/app/spm/spm.component.sass
+++ b/overlays/spm/frontend/src/app/spm/spm.component.sass
@@ -0,0 +1,13 @@
1#add-button
2 position: fixed
3 bottom: 16px
4 right: 16px
5
6#mail-panel-container
7 margin-bottom: 88px
8
9.mat-card
10 overflow: hidden
11
12.mat-card-footer
13 margin: -16px
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.ts b/overlays/spm/frontend/src/app/spm/spm.component.ts
index 7d172052..f0655b55 100644
--- a/overlays/spm/frontend/src/app/spm/spm.component.ts
+++ b/overlays/spm/frontend/src/app/spm/spm.component.ts
@@ -1,24 +1,193 @@
1import { Component, OnInit } from '@angular/core'; 1import { Component, OnInit, OnDestroy } from '@angular/core';
2import { MatIconRegistry, MatIconModule } from '@angular/material/icon'; 2import { MatIconRegistry, MatIconModule } from '@angular/material/icon';
3import { DomSanitizer } from "@angular/platform-browser"; 3import { DomSanitizer } from "@angular/platform-browser";
4import { BehaviorSubject, interval, Subscription, catchError, throwError } from 'rxjs';
5import { HttpClient, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angular/common/http';
6
7import { v4 as uuidv4 } from 'uuid';
8import jwtDecode, { JwtPayload } from "jwt-decode";
9
10import { map, withLatestFrom } from 'rxjs/operators';
11
12export interface LoadingSpmMail {
13 state: "loading";
14}
15
16export interface LoadedSpmMail {
17 state: "loaded" | "claiming";
18 local: string;
19 domain: string;
20 jwt: string;
21 start?: Date;
22 expiration?: Date;
23 percent_expiration?: number;
24}
25
26export interface ExpiredSpmMail {
27 state: "expired";
28 local: string;
29 domain: string;
30}
31
32export interface ClaimedSpmMail {
33 state: "claimed";
34 local: string;
35 domain: string;
36}
37
38export interface SpmJwtUnregisteredPayload {
39 local: string;
40}
41
42type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload;
43
44export type SpmMail = LoadingSpmMail | LoadedSpmMail | ClaimedSpmMail | ExpiredSpmMail;
45
46function percentExpiration(start: Date, exp: Date, now?: Date) {
47 if (!now) {
48 now = new Date();
49 }
50
51 if (now < start) {
52 return 100;
53 } else if (exp < now) {
54 return 0;
55 } else {
56 const length = exp.getTime() - start.getTime();
57 const diff = exp.getTime() - now.getTime();
58 return 100 * (diff / length);
59 }
60}
61
62function updateSpmExpiration(value: SpmMail): SpmMail {
63 if (value.state === 'loaded') {
64 const percent_expiration = value.expiration && value.start ? percentExpiration(value.start, value.expiration) : undefined;
65 if (percent_expiration !== undefined && percent_expiration <= 0) {
66 return {state: "expired", local: value.local, domain: value.domain};
67 } else {
68 return {...value, percent_expiration: percent_expiration}
69 }
70 } else {
71 return value;
72 }
73}
4 74
5@Component({ 75@Component({
6 selector: 'app-spm', 76 selector: 'app-spm',
7 templateUrl: './spm.component.html', 77 templateUrl: './spm.component.html',
8 styleUrls: ['./spm.component.sass'] 78 styleUrls: ['./spm.component.sass']
9}) 79})
10export class SpmComponent implements OnInit { 80export class SpmComponent implements OnInit, OnDestroy {
81 spmMails$: BehaviorSubject<Map<string, SpmMail>> = new BehaviorSubject(new Map());
82 http: HttpClient;
83
84 intervalSubscription?: Subscription;
11 85
12 constructor(private matIconRegistry: MatIconRegistry, 86 constructor(private matIconRegistry: MatIconRegistry,
13 private domSanitizer: DomSanitizer 87 private domSanitizer: DomSanitizer,
88 http: HttpClient,
14 ) { 89 ) {
15 this.matIconRegistry.addSvgIcon( 90 this.matIconRegistry.addSvgIcon(
16 `thumb_up`, 91 `add`,
17 this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/thumb_up/baseline.svg`) 92 this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/add/baseline.svg`)
93 );
94 this.matIconRegistry.addSvgIcon(
95 `check`,
96 this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/check/baseline.svg`)
97 );
98 this.matIconRegistry.addSvgIcon(
99 `delete_forever`,
100 this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/delete_forever/baseline.svg`)
18 ); 101 );
102
103 this.http = http;
104 }
105
106 add() {
107 const ident = uuidv4();
108 const curr: Map<string, SpmMail> = this.spmMails$.getValue();
109 curr.set(ident, { state: 'loading' });
110 this.spmMails$.next(curr);
111 this.http.get('/spm/generate', { headers: new HttpHeaders({ 'Accept': 'text/plain' }), observe: 'body' as const, responseType: 'text' as const }).subscribe(payload => this.load(ident, payload))
112 }
113
114 load(k: string, encoded: string) {
115 const payload = jwtDecode<SpmJwtPayload>(encoded);
116 if (typeof payload.aud === 'string') {
117 const curr: Map<string, SpmMail> = this.spmMails$.getValue();
118 curr.set(k, {
119 state: 'loaded',
120 local: payload.local,
121 domain: payload.aud,
122 jwt: encoded,
123 expiration: payload.exp ? new Date(1000 * payload.exp) : undefined,
124 start: payload.nbf ? new Date(1000 * payload.nbf) : undefined,
125 percent_expiration: payload.exp && payload.nbf ? percentExpiration(new Date(1000 * payload.nbf), new Date(1000 * payload.exp)) : undefined
126 });
127 this.spmMails$.next(curr);
128 }
129 }
130
131 handleClaimError(k: string, error: HttpErrorResponse) {
132 this.setState(k, 'loaded');
133
134 return throwError(() => new Error('Claiming failed.'));
135 }
136
137 claim(k: string) {
138 const val = this.spmMails$.getValue().get(k);
139 if (!val || val.state !== 'loaded')
140 return;
141
142 this.setState(k, 'claiming');
143
144 this.http.post('/spm/claim', val.jwt, { observe: 'response' as const, headers: new HttpHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }) }).pipe(catchError(error => this.handleClaimError(k, error))).subscribe(response => this.claimed(k, response))
19 } 145 }
20 146
21 ngOnInit(): void { 147 setState(k: string, state: 'loaded' | 'claiming') {
148 const curr: Map<string, SpmMail> = this.spmMails$.getValue();
149 const val = curr.get(k);
150 if (!val || (val.state !== 'loaded' && val.state !== 'claiming'))
151 return;
152
153 curr.set(k, {...val, state: state});
154 this.spmMails$.next(curr);
155 }
156
157 claimed(k: string, response: HttpResponse<any>) {
158 if (response.status !== 204) {
159 this.setState(k, 'loaded');
160 return;
161 }
162
163 const curr: Map<string, SpmMail> = this.spmMails$.getValue();
164 const val = curr.get(k);
165 if (val && (val.state === 'claiming' || val.state === 'loaded')) {
166 curr.set(k, {state: "claimed", local: val.local, domain: val.domain});
167 }
168 this.spmMails$.next(curr);
22 } 169 }
23 170
171 forget(k: string) {
172 const curr: Map<string, SpmMail> = this.spmMails$.getValue();
173 curr.delete(k);
174 this.spmMails$.next(curr);
175 }
176
177 stepSpmMails(curr: Map<string, SpmMail>) {
178 curr.forEach((value, key) => curr.set(key, updateSpmExpiration(value)));
179 this.spmMails$.next(curr);
180 }
181
182 ngOnInit() {
183 this.intervalSubscription = interval(1000).pipe(withLatestFrom(this.spmMails$)).subscribe(([_, mails]: [number, Map<string, SpmMail>]) => this.stepSpmMails(mails));
184 }
185
186 ngOnDestroy() {
187 this.intervalSubscription?.unsubscribe();
188 }
189
190 asIsOrder(a: any, b: any) {
191 return -1;
192 }
24} 193}
diff --git a/overlays/spm/frontend/src/app/spm/spm.module.ts b/overlays/spm/frontend/src/app/spm/spm.module.ts
new file mode 100644
index 00000000..f51e6e98
--- /dev/null
+++ b/overlays/spm/frontend/src/app/spm/spm.module.ts
@@ -0,0 +1,31 @@
1import { NgModule } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { SpmComponent } from './spm.component';
4import { RouterModule } from '@angular/router';
5
6import { MatIconModule } from '@angular/material/icon';
7import { MatButtonModule } from '@angular/material/button';
8import { MatCardModule } from '@angular/material/card';
9import { FlexLayoutModule } from "@angular/flex-layout";
10import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
11import { MatProgressBarModule } from '@angular/material/progress-bar';
12
13@NgModule({
14 declarations: [SpmComponent],
15 exports: [SpmComponent],
16 imports: [
17 RouterModule.forChild([{
18 path: '',
19 pathMatch: 'full',
20 component: SpmComponent
21 }]),
22 MatIconModule,
23 MatButtonModule,
24 MatCardModule,
25 CommonModule,
26 FlexLayoutModule,
27 MatProgressSpinnerModule,
28 MatProgressBarModule,
29 ]
30})
31export class SpmModule {}
diff --git a/overlays/spm/frontend/src/custom-theme.scss b/overlays/spm/frontend/src/custom-theme.scss
index 5c2d48aa..36369bd5 100644
--- a/overlays/spm/frontend/src/custom-theme.scss
+++ b/overlays/spm/frontend/src/custom-theme.scss
@@ -49,3 +49,5 @@ $spm-frontend-dark-theme: mat.define-dark-theme((
49 49
50html, body { height: 100%; } 50html, body { height: 100%; }
51body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 51body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
52
53.mono { font-family: "Source Sans Pro", monospace; }
diff --git a/overlays/spm/frontend/src/styles.sass b/overlays/spm/frontend/src/styles.sass
index 4f2cb82a..4ffe0b36 100644
--- a/overlays/spm/frontend/src/styles.sass
+++ b/overlays/spm/frontend/src/styles.sass
@@ -1,2 +1,3 @@
1/* You can add global styles to this file, and also import other style files */ 1/* You can add global styles to this file, and also import other style files */
2@import @fontsource/roboto 2@import @fontsource/roboto
3@import @fontsource/source-sans-pro
diff --git a/overlays/spm/frontend/yarn-project.nix b/overlays/spm/frontend/yarn-project.nix
index a070e3f2..07017812 100644
--- a/overlays/spm/frontend/yarn-project.nix
+++ b/overlays/spm/frontend/yarn-project.nix
@@ -120,17 +120,21 @@ let
120 120
121cacheEntries = { 121cacheEntries = {
122"@fontsource/roboto@npm:4.5.7" = { filename = "@fontsource-roboto-npm-4.5.7-315fd54028-3f9afc7f4c.zip"; sha512 = "3f9afc7f4c77e517e804e65823db5aea2513e8352fbc26cf7ea8b8b11e7dc6eff48d7a2855ed71e3a6b727446b2bf137629e2389fe3d676be45d95dd9992b558"; }; 122"@fontsource/roboto@npm:4.5.7" = { filename = "@fontsource-roboto-npm-4.5.7-315fd54028-3f9afc7f4c.zip"; sha512 = "3f9afc7f4c77e517e804e65823db5aea2513e8352fbc26cf7ea8b8b11e7dc6eff48d7a2855ed71e3a6b727446b2bf137629e2389fe3d676be45d95dd9992b558"; };
123"@fontsource/source-sans-pro@npm:4.5.10" = { filename = "@fontsource-source-sans-pro-npm-4.5.10-520cde2259-d0b786cdff.zip"; sha512 = "d0b786cdff56e2b9f0dbb557473342ac95bc2021eea57097a29244936554a1f452b1ba5b053f11f2f3bcf58ba77823ac1e2a3ff8aba8c4618026a131a025a86d"; };
123"@material-icons/svg@npm:1.0.28" = { filename = "@material-icons-svg-npm-1.0.28-205466118b-c40b15422b.zip"; sha512 = "c40b15422b0d74ef9bb6ac14a8e7669a435d12bfcc8ab2fb7c986a95a37dad1abaac6e98d52b195b7643f982f7e5f21413c5c730ba5e3c810f0c04f1da068af7"; }; 124"@material-icons/svg@npm:1.0.28" = { filename = "@material-icons-svg-npm-1.0.28-205466118b-c40b15422b.zip"; sha512 = "c40b15422b0d74ef9bb6ac14a8e7669a435d12bfcc8ab2fb7c986a95a37dad1abaac6e98d52b195b7643f982f7e5f21413c5c730ba5e3c810f0c04f1da068af7"; };
124"@types/jasmine@npm:3.10.6" = { filename = "@types-jasmine-npm-3.10.6-bc64f97df3-dff2c26a9e.zip"; sha512 = "dff2c26a9ecbc8198d2f5bf1860275b0b323c80db772c2417d7217afa28bd05fee4b98ab3673dcb18d859f2cd0e084b0dcbd33629fcbe6945b08a72f8e5c36d2"; }; 125"@types/jasmine@npm:3.10.6" = { filename = "@types-jasmine-npm-3.10.6-bc64f97df3-dff2c26a9e.zip"; sha512 = "dff2c26a9ecbc8198d2f5bf1860275b0b323c80db772c2417d7217afa28bd05fee4b98ab3673dcb18d859f2cd0e084b0dcbd33629fcbe6945b08a72f8e5c36d2"; };
125"@types/node@npm:12.20.52" = { filename = "@types-node-npm-12.20.52-be1a44f9b8-b720ee939a.zip"; sha512 = "b720ee939a43f1427baf1606deee97e50892ff482bf3d64ab8a1eda3a50802a80866a232aeab337621f6ea4fdc4515147d7829337653e9e99d97c3aaeec3615d"; }; 126"@types/node@npm:12.20.52" = { filename = "@types-node-npm-12.20.52-be1a44f9b8-b720ee939a.zip"; sha512 = "b720ee939a43f1427baf1606deee97e50892ff482bf3d64ab8a1eda3a50802a80866a232aeab337621f6ea4fdc4515147d7829337653e9e99d97c3aaeec3615d"; };
127"@types/uuid@npm:8.3.4" = { filename = "@types-uuid-npm-8.3.4-7547f4402c-6f11f3ff70.zip"; sha512 = "6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f"; };
126"jasmine-core@npm:4.0.1" = { filename = "jasmine-core-npm-4.0.1-e72087e57c-f097bf2c68.zip"; sha512 = "f097bf2c681acb55e03fb56a1569a9dfa8fcdf7c291aeb8786cb693c3a803ba2a59d6bac4c1facd239d778cc6f643d0a78920d9d278cba5d0cd46779b2546822"; }; 128"jasmine-core@npm:4.0.1" = { filename = "jasmine-core-npm-4.0.1-e72087e57c-f097bf2c68.zip"; sha512 = "f097bf2c681acb55e03fb56a1569a9dfa8fcdf7c291aeb8786cb693c3a803ba2a59d6bac4c1facd239d778cc6f643d0a78920d9d278cba5d0cd46779b2546822"; };
127"karma-jasmine-html-reporter@npm:1.7.0" = { filename = "karma-jasmine-html-reporter-npm-1.7.0-87aa5c7cce-926c25858e.zip"; sha512 = "926c25858ea45115496524749881837de0091e3f8119e0f81a18c377e69fb82f832836b94e08d2a24fbc49aa21b491ae0bb861f07404f144c92bc84f409ef202"; }; 129"karma-jasmine-html-reporter@npm:1.7.0" = { filename = "karma-jasmine-html-reporter-npm-1.7.0-87aa5c7cce-926c25858e.zip"; sha512 = "926c25858ea45115496524749881837de0091e3f8119e0f81a18c377e69fb82f832836b94e08d2a24fbc49aa21b491ae0bb861f07404f144c92bc84f409ef202"; };
128"tslib@npm:2.4.0" = { filename = "tslib-npm-2.4.0-9cb6dc5030-8c4aa6a3c5.zip"; sha512 = "8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113"; }; 130"tslib@npm:2.4.0" = { filename = "tslib-npm-2.4.0-9cb6dc5030-8c4aa6a3c5.zip"; sha512 = "8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113"; };
131"uuid@npm:8.3.2" = { filename = "uuid-npm-8.3.2-eca0baba53-5575a8a75c.zip"; sha512 = "5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df"; };
129"typescript@npm:4.6.4" = { filename = "typescript-npm-4.6.4-114dfa5f7e-e7bfcc39cd.zip"; sha512 = "e7bfcc39cd4571a63a54e5ea21f16b8445268b9900bf55aee0e02ad981be576acc140eba24f1af5e3c1457767c96cea6d12861768fb386cf3ffb34013718631a"; }; 132"typescript@npm:4.6.4" = { filename = "typescript-npm-4.6.4-114dfa5f7e-e7bfcc39cd.zip"; sha512 = "e7bfcc39cd4571a63a54e5ea21f16b8445268b9900bf55aee0e02ad981be576acc140eba24f1af5e3c1457767c96cea6d12861768fb386cf3ffb34013718631a"; };
130"@angular/animations@npm:13.3.9" = { filename = "@angular-animations-npm-13.3.9-e1d514c5f5-4c77f5678a.zip"; sha512 = "4c77f5678acc88cc6cba6e76c498bc0951dbcba2f985eded4972f830857def10918551a805882017655df8dbc8c39821ee8a5ca295d0abf7d82746799aa7f212"; }; 133"@angular/animations@npm:13.3.9" = { filename = "@angular-animations-npm-13.3.9-e1d514c5f5-4c77f5678a.zip"; sha512 = "4c77f5678acc88cc6cba6e76c498bc0951dbcba2f985eded4972f830857def10918551a805882017655df8dbc8c39821ee8a5ca295d0abf7d82746799aa7f212"; };
131"@angular/common@npm:13.3.9" = { filename = "@angular-common-npm-13.3.9-9a7d731ff5-984af09c8a.zip"; sha512 = "984af09c8af87d78a85f743e6923c047c032489a97dd93b8d10702e7556cb842b63a2c836c339f418fe73cc0cd0ce50e262a27a5c721cd8bec35279376625219"; }; 134"@angular/common@npm:13.3.9" = { filename = "@angular-common-npm-13.3.9-9a7d731ff5-984af09c8a.zip"; sha512 = "984af09c8af87d78a85f743e6923c047c032489a97dd93b8d10702e7556cb842b63a2c836c339f418fe73cc0cd0ce50e262a27a5c721cd8bec35279376625219"; };
132"@angular/compiler@npm:13.3.9" = { filename = "@angular-compiler-npm-13.3.9-06a00a359f-4e80398d61.zip"; sha512 = "4e80398d6121b70961ec3a7565ba897dcd63788b78c4011817fd7d3114a398f29c79f5ca985bffcc1e7e11a57dc89054e5cfef410720aacb9b9368647f452798"; }; 135"@angular/compiler@npm:13.3.9" = { filename = "@angular-compiler-npm-13.3.9-06a00a359f-4e80398d61.zip"; sha512 = "4e80398d6121b70961ec3a7565ba897dcd63788b78c4011817fd7d3114a398f29c79f5ca985bffcc1e7e11a57dc89054e5cfef410720aacb9b9368647f452798"; };
133"@angular/core@npm:13.3.9" = { filename = "@angular-core-npm-13.3.9-8bfa1070cd-4b4ce78de4.zip"; sha512 = "4b4ce78de4aadfb6f5e2d8417157d60337052c02608137e5a6548d8e6a8ef9bb4edd29187a2b8ea2720527c20731797b5d0d97ab800e580ccc5e5b609caf80c5"; }; 136"@angular/core@npm:13.3.9" = { filename = "@angular-core-npm-13.3.9-8bfa1070cd-4b4ce78de4.zip"; sha512 = "4b4ce78de4aadfb6f5e2d8417157d60337052c02608137e5a6548d8e6a8ef9bb4edd29187a2b8ea2720527c20731797b5d0d97ab800e580ccc5e5b609caf80c5"; };
137"@angular/flex-layout@npm:13.0.0-beta.38" = { filename = "@angular-flex-layout-npm-13.0.0-beta.38-a6255273fa-c5f22d6748.zip"; sha512 = "c5f22d6748ad8dc2ebc9fa57c82d80cb9f9942d66dfb0246315d351872a2c9df5fa95d849fb0d2750ca99812d5e27d073b7842e07e6f21e541cbe337d9f4e7ed"; };
134"@angular/forms@npm:13.3.9" = { filename = "@angular-forms-npm-13.3.9-bd7c125354-7d95a7c482.zip"; sha512 = "7d95a7c4828f84be28882320ce4ea685b7990bb0dd920417ce3285d4ad61231b546c27e75b8fc90c4a24629c1cb5ae7692af59f2d9992887672ee3461027683d"; }; 138"@angular/forms@npm:13.3.9" = { filename = "@angular-forms-npm-13.3.9-bd7c125354-7d95a7c482.zip"; sha512 = "7d95a7c4828f84be28882320ce4ea685b7990bb0dd920417ce3285d4ad61231b546c27e75b8fc90c4a24629c1cb5ae7692af59f2d9992887672ee3461027683d"; };
135"@angular/material@npm:13.3.7" = { filename = "@angular-material-npm-13.3.7-5b8dc87806-a98ea8117e.zip"; sha512 = "a98ea8117e381412d77f972247f56b87542c01e97964ef6b6d369df5c0a9bc3d247886ad22b235a4e5a471f20e1724297036c9acfcdd2d03edaa4bedd8f7bbf8"; }; 139"@angular/material@npm:13.3.7" = { filename = "@angular-material-npm-13.3.7-5b8dc87806-a98ea8117e.zip"; sha512 = "a98ea8117e381412d77f972247f56b87542c01e97964ef6b6d369df5c0a9bc3d247886ad22b235a4e5a471f20e1724297036c9acfcdd2d03edaa4bedd8f7bbf8"; };
136"@angular/platform-browser-dynamic@npm:13.3.9" = { filename = "@angular-platform-browser-dynamic-npm-13.3.9-3414000d2b-34967ed4fd.zip"; sha512 = "34967ed4fd6b81bacdef17432334e6c99656936d0f4625d8fae854f0984868e6ce0b5c497fb028c68971d09eaf85080ce201ee023c803c1126970a2a831528cf"; }; 140"@angular/platform-browser-dynamic@npm:13.3.9" = { filename = "@angular-platform-browser-dynamic-npm-13.3.9-3414000d2b-34967ed4fd.zip"; sha512 = "34967ed4fd6b81bacdef17432334e6c99656936d0f4625d8fae854f0984868e6ce0b5c497fb028c68971d09eaf85080ce201ee023c803c1126970a2a831528cf"; };
@@ -138,9 +142,11 @@ cacheEntries = {
138"@angular/router@npm:13.3.9" = { filename = "@angular-router-npm-13.3.9-c3b3c739a8-e675ff9a3e.zip"; sha512 = "e675ff9a3e33c92041c897601d633372a3cfaf502d4779b01469f1f26798d0bdf9b2b411082fe64d997bda0c75f7f12fbb32013286447b1398bc70565df028cb"; }; 142"@angular/router@npm:13.3.9" = { filename = "@angular-router-npm-13.3.9-c3b3c739a8-e675ff9a3e.zip"; sha512 = "e675ff9a3e33c92041c897601d633372a3cfaf502d4779b01469f1f26798d0bdf9b2b411082fe64d997bda0c75f7f12fbb32013286447b1398bc70565df028cb"; };
139"karma-chrome-launcher@npm:3.1.1" = { filename = "karma-chrome-launcher-npm-3.1.1-871ed2dfa7-8442219105.zip"; sha512 = "8442219105e1f11a9284fd47f2e21e34720f7e725f25ea08f7525a7ec2088e2c1b65e2def4d7780139d296afc5c30bf4e1d4a839a097eb814031c2f6b379b39f"; }; 143"karma-chrome-launcher@npm:3.1.1" = { filename = "karma-chrome-launcher-npm-3.1.1-871ed2dfa7-8442219105.zip"; sha512 = "8442219105e1f11a9284fd47f2e21e34720f7e725f25ea08f7525a7ec2088e2c1b65e2def4d7780139d296afc5c30bf4e1d4a839a097eb814031c2f6b379b39f"; };
140"karma-jasmine@npm:4.0.2" = { filename = "karma-jasmine-npm-4.0.2-8647c711ec-bf884704af.zip"; sha512 = "bf884704af1fd19816d9f4e96b25e286ff1a57adcabe1f15e3d2b3e9c1da873c1c843b9eab4274c27e63a99f1c3dea864f1f5eca1a10dc065e6e9d5796c207b4"; }; 144"karma-jasmine@npm:4.0.2" = { filename = "karma-jasmine-npm-4.0.2-8647c711ec-bf884704af.zip"; sha512 = "bf884704af1fd19816d9f4e96b25e286ff1a57adcabe1f15e3d2b3e9c1da873c1c843b9eab4274c27e63a99f1c3dea864f1f5eca1a10dc065e6e9d5796c207b4"; };
145"ngx-webstorage@npm:9.0.0" = { filename = "ngx-webstorage-npm-9.0.0-64bf01f819-988ecf069d.zip"; sha512 = "988ecf069d3bdf2c8108cc3e011410eb2e951b6b7932ceddcd5bb1141d42cabd62be97e2d248dd495d5ce4917934ef866a431a0afecd8d90de1040978a933b82"; };
141"rxjs@npm:7.5.5" = { filename = "rxjs-npm-7.5.5-d0546b1ccb-e034f60805.zip"; sha512 = "e034f60805210cce756dd2f49664a8108780b117cf5d0e2281506e9e6387f7b4f1532d974a8c8b09314fa7a16dd2f6cff3462072a5789672b5dcb45c4173f3c6"; }; 146"rxjs@npm:7.5.5" = { filename = "rxjs-npm-7.5.5-d0546b1ccb-e034f60805.zip"; sha512 = "e034f60805210cce756dd2f49664a8108780b117cf5d0e2281506e9e6387f7b4f1532d974a8c8b09314fa7a16dd2f6cff3462072a5789672b5dcb45c4173f3c6"; };
142"zone.js@npm:0.11.5" = { filename = "zone.js-npm-0.11.5-78b46cd237-7dba3af83c.zip"; sha512 = "7dba3af83cc68e881e8a5cc549e4a7bab5f32c5289838f14eb251871816210a39ed8bcd89030593b56d469b4aa8271d644e71ad408fcadd4ccd61b8c7203f2ef"; }; 147"zone.js@npm:0.11.5" = { filename = "zone.js-npm-0.11.5-78b46cd237-7dba3af83c.zip"; sha512 = "7dba3af83cc68e881e8a5cc549e4a7bab5f32c5289838f14eb251871816210a39ed8bcd89030593b56d469b4aa8271d644e71ad408fcadd4ccd61b8c7203f2ef"; };
143"@angular/cdk@npm:13.3.7" = { filename = "@angular-cdk-npm-13.3.7-58a23cc0a3-a2944da577.zip"; sha512 = "a2944da5774a67956fe48762748b2e24dbd58be760652aca5c22fcfa67ad3e01635f6890c689596fe1525d6dbb2a7bd46ed616bdc4d9ee05db64d849b410cbf9"; }; 148"@angular/cdk@npm:13.3.7" = { filename = "@angular-cdk-npm-13.3.7-58a23cc0a3-a2944da577.zip"; sha512 = "a2944da5774a67956fe48762748b2e24dbd58be760652aca5c22fcfa67ad3e01635f6890c689596fe1525d6dbb2a7bd46ed616bdc4d9ee05db64d849b410cbf9"; };
149"jwt-decode@npm:3.1.2" = { filename = "jwt-decode-npm-3.1.2-bf3ab26591-20a4b072d4.zip"; sha512 = "20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb"; };
144"jasmine-core@npm:3.99.1" = { filename = "jasmine-core-npm-3.99.1-07f52d92cc-4e4a89739d.zip"; sha512 = "4e4a89739d99e471b86c7ccc4c5c244a77cc6d1e17b2b0d87d81266b8415697354d8873f7e764790a10661744f73a753a6e9bcd9b3e48c66a0c9b8a092b071b7"; }; 150"jasmine-core@npm:3.99.1" = { filename = "jasmine-core-npm-3.99.1-07f52d92cc-4e4a89739d.zip"; sha512 = "4e4a89739d99e471b86c7ccc4c5c244a77cc6d1e17b2b0d87d81266b8415697354d8873f7e764790a10661744f73a753a6e9bcd9b3e48c66a0c9b8a092b071b7"; };
145"parse5@npm:5.1.1" = { filename = "parse5-npm-5.1.1-8e63d82cff-613a714af4.zip"; sha512 = "613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65"; }; 151"parse5@npm:5.1.1" = { filename = "parse5-npm-5.1.1-8e63d82cff-613a714af4.zip"; sha512 = "613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65"; };
146"which@npm:1.3.1" = { filename = "which-npm-1.3.1-f0ebb8bdd8-f2e185c624.zip"; sha512 = "f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04"; }; 152"which@npm:1.3.1" = { filename = "which-npm-1.3.1-f0ebb8bdd8-f2e185c624.zip"; sha512 = "f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04"; };
@@ -182,7 +188,6 @@ cacheEntries = {
182"ini@npm:2.0.0" = { filename = "ini-npm-2.0.0-28f7426761-e7aadc5fb2.zip"; sha512 = "e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e"; }; 188"ini@npm:2.0.0" = { filename = "ini-npm-2.0.0-28f7426761-e7aadc5fb2.zip"; sha512 = "e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e"; };
183"jsonc-parser@npm:3.0.0" = { filename = "jsonc-parser-npm-3.0.0-66e692e88a-1df2326f1f.zip"; sha512 = "1df2326f1f9688de30c70ff19c5b2a83ba3b89a1036160da79821d1361090775e9db502dc57a67c11b56e1186fc1ed70b887f25c5febf9a3ec4f91435836c99d"; }; 189"jsonc-parser@npm:3.0.0" = { filename = "jsonc-parser-npm-3.0.0-66e692e88a-1df2326f1f.zip"; sha512 = "1df2326f1f9688de30c70ff19c5b2a83ba3b89a1036160da79821d1361090775e9db502dc57a67c11b56e1186fc1ed70b887f25c5febf9a3ec4f91435836c99d"; };
184"symbol-observable@npm:4.0.0" = { filename = "symbol-observable-npm-4.0.0-5c36594410-212c7edce6.zip"; sha512 = "212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8"; }; 190"symbol-observable@npm:4.0.0" = { filename = "symbol-observable-npm-4.0.0-5c36594410-212c7edce6.zip"; sha512 = "212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8"; };
185"uuid@npm:8.3.2" = { filename = "uuid-npm-8.3.2-eca0baba53-5575a8a75c.zip"; sha512 = "5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df"; };
186"debug@npm:4.3.3" = { filename = "debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip"; sha512 = "14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16"; }; 191"debug@npm:4.3.3" = { filename = "debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip"; sha512 = "14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16"; };
187"semver@npm:7.3.5" = { filename = "semver-npm-7.3.5-618cf5db6a-5eafe6102b.zip"; sha512 = "5eafe6102bea2a7439897c1856362e31cc348ccf96efd455c8b5bc2c61e6f7e7b8250dc26b8828c1d76a56f818a7ee907a36ae9fb37a599d3d24609207001d60"; }; 192"semver@npm:7.3.5" = { filename = "semver-npm-7.3.5-618cf5db6a-5eafe6102b.zip"; sha512 = "5eafe6102bea2a7439897c1856362e31cc348ccf96efd455c8b5bc2c61e6f7e7b8250dc26b8828c1d76a56f818a7ee907a36ae9fb37a599d3d24609207001d60"; };
188"yallist@npm:4.0.0" = { filename = "yallist-npm-4.0.0-b493d9e907-343617202a.zip"; sha512 = "343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5"; }; 193"yallist@npm:4.0.0" = { filename = "yallist-npm-4.0.0-b493d9e907-343617202a.zip"; sha512 = "343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5"; };
@@ -984,7 +989,6 @@ cacheEntries = {
984"jsesc@npm:0.5.0" = { filename = "jsesc-npm-0.5.0-6827074492-b8b44cbfc9.zip"; sha512 = "b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17"; }; 989"jsesc@npm:0.5.0" = { filename = "jsesc-npm-0.5.0-6827074492-b8b44cbfc9.zip"; sha512 = "b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17"; };
985"unicode-canonical-property-names-ecmascript@npm:2.0.0" = { filename = "unicode-canonical-property-names-ecmascript-npm-2.0.0-d2d8554a14-39be078afd.zip"; sha512 = "39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45"; }; 990"unicode-canonical-property-names-ecmascript@npm:2.0.0" = { filename = "unicode-canonical-property-names-ecmascript-npm-2.0.0-d2d8554a14-39be078afd.zip"; sha512 = "39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45"; };
986"unicode-property-aliases-ecmascript@npm:2.0.0" = { filename = "unicode-property-aliases-ecmascript-npm-2.0.0-1636cb7768-dda4d39128.zip"; sha512 = "dda4d39128cbbede2ac60fbb85493d979ec65913b8a486bf7cb7a375a2346fa48cbf9dc6f1ae23376e7e8e684c2b411434891e151e865a661b40a85407db51d0"; }; 991"unicode-property-aliases-ecmascript@npm:2.0.0" = { filename = "unicode-property-aliases-ecmascript-npm-2.0.0-1636cb7768-dda4d39128.zip"; sha512 = "dda4d39128cbbede2ac60fbb85493d979ec65913b8a486bf7cb7a375a2346fa48cbf9dc6f1ae23376e7e8e684c2b411434891e151e865a661b40a85407db51d0"; };
987"ngx-webstorage@npm:9.0.0" = { filename = "ngx-webstorage-npm-9.0.0-64bf01f819-988ecf069d.zip"; sha512 = "988ecf069d3bdf2c8108cc3e011410eb2e951b6b7932ceddcd5bb1141d42cabd62be97e2d248dd495d5ce4917934ef866a431a0afecd8d90de1040978a933b82"; };
988}; 992};
989 993
990in optionalOverride overrideAttrs project 994in optionalOverride overrideAttrs project
diff --git a/overlays/spm/frontend/yarn.lock b/overlays/spm/frontend/yarn.lock
index 21ace08d..9bd93e91 100644
--- a/overlays/spm/frontend/yarn.lock
+++ b/overlays/spm/frontend/yarn.lock
@@ -283,6 +283,21 @@ __metadata:
283 languageName: node 283 languageName: node
284 linkType: hard 284 linkType: hard
285 285
286"@angular/flex-layout@npm:^13.0.0-beta.38":
287 version: 13.0.0-beta.38
288 resolution: "@angular/flex-layout@npm:13.0.0-beta.38"
289 dependencies:
290 tslib: ^2.3.0
291 peerDependencies:
292 "@angular/cdk": ^13.0.0
293 "@angular/common": ^13.0.0
294 "@angular/core": ^13.0.0
295 "@angular/platform-browser": ^13.0.0
296 rxjs: ^6.5.3 || ^7.4.0
297 checksum: c5f22d6748ad8dc2ebc9fa57c82d80cb9f9942d66dfb0246315d351872a2c9df5fa95d849fb0d2750ca99812d5e27d073b7842e07e6f21e541cbe337d9f4e7ed
298 languageName: node
299 linkType: hard
300
286"@angular/forms@npm:~13.3.0": 301"@angular/forms@npm:~13.3.0":
287 version: 13.3.9 302 version: 13.3.9
288 resolution: "@angular/forms@npm:13.3.9" 303 resolution: "@angular/forms@npm:13.3.9"
@@ -1710,6 +1725,13 @@ __metadata:
1710 languageName: node 1725 languageName: node
1711 linkType: hard 1726 linkType: hard
1712 1727
1728"@fontsource/source-sans-pro@npm:^4.5.10":
1729 version: 4.5.10
1730 resolution: "@fontsource/source-sans-pro@npm:4.5.10"
1731 checksum: d0b786cdff56e2b9f0dbb557473342ac95bc2021eea57097a29244936554a1f452b1ba5b053f11f2f3bcf58ba77823ac1e2a3ff8aba8c4618026a131a025a86d
1732 languageName: node
1733 linkType: hard
1734
1713"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": 1735"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3":
1714 version: 1.1.3 1736 version: 1.1.3
1715 resolution: "@gar/promisify@npm:1.1.3" 1737 resolution: "@gar/promisify@npm:1.1.3"
@@ -2164,6 +2186,13 @@ __metadata:
2164 languageName: node 2186 languageName: node
2165 linkType: hard 2187 linkType: hard
2166 2188
2189"@types/uuid@npm:^8.3.4":
2190 version: 8.3.4
2191 resolution: "@types/uuid@npm:8.3.4"
2192 checksum: 6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f
2193 languageName: node
2194 linkType: hard
2195
2167"@types/ws@npm:^8.2.2": 2196"@types/ws@npm:^8.2.2":
2168 version: 8.5.3 2197 version: 8.5.3
2169 resolution: "@types/ws@npm:8.5.3" 2198 resolution: "@types/ws@npm:8.5.3"
@@ -5457,6 +5486,13 @@ __metadata:
5457 languageName: node 5486 languageName: node
5458 linkType: hard 5487 linkType: hard
5459 5488
5489"jwt-decode@npm:^3.1.2":
5490 version: 3.1.2
5491 resolution: "jwt-decode@npm:3.1.2"
5492 checksum: 20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb
5493 languageName: node
5494 linkType: hard
5495
5460"karma-chrome-launcher@npm:~3.1.0": 5496"karma-chrome-launcher@npm:~3.1.0":
5461 version: 3.1.1 5497 version: 3.1.1
5462 resolution: "karma-chrome-launcher@npm:3.1.1" 5498 resolution: "karma-chrome-launcher@npm:3.1.1"
@@ -8083,16 +8119,20 @@ __metadata:
8083 "@angular/compiler": ~13.3.0 8119 "@angular/compiler": ~13.3.0
8084 "@angular/compiler-cli": ~13.3.0 8120 "@angular/compiler-cli": ~13.3.0
8085 "@angular/core": ~13.3.0 8121 "@angular/core": ~13.3.0
8122 "@angular/flex-layout": ^13.0.0-beta.38
8086 "@angular/forms": ~13.3.0 8123 "@angular/forms": ~13.3.0
8087 "@angular/material": 13.3.7 8124 "@angular/material": 13.3.7
8088 "@angular/platform-browser": ~13.3.0 8125 "@angular/platform-browser": ~13.3.0
8089 "@angular/platform-browser-dynamic": ~13.3.0 8126 "@angular/platform-browser-dynamic": ~13.3.0
8090 "@angular/router": ~13.3.0 8127 "@angular/router": ~13.3.0
8091 "@fontsource/roboto": ^4.5.7 8128 "@fontsource/roboto": ^4.5.7
8129 "@fontsource/source-sans-pro": ^4.5.10
8092 "@material-icons/svg": ^1.0.28 8130 "@material-icons/svg": ^1.0.28
8093 "@types/jasmine": ~3.10.0 8131 "@types/jasmine": ~3.10.0
8094 "@types/node": ^12.11.1 8132 "@types/node": ^12.11.1
8133 "@types/uuid": ^8.3.4
8095 jasmine-core: ~4.0.0 8134 jasmine-core: ~4.0.0
8135 jwt-decode: ^3.1.2
8096 karma: ~6.3.0 8136 karma: ~6.3.0
8097 karma-chrome-launcher: ~3.1.0 8137 karma-chrome-launcher: ~3.1.0
8098 karma-coverage: ~2.1.0 8138 karma-coverage: ~2.1.0
@@ -8102,6 +8142,7 @@ __metadata:
8102 rxjs: ~7.5.0 8142 rxjs: ~7.5.0
8103 tslib: ^2.3.0 8143 tslib: ^2.3.0
8104 typescript: ~4.6.2 8144 typescript: ~4.6.2
8145 uuid: ^8.3.2
8105 zone.js: ~0.11.4 8146 zone.js: ~0.11.4
8106 languageName: unknown 8147 languageName: unknown
8107 linkType: soft 8148 linkType: soft