diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7489167d70a2f51f954e539834215b1b392a498d..fc7f269f857c208f90c296c969cd5925b22bb6a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,8 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - run: cd deploy - - run: npm i - - run: npm run test + - run: | + cd deploy + npm i + npm run test + \ No newline at end of file diff --git a/common/constants.js b/common/constants.js index e2b47dec26803cfc3730f0c00d03837be6826e84..2ba7fc2bb8c3a9ca8dcdf85ba95d770e2dbb9d2b 100644 --- a/common/constants.js +++ b/common/constants.js @@ -96,7 +96,12 @@ RECEPTOR_PR_CAPTION: `For a single tissue sample, an exemplary density distribution for a single receptor from the pial surface to the border between layer VI and the white matter.`, RECEPTOR_AR_CAPTION: `An exemplary density distribution of a single receptor for one laminar cross-section in a single tissue sample.`, - DATA_NOT_READY: `Still fetching data. Please try again in a few moments.` + DATA_NOT_READY: `Still fetching data. Please try again in a few moments.`, + QUICKTOUR_HEADER: `Welcome to ebrains siibra explorer`, + PERMISSION_TO_QUICKTOUR: `Would you like a quick tour?`, + QUICKTOUR_OK: `Start`, + QUICKTOUR_NEXTTIME: `Not now`, + QUICKTOUR_CANCEL: `Dismiss`, } exports.QUICKTOUR_DESC ={ @@ -104,7 +109,7 @@ ATLAS_SELECTOR: `This is the atlas selector. Click here to choose between EBRAINS reference atlases of different species.`, CHIPS: `These "chips" indicate the currently selected parcellation map as well as selected region. Click the chip to see different versions, if any. Click (i) to read more about a selected item. Click (x) to clear a selection.`, SLICE_VIEW: `The planar views allow you to zoom in to full resolution (mouse wheel), pan the view (click+drag), and select oblique sections (shift+click+drag). You can double-click brain regions to select them.`, - PERSPECTIVE_VIEW: `The 3D view gives an overview of the brain with limited resolution. It can be independently rotated. Click the „eye“ icon on the bottom left to toggle pure surface view.`, + PERSPECTIVE_VIEW: `The 3D view gives an overview of the brain with limited resolution. It can be independently rotated. On the 3d view you can find additional settings.`, VIEW_ICONS: `Use these icons in any of the views to maximize it and zoom in/out.`, TOP_MENU: `These icons provide access to plugins, pinned datasets, and user documentation. Use the profile icon to login with your EBRAINS account.`, LAYER_SELECTOR: `This is the atlas layer browser. If an atlas supports multiple template spaces or parcellation maps, you will find them here.`, diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 25a3c4ffea3cfadaf03516675dbfa734cf5c7629..01d2391c3916c8e6eeb1706c711aab6ee4df3825 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -48,6 +48,7 @@ [quick-tour-description-md]="quickTourFinale.descriptionMd" [quick-tour-order]="quickTourFinale.order" [quick-tour-overwrite-arrow]="emptyArrowTmpl" + quick-tour-severity="low" #media="iavMediaQuery"> <!-- prevent default is required so that user do not zoom in on UI or scroll on mobile UI --> <iav-cmp-viewer-container @@ -166,4 +167,4 @@ </ng-template> <ng-template #emptyArrowTmpl> -</ng-template> \ No newline at end of file +</ng-template> diff --git a/src/ui/quickTour/constrants.ts b/src/ui/quickTour/constrants.ts index 1a09f2d060ce55b0033a2855b59bd90567857cb7..38bb2d62b6443ac07caf3f8ff2a3217269f36a35 100644 --- a/src/ui/quickTour/constrants.ts +++ b/src/ui/quickTour/constrants.ts @@ -23,3 +23,15 @@ export interface IQuickTourOverwritePosition { export type TQuickTourPosition = TPosition export const QUICK_TOUR_CMP_INJTKN = new InjectionToken('QUICK_TOUR_CMP_INJTKN') + +export enum EnumQuickTourSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'hight', +} + +export const PERMISSION_DIALOG_ACTIONS = { + START: `start`, + CANCEL: `cancel`, + NOTNOW: `notnow`, +} \ No newline at end of file diff --git a/src/ui/quickTour/module.ts b/src/ui/quickTour/module.ts index b084d4d3f98ba03f836dd56d45288a2beae84f35..458170c31bfa501f71feaad8e8255412001358e7 100644 --- a/src/ui/quickTour/module.ts +++ b/src/ui/quickTour/module.ts @@ -11,6 +11,7 @@ import { ArrowComponent } from "./arrowCmp/arrow.component"; import { WindowResizeModule } from "src/util/windowResize"; import { QUICK_TOUR_CMP_INJTKN } from "./constrants"; import { ComponentsModule } from "src/components"; +import {StartTourDialogDialog} from "src/ui/quickTour/startTourDialog/startTourDialog.component"; @NgModule({ imports: [ @@ -25,6 +26,7 @@ import { ComponentsModule } from "src/components"; QuickTourComponent, QuickTourDirective, ArrowComponent, + StartTourDialogDialog ], exports: [ QuickTourDirective, @@ -40,6 +42,7 @@ import { ComponentsModule } from "src/components"; provide: QUICK_TOUR_CMP_INJTKN, useValue: QuickTourComponent } - ] + ], + entryComponents: [ StartTourDialogDialog ] }) export class QuickTourModule{} diff --git a/src/ui/quickTour/quickTour.service.ts b/src/ui/quickTour/quickTour.service.ts index 78bd0c164431b0b40c119cbce56f6380eaec28a4..3853a19497de7f3494d77c587b1a1e044cf00f5c 100644 --- a/src/ui/quickTour/quickTour.service.ts +++ b/src/ui/quickTour/quickTour.service.ts @@ -4,22 +4,10 @@ import { Overlay, OverlayRef } from "@angular/cdk/overlay"; import { ComponentPortal } from "@angular/cdk/portal"; import { QuickTourThis } from "./quickTourThis.directive"; import { DoublyLinkedList, IDoublyLinkedItem } from 'src/util' -import { QUICK_TOUR_CMP_INJTKN } from "./constrants"; - -export function findInLinkedList<T extends object>(first: IDoublyLinkedItem<T>, predicate: (linkedObj: IDoublyLinkedItem<T>) => boolean): IDoublyLinkedItem<T>{ - let compareObj = first, - returnObj: IDoublyLinkedItem<T> = null - - do { - if (predicate(compareObj)) { - returnObj = compareObj - break - } - compareObj = compareObj.next - } while(!!compareObj) - - return returnObj -} +import { EnumQuickTourSeverity, PERMISSION_DIALOG_ACTIONS, QUICK_TOUR_CMP_INJTKN } from "./constrants"; +import { LOCAL_STORAGE_CONST } from "src/util/constants"; +import { MatDialog, MatDialogRef } from "@angular/material/dialog"; +import { StartTourDialogDialog } from "src/ui/quickTour/startTourDialog/startTourDialog.component"; @Injectable() export class QuickTourService { @@ -34,6 +22,10 @@ export class QuickTourService { public currActiveSlide: IDoublyLinkedItem<QuickTourThis> public slides = new DoublyLinkedList<QuickTourThis>() + private startTourDialogRef: MatDialogRef<any> + + public autoStartTriggered = false + constructor( private overlay: Overlay, /** @@ -42,6 +34,7 @@ export class QuickTourService { * makes sense, since we want to keep the dependency of svc on cmp as loosely (or non existent) as possible */ @Inject(QUICK_TOUR_CMP_INJTKN) private quickTourCmp: any, + private matDialog: MatDialog ){ } @@ -56,12 +49,41 @@ export class QuickTourService { return linkedItem.thisObj.order < dir.order } ) + + + if (dir.quickTourSeverity === EnumQuickTourSeverity.MEDIUM || dir.quickTourSeverity === EnumQuickTourSeverity.HIGH) { + this.autoStart() + } } public unregister(dir: QuickTourThis){ this.slides.remove(dir) } + autoStart() { + + // if already viewed quick tour, return + if (localStorage.getItem(LOCAL_STORAGE_CONST.QUICK_TOUR_VIEWED)){ + return + } + // if auto start already triggered, return + if (this.autoStartTriggered) return + this.autoStartTriggered = true + this.startTourDialogRef = this.matDialog.open(StartTourDialogDialog) + this.startTourDialogRef.afterClosed().subscribe(res => { + switch (res) { + case PERMISSION_DIALOG_ACTIONS.START: + this.startTour() + localStorage.setItem(LOCAL_STORAGE_CONST.QUICK_TOUR_VIEWED, 'true') + break + case PERMISSION_DIALOG_ACTIONS.CANCEL: + localStorage.setItem(LOCAL_STORAGE_CONST.QUICK_TOUR_VIEWED, 'true') + break + } + }) + + } + public startTour() { if (!this.overlayRef) { this.overlayRef = this.overlay.create({ @@ -70,14 +92,14 @@ export class QuickTourService { hasBackdrop: true, backdropClass: ['pe-none', 'cdk-overlay-dark-backdrop'], positionStrategy: this.overlay.position().global(), - }) + }) } - + if (!this.cmpRef) { this.cmpRef = this.overlayRef.attach( new ComponentPortal(this.quickTourCmp) ) - + this.currActiveSlide = this.slides.first this.currentTip$.next(this.currActiveSlide) } @@ -92,15 +114,27 @@ export class QuickTourService { } public nextSlide() { + if (!this.currActiveSlide.next) return this.currActiveSlide = this.currActiveSlide.next this.currentTip$.next(this.currActiveSlide) } public previousSlide() { + if (!this.currActiveSlide.prev) return this.currActiveSlide = this.currActiveSlide.prev this.currentTip$.next(this.currActiveSlide) } + public ff(index: number) { + try { + const slide = this.slides.get(index) + this.currActiveSlide = slide + this.currentTip$.next(slide) + } catch (_e) { + console.warn(`cannot find slide with index ${index}`) + } + } + changeDetected(dir: QuickTourThis) { if (this.currActiveSlide?.thisObj === dir) { this.detectChanges$.next(null) diff --git a/src/ui/quickTour/quickTourComponent/quickTour.component.ts b/src/ui/quickTour/quickTourComponent/quickTour.component.ts index a2bff2c3f86c6eae62e298397d99d6695791f9b5..37c8dbb60827011899b04578f7eb6ddde51d382f 100644 --- a/src/ui/quickTour/quickTourComponent/quickTour.component.ts +++ b/src/ui/quickTour/quickTourComponent/quickTour.component.ts @@ -1,12 +1,12 @@ import { Component, ElementRef, - HostListener, + OnDestroy, SecurityContext, TemplateRef, ViewChild, } from "@angular/core"; -import { combineLatest, Subscription } from "rxjs"; +import { combineLatest, fromEvent, Subscription } from "rxjs"; import { QuickTourService } from "../quickTour.service"; import { debounceTime, map, shareReplay } from "rxjs/operators"; import { DomSanitizer } from "@angular/platform-browser"; @@ -19,7 +19,7 @@ import { clamp } from "src/util/generator"; './quickTour.style.css' ], }) -export class QuickTourComponent { +export class QuickTourComponent implements OnDestroy{ static TourCardMargin = 24 static TourCardWidthPx = 256 @@ -33,13 +33,6 @@ export class QuickTourComponent { public arrowTmpl: TemplateRef<any> public arrowSrc: string - @HostListener('window:keydown', ['$event']) - keydownListener(ev: KeyboardEvent){ - if (ev.key === 'Escape') { - this.quickTourService.endTour() - } - } - public tourCardTransform = `translate(-500px, -500px)` public customArrowTransform = `translate(-500px, -500px)` @@ -126,10 +119,28 @@ export class QuickTourComponent { } this.currTip = linkedObj.thisObj this.calculateTransforms() + }), + + fromEvent(window, 'keydown', { capture: true }).subscribe((ev: KeyboardEvent) => { + if (ev.key === 'Escape') { + this.quickTourService.endTour() + } + if (ev.key === 'ArrowRight') { + this.quickTourService.nextSlide() + ev.stopPropagation() + } + if (ev.key === 'ArrowLeft') { + this.quickTourService.previousSlide() + ev.stopPropagation() + } }) ) } + ngOnDestroy(){ + while (this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() + } + nextSlide(){ this.quickTourService.nextSlide() } @@ -138,6 +149,10 @@ export class QuickTourComponent { this.quickTourService.previousSlide() } + ff(index: number){ + this.quickTourService.ff(index) + } + endTour(){ this.quickTourService.endTour() } diff --git a/src/ui/quickTour/quickTourComponent/quickTour.style.css b/src/ui/quickTour/quickTourComponent/quickTour.style.css index c6d9f7bc0c7df887ee87b0114e6b37c18187f437..cf9d9de0e4d057209c09d6ff5461d5a2844d9708 100644 --- a/src/ui/quickTour/quickTourComponent/quickTour.style.css +++ b/src/ui/quickTour/quickTourComponent/quickTour.style.css @@ -1,16 +1,16 @@ :host { - display: inline-flex; - width: 0px; - height: 0px; - position: relative; + display: inline-flex; + width: 0px; + height: 0px; + position: relative; } mat-card { - position: absolute; - margin: 0; - z-index: 10; + position: absolute; + margin: 0; + z-index: 10; } .custom-svg >>> svg @@ -21,3 +21,13 @@ mat-card stroke-linecap: round; stroke-linejoin: round; } + +.progress-dot +{ + transition: opacity ease-in-out 300ms; +} + +.progress-dot:hover +{ + opacity: 0.8!important; +} \ No newline at end of file diff --git a/src/ui/quickTour/quickTourComponent/quickTour.template.html b/src/ui/quickTour/quickTourComponent/quickTour.template.html index 2bb90559f8e2b8d77115d4b059941f0fa95ab2a8..fe745b9e97874dde047d83598ee3b625cfe8eac8 100644 --- a/src/ui/quickTour/quickTourComponent/quickTour.template.html +++ b/src/ui/quickTour/quickTourComponent/quickTour.template.html @@ -16,12 +16,14 @@ <mat-card-actions> <button mat-icon-button (click)="prevSlide()" - [disabled]="isFirst$ | async"> + [disabled]="isFirst$ | async" + matTooltip="Previous [LEFT ARROW]"> <i class="fas fa-chevron-left"></i> </button> <button mat-icon-button (click)="nextSlide()" - [disabled]="isLast$ | async"> + [disabled]="isLast$ | async" + matTooltip="Next [RIGHT ARROW]"> <i class="fas fa-chevron-right"></i> </button> @@ -29,7 +31,8 @@ <ng-template [ngIf]="isLast$ | async" [ngIfElse]="notLastTmpl"> <button mat-stroked-button color="primary" - (click)="endTour()"> + (click)="endTour()" + matTooltip="Dismiss [ESC]"> <i class="m-1 fas fa-check"></i> <span>complete</span> </button> @@ -37,16 +40,19 @@ <!-- dismiss (not last) --> <ng-template #notLastTmpl> - <button mat-icon-button (click)="endTour()"> + <button mat-icon-button + (click)="endTour()" + matTooltip="Dismiss [ESC]"> <i class="fas fa-times"></i> </button> </ng-template> <!-- progress dots --> <span class="muted d-inline-flex align-items-center"> - <i *ngFor="let active of quickTourProgress$ | async" + <i *ngFor="let active of quickTourProgress$ | async; let index = index" + (click)="ff(index)" [ngClass]="{ 'fa-xs muted-3': !active }" - class="ml-1 fas fa-circle"></i> + class="ml-1 fas fa-circle cursor-pointer progress-dot"></i> </span> </mat-card-actions> </mat-card> diff --git a/src/ui/quickTour/quickTourThis.directive.ts b/src/ui/quickTour/quickTourThis.directive.ts index 1493a3972f7a1874e5ad896eee3d2d2489920c67..1908166452147779e29c35e08d9838f60e1479bc 100644 --- a/src/ui/quickTour/quickTourThis.directive.ts +++ b/src/ui/quickTour/quickTourThis.directive.ts @@ -1,6 +1,7 @@ import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, TemplateRef } from "@angular/core"; import { QuickTourService } from "src/ui/quickTour/quickTour.service"; -import { IQuickTourOverwritePosition, TQuickTourPosition } from "src/ui/quickTour/constrants"; +import { EnumQuickTourSeverity, IQuickTourOverwritePosition, TQuickTourPosition } from "src/ui/quickTour/constrants"; +import {LOCAL_STORAGE_CONST} from "src/util/constants"; @Directive({ selector: '[quick-tour]', @@ -14,7 +15,8 @@ export class QuickTourThis implements OnInit, OnChanges, OnDestroy { @Input('quick-tour-position') position: TQuickTourPosition @Input('quick-tour-overwrite-position') overwritePosition: IQuickTourOverwritePosition @Input('quick-tour-overwrite-arrow') overWriteArrow: TemplateRef<any> | string - + @Input('quick-tour-severity') quickTourSeverity: EnumQuickTourSeverity = EnumQuickTourSeverity.MEDIUM + private attachedTmpl: ElementRef constructor( diff --git a/src/ui/quickTour/startTourDialog/startTourDialog.component.ts b/src/ui/quickTour/startTourDialog/startTourDialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac77e1d479fa84a408d228f51ba6d1d65ddc39d7 --- /dev/null +++ b/src/ui/quickTour/startTourDialog/startTourDialog.component.ts @@ -0,0 +1,14 @@ +import {Component} from "@angular/core"; +import {MatDialogRef} from "@angular/material/dialog"; +import { CONST } from 'common/constants' +import { PERMISSION_DIALOG_ACTIONS } from "../constrants"; + +@Component({ + selector: 'quick-tour-start-dialog', + templateUrl: './startTourDialog.template.html', +}) +export class StartTourDialogDialog { + public CONST = CONST + public PERMISSION_DIALOG_ACTIONS = PERMISSION_DIALOG_ACTIONS + constructor(public dialogRef: MatDialogRef<StartTourDialogDialog>) {} +} diff --git a/src/ui/quickTour/startTourDialog/startTourDialog.template.html b/src/ui/quickTour/startTourDialog/startTourDialog.template.html new file mode 100644 index 0000000000000000000000000000000000000000..2f0db03ffac09e383c69ec1d4a62f60b0de33b72 --- /dev/null +++ b/src/ui/quickTour/startTourDialog/startTourDialog.template.html @@ -0,0 +1,34 @@ +<h3 mat-dialog-title> + {{ CONST.QUICKTOUR_HEADER }} +</h3> + +<div mat-dialog-content> + <span> + {{ CONST.PERMISSION_TO_QUICKTOUR }} + </span> +</div> + +<div mat-dialog-actions class="d-flex"> + <button + mat-raised-button + color="primary" + [mat-dialog-close]="PERMISSION_DIALOG_ACTIONS.START"> + <i class="fas fa-play"></i> + <span> + {{ CONST.QUICKTOUR_OK }} + </span> + </button> + <button + mat-stroked-button + [mat-dialog-close]="PERMISSION_DIALOG_ACTIONS.NOTNOW"> + {{ CONST.QUICKTOUR_NEXTTIME }} + </button> + + <div class="flex-grow-1 flex-shrink-1"></div> + + <button + mat-button + [mat-dialog-close]="PERMISSION_DIALOG_ACTIONS.CANCEL"> + {{ CONST.QUICKTOUR_CANCEL }} + </button> +</div> diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index 96799b36b8c832987b74ce0add621ac97634fde2..cb253e7ecf66bd29d1cb972f7d3f491b1e220f3d 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -45,6 +45,7 @@ quick-tour [quick-tour-description]="quickTourData.description" [quick-tour-order]="quickTourData.order" + quick-tour-severity="low" [iav-key-listener]="keyListenerConfig" (iav-key-event)="openTmplWithDialog(helperOnePager, helperOnePagerConfig)"> diff --git a/src/util/LinkedList.ts b/src/util/LinkedList.ts index 478362128b9212a34bbff8baa8a08404bc82ac84..77e8e21e553ee16d74aebd343ee1c1ff6a81b3d9 100644 --- a/src/util/LinkedList.ts +++ b/src/util/LinkedList.ts @@ -85,6 +85,18 @@ export class DoublyLinkedList<T extends object>{ size(){ return this._size } + + get(idx: number) { + if (idx >= this.size()) throw new Error(`Index out of bound!`) + let i = 0 + let curr = this.first + while (i < idx) { + curr = curr.next + if (!curr) throw new Error(`Index out of bound!`) + i ++ + } + return curr + } } export function FindInLinkedList<T extends object>(list: DoublyLinkedList<T>, predicate: (element: IDoublyLinkedItem<T>) => boolean){ diff --git a/src/util/constants.ts b/src/util/constants.ts index 01f4bc78af5a6fe058e8250300799a10cf46df9e..7a8bb4a12b67fa82447a8d82749dc5d319489c87 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -7,6 +7,7 @@ export const LOCAL_STORAGE_CONST = { MOBILE_UI: 'fzj.xg.iv.MOBILE_UI', AGREE_COOKIE: 'fzj.xg.iv.AGREE_COOKIE', AGREE_KG_TOS: 'fzj.xg.iv.AGREE_KG_TOS', + QUICK_TOUR_VIEWED: 'fzj.dg.iv.QUICK_TOUR_VIEWED', FAV_DATASET: 'fzj.xg.iv.FAV_DATASET_V2', } @@ -41,7 +42,7 @@ export const appendScriptFactory = (document: Document) => { }) } -export const REMOVE_SCRIPT_TOKEN: InjectionToken<(el: HTMLScriptElement) => void> = new InjectionToken(`REMOVE_SCRIPT_TOKEN`) +export const REMOVE_SCRIPT_TOKEN: InjectionToken<(el: HTMLScriptElement) => void> = new InjectionToken(`REMOVE_SCRIPT_TOKEN`) export const removeScriptFactory = (document: Document) => { return (srcEl: HTMLScriptElement) => { @@ -73,10 +74,10 @@ import { EnumColorMapName, mapKeyColorMap } from './colorMaps' import { InjectionToken } from "@angular/core" export const getShader = ({ - colormap = EnumColorMapName.GREYSCALE, + colormap = EnumColorMapName.GREYSCALE, lowThreshold = 0, highThreshold = 1, - brightness = 0, + brightness = 0, contrast = 0, removeBg = false } = {}): string => { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index ee7e954bb5e6a682ffa9863317ea58f4e8c3862a..dc62faf1dba793280564f84c42905d0ceb5c58f5 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -32,7 +32,7 @@ }"> </ng-container> </div> - + <annotating-tools-panel class="z-index-10"> </annotating-tools-panel> </div> @@ -52,7 +52,7 @@ </mat-card> </div> </iav-layout-fourcorners> - + </mat-drawer-content> </mat-drawer-container> @@ -259,7 +259,7 @@ </atlas-layer-selector> <!-- chips --> - <div class="flex-grow-0 p-1 flex-shrink-1 overflow-y-hidden overflow-x-auto pe-all"> + <div *ngIf="parcellationSelected$ | async" class="flex-grow-0 p-1 flex-shrink-1 overflow-y-hidden overflow-x-auto pe-all"> <viewer-state-breadcrumb (on-item-click)="handleChipClick()"> @@ -625,12 +625,12 @@ [ngTemplateOutlet]="tmplRef.tmpl" [ngTemplateOutletContext]="{$implicit: tmplRef.data}"> </ng-template> - + <!-- template not provided --> <ng-template #fallbackTmpl> {{ tmplRef.data.message || 'test' }} </ng-template> - + <mat-divider></mat-divider> </mat-card-content> </mat-card>