diff --git a/.npmrc b/.npmrc index 3370dbb..d4c285f 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,2 @@ save-exact=true - @capawesome-team:registry=https://npm.pkg.github.com diff --git a/package-lock.json b/package-lock.json index f7683db..29ce695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@capacitor/splash-screen": "5.0.7", "@capacitor/status-bar": "5.0.7", "@capawesome-team/capacitor-file-opener": "5.0.4", + "@capawesome-team/capacitor-nfc": "5.1.0-dev.7d38c01.1709239536", "@fortawesome/angular-fontawesome": "0.14.1", "@fortawesome/fontawesome-svg-core": "6.5.1", "@fortawesome/free-brands-svg-icons": "6.5.1", @@ -2988,6 +2989,15 @@ "@capacitor/core": "^5.0.0" } }, + "node_modules/@capawesome-team/capacitor-nfc": { + "version": "5.1.0-dev.7d38c01.1709239536", + "resolved": "https://npm.pkg.github.com/download/@capawesome-team/capacitor-nfc/5.1.0-dev.7d38c01.1709239536/0507eaf63ad1ac6bd6649771c8c37d08c1281af0", + "integrity": "sha512-nx5me7cwR5e9YCruGfX8dru2RfuZ/wKfxCZHildEmFETkFR/aRUsJx/RXtGXUTTjNGwqulscD0Sr9k1lgJPrnw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -24822,6 +24832,12 @@ "integrity": "sha512-YpSz4b3Rip63WSsLCZk7crgk15VEUmOxy8s6x/Pwo8vyLzMfkQSeToigD4AI8XV5PMCDO+UHzneyWH4Cs8S30Q==", "requires": {} }, + "@capawesome-team/capacitor-nfc": { + "version": "5.1.0-dev.7d38c01.1709239536", + "resolved": "https://npm.pkg.github.com/download/@capawesome-team/capacitor-nfc/5.1.0-dev.7d38c01.1709239536/0507eaf63ad1ac6bd6649771c8c37d08c1281af0", + "integrity": "sha512-nx5me7cwR5e9YCruGfX8dru2RfuZ/wKfxCZHildEmFETkFR/aRUsJx/RXtGXUTTjNGwqulscD0Sr9k1lgJPrnw==", + "requires": {} + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/package.json b/package.json index e08ec1e..668badb 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@capacitor/splash-screen": "5.0.7", "@capacitor/status-bar": "5.0.7", "@capawesome-team/capacitor-file-opener": "5.0.4", + "@capawesome-team/capacitor-nfc": "5.1.0-dev.7d38c01.1709239536", "@fortawesome/angular-fontawesome": "0.14.1", "@fortawesome/fontawesome-svg-core": "6.5.1", "@fortawesome/free-brands-svg-icons": "6.5.1", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 58d5dd6..1417cab 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,7 @@ +import { registerLocaleData } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; -import { ErrorHandler, NgModule } from '@angular/core'; +import localeDe from '@angular/common/locales/de'; +import { ErrorHandler, LOCALE_ID, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; import { CoreModule, GlobalErrorHandlerService } from '@app/core'; @@ -11,6 +13,8 @@ import { register as registerSwiper } from 'swiper/element/bundle'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +registerLocaleData(localeDe); + registerSwiper(); @NgModule({ @@ -34,6 +38,7 @@ registerSwiper(); provide: ErrorHandler, useClass: GlobalErrorHandlerService, }, + { provide: LOCALE_ID, useValue: 'de-DE' }, ], bootstrap: [AppComponent], }) diff --git a/src/app/core/services/capacitor/capacitor-nfc/capacitor-nfc.service.ts b/src/app/core/services/capacitor/capacitor-nfc/capacitor-nfc.service.ts new file mode 100644 index 0000000..0fc18e2 --- /dev/null +++ b/src/app/core/services/capacitor/capacitor-nfc/capacitor-nfc.service.ts @@ -0,0 +1,111 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Capacitor } from '@capacitor/core'; +import { + ConnectOptions, + Nfc, + NfcTag, + PollingOption, + TransceiveOptions, + TransceiveResult, + WriteOptions, +} from '@capawesome-team/capacitor-nfc'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class CapacitorNfcService { + private readonly scannedTagSubject = new Subject(); + private readonly lastScannedTagSubject = new ReplaySubject(1); + private readonly sessionCanceledSubject = new Subject(); + private readonly sessionErrorSubject = new Subject(); + + constructor(private readonly ngZone: NgZone) { + void Nfc.removeAllListeners().then(() => { + void Nfc.addListener('nfcTagScanned', event => { + this.ngZone.run(() => { + this.scannedTagSubject.next(event.nfcTag); + this.lastScannedTagSubject.next(event.nfcTag); + }); + }); + void Nfc.addListener('scanSessionCanceled', () => { + this.ngZone.run(() => { + this.sessionCanceledSubject.next(); + }); + }); + void Nfc.addListener('scanSessionError', event => { + this.ngZone.run(() => { + this.sessionErrorSubject.next(event.message); + }); + }); + }); + } + + public get scannedTag$(): Observable { + return this.scannedTagSubject.asObservable(); + } + + public get lastScannedTag$(): Observable { + return this.lastScannedTagSubject.asObservable(); + } + + public get sessionCanceled$(): Observable { + return this.sessionCanceledSubject.asObservable(); + } + + public get sessionError$(): Observable { + return this.sessionErrorSubject.asObservable(); + } + + public async startScanSession(): Promise { + await Nfc.startScanSession({ + pollingOptions: [PollingOption.iso14443, PollingOption.iso15693], + }); + } + + public async stopScanSession(): Promise { + await Nfc.stopScanSession(); + } + + public async write(options: WriteOptions): Promise { + await Nfc.write(options); + } + + public async erase(): Promise { + await Nfc.erase(); + } + + public async format(): Promise { + await Nfc.format(); + } + + public transceive(options: TransceiveOptions): Promise { + return Nfc.transceive(options); + } + + public connect(options: ConnectOptions): Promise { + return Nfc.connect(options); + } + + public async close(): Promise { + await Nfc.close(); + } + + public async openSettings(): Promise { + await Nfc.openSettings(); + } + + public async isSupported(): Promise { + const { isSupported } = await Nfc.isSupported(); + return isSupported; + } + + public async isEnabled(): Promise { + const isAndroid = Capacitor.getPlatform() === 'android'; + if (!isAndroid) { + return true; + } + const { isEnabled } = await Nfc.isEnabled(); + return isEnabled; + } +} diff --git a/src/app/core/services/capacitor/index.ts b/src/app/core/services/capacitor/index.ts index fe5fda4..2324299 100644 --- a/src/app/core/services/capacitor/index.ts +++ b/src/app/core/services/capacitor/index.ts @@ -1 +1,2 @@ export * from './capacitor-app/capacitor-app.service'; +export * from './capacitor-nfc/capacitor-nfc.service'; diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index d3d353c..9a73519 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -3,6 +3,8 @@ export * from './capacitor'; export * from './dialog/dialog.service'; export * from './global-error-handler/global-error-handler.service'; export * from './native-http/native-http.service'; +export * from './nfc'; export * from './notification/notification.service'; export * from './storage/storage.service'; +export * from './timeout/timeout.service'; export * from './user/user.service'; diff --git a/src/app/core/services/nfc/index.ts b/src/app/core/services/nfc/index.ts new file mode 100644 index 0000000..85ee196 --- /dev/null +++ b/src/app/core/services/nfc/index.ts @@ -0,0 +1,2 @@ +export * from './nfc-helper/nfc-helper.service'; +export * from './nfc/nfc.service'; diff --git a/src/app/core/services/nfc/nfc-helper/nfc-helper.service.ts b/src/app/core/services/nfc/nfc-helper/nfc-helper.service.ts new file mode 100644 index 0000000..d893382 --- /dev/null +++ b/src/app/core/services/nfc/nfc-helper/nfc-helper.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { NfcUtils } from '@capawesome-team/capacitor-nfc'; + +@Injectable({ + providedIn: 'root', +}) +export class NfcHelperService { + private readonly nfcUtilsInstance = new NfcUtils(); + + constructor() {} + + public convertHexToBytes(hex: string): number[] { + return this.nfcUtilsInstance.convertHexToBytes({ hex }).bytes; + } + + public convertBytesToHex(bytes: number[]): string { + return this.nfcUtilsInstance.convertBytesToHex({ bytes }).hex; + } + + public convertHexToNumber(hex: string): number { + return this.nfcUtilsInstance.convertHexToNumber({ hex }).number; + } +} diff --git a/src/app/core/services/nfc/nfc/nfc.service.ts b/src/app/core/services/nfc/nfc/nfc.service.ts new file mode 100644 index 0000000..fc31140 --- /dev/null +++ b/src/app/core/services/nfc/nfc/nfc.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@angular/core'; +import { NdefMessage, NfcTag, NfcTagTechType } from '@capawesome-team/capacitor-nfc'; +import { Observable } from 'rxjs'; +import { CapacitorNfcService } from '../../capacitor'; + +@Injectable({ + providedIn: 'root', +}) +export class NfcService { + constructor(private readonly capacitorNfcService: CapacitorNfcService) {} + + public get scannedTag$(): Observable { + return this.capacitorNfcService.scannedTag$; + } + + public get lastScannedTag$(): Observable { + return this.capacitorNfcService.lastScannedTag$; + } + + public async startScanSession(): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + await this.capacitorNfcService.startScanSession(); + } + + public async stopScanSession(): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + return; + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + return; + } + await this.capacitorNfcService.stopScanSession(); + } + + public async write(message: NdefMessage): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + await this.capacitorNfcService.write({ + message, + }); + } + + public async erase(): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + await this.capacitorNfcService.erase(); + } + + public async format(): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + await this.capacitorNfcService.format(); + } + + public async transceive(techType: NfcTagTechType, data: number[]): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + const { response } = await this.capacitorNfcService.transceive({ + techType, + data, + }); + return response; + } + + public async connect(techType: NfcTagTechType): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + throw this.createNotSupportedError(); + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + throw this.createNotEnabledError(); + } + await this.capacitorNfcService.connect({ + techType, + }); + } + + public async close(): Promise { + const isSupported = await this.isSupported(); + if (!isSupported) { + return; + } + const isEnabled = await this.isEnabled(); + if (!isEnabled) { + return; + } + await this.capacitorNfcService.close(); + } + + public isSupported(): Promise { + return this.capacitorNfcService.isSupported(); + } + + public isEnabled(): Promise { + return this.capacitorNfcService.isEnabled(); + } + + private createNotSupportedError(): Error { + const message = 'Dein Gerät unterstützt kein NFC.'; + return new Error(message); + } + + private createNotEnabledError(): Error { + const message = 'Bitte aktiviere NFC in den Einstellungen deines Geräts.'; + return new Error(message); + } +} diff --git a/src/app/core/services/timeout/timeout.service.ts b/src/app/core/services/timeout/timeout.service.ts new file mode 100644 index 0000000..d646054 --- /dev/null +++ b/src/app/core/services/timeout/timeout.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class TimeoutService { + public timeout(callback: () => T, timeoutMs: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + const result = callback(); + resolve(result); + }, timeoutMs); + }); + } +} diff --git a/src/app/modules/canteen/canteen.module.ts b/src/app/modules/canteen/canteen.module.ts index f4acc62..ab22d91 100644 --- a/src/app/modules/canteen/canteen.module.ts +++ b/src/app/modules/canteen/canteen.module.ts @@ -1,13 +1,19 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { SharedModule } from '@app/shared'; import { CanteenRoutingModule } from './canteen-routing.module'; -import { CanteenDishCardComponent } from './components'; +import { CanteenCardBalanceModalComponent, CanteenDishCardComponent, CanteenMenuPopoverComponent } from './components'; import { CanteenPage } from './pages'; import { CanteenFoodLabelPipe } from './pipes'; @NgModule({ imports: [CanteenRoutingModule, SharedModule], - declarations: [CanteenPage, CanteenDishCardComponent, CanteenFoodLabelPipe], + declarations: [ + CanteenPage, + CanteenCardBalanceModalComponent, + CanteenDishCardComponent, + CanteenMenuPopoverComponent, + CanteenFoodLabelPipe, + ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CanteenPageModule {} diff --git a/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.html b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.html new file mode 100644 index 0000000..156990b --- /dev/null +++ b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.html @@ -0,0 +1,56 @@ + + + Wohnungsmarkt + + + + + + + + + + + + Guthaben + + + @if (balance()) { + Dein aktuelles Guthaben beträgt: +
+

{{ balance() | currency: "EUR" : "symbol" : "1.2-2" : "de" }}

+
+ } @else { +
+ Mithilfe der NFC-Schnittstelle deines Smartphones kannst du das aktuelle Guthaben deiner Karte auslesen. +
+ } +
+ + + + Guthaben auslesen + + + +
+ + + Karte aufwerten + + + An folgenden Orten kannst du deine Karte aufwerten: +
    +
  • Mensa (EC-Karte / Bargeld)
  • +
  • HFU (EC-Karte)
  • +
  • DHBW VS (EC-Karte)
  • +
+ Mehr Informationen findest du unter www.swfr.de. +
+
+
diff --git a/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.scss b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.scss new file mode 100644 index 0000000..8ad8eee --- /dev/null +++ b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.scss @@ -0,0 +1,5 @@ +.balance { + text-align: center; + color: var(--ion-color-primary); + margin: 12px 0; +} diff --git a/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.spec.ts b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.spec.ts new file mode 100644 index 0000000..3aef39f --- /dev/null +++ b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DialogService, NotificationService } from '@app/core'; +import { createSpyObj } from '@tests/helpers'; +import { createPipeMock } from '@tests/mocks'; +import { SharedTestingModule } from '@tests/modules'; +import { createDialogServiceSpy } from '@tests/spies'; +import { ApartmentModalComponent } from './apartment-modal.component'; + +describe('ApartmentModalComponent', () => { + let component: ApartmentModalComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let dialogServiceSpy: jest.Mocked; + let notificationServiceSpy: jest.Mocked; + + beforeEach(async () => { + dialogServiceSpy = createDialogServiceSpy(); + notificationServiceSpy = createSpyObj('NotificationService', { + showToast: undefined, + }); + + await TestBed.configureTestingModule({ + declarations: [ApartmentModalComponent, createPipeMock({ name: 'apartmentDate' })], + imports: [SharedTestingModule], + providers: [ + { provide: DialogService, useValue: dialogServiceSpy }, + { provide: NotificationService, useValue: notificationServiceSpy }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ApartmentModalComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + afterEach(() => { + element.remove(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.ts b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.ts new file mode 100644 index 0000000..74b9121 --- /dev/null +++ b/src/app/modules/canteen/components/canteen-card-balance-modal/canteen-card-balance-modal.component.ts @@ -0,0 +1,124 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + WritableSignal, + inject, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DialogService, NfcHelperService, NfcService, TimeoutService } from '@app/core'; +import { Capacitor } from '@capacitor/core'; +import { NfcTagTechType } from '@capawesome-team/capacitor-nfc'; +import { Subject, take, takeUntil } from 'rxjs'; + +@Component({ + selector: 'app-canteen-card-balance-modal', + templateUrl: './canteen-card-balance-modal.component.html', + styleUrls: ['./canteen-card-balance-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CanteenCardBalanceModalComponent { + public balance: WritableSignal = signal(undefined); + public isScanSessionActive: WritableSignal = signal(false); + + private readonly destroyRef = inject(DestroyRef); + private readonly cancelSubject = new Subject(); + private readonly cancel$ = this.cancelSubject.asObservable(); + + private activeWriterAlert: HTMLIonAlertElement | undefined; + + constructor( + private readonly dialogService: DialogService, + private readonly nfcService: NfcService, + private readonly nfcHelperService: NfcHelperService, + private readonly changeDetectorRef: ChangeDetectorRef, + private readonly timeoutService: TimeoutService, + ) {} + + public async closeModal(): Promise { + await this.dialogService.dismissModal(); + } + + public async startScanSession(): Promise { + this.isScanSessionActive.set(true); + try { + await this.nfcService.startScanSession(); + } catch (error) { + throw error; + } finally { + /** + * **Workaround** + * + * The error dialog is not consistently presented and you had to interact + * with the page first (e.g. by random click) for the alert to show up. + */ + await this.timeoutService.timeout(() => this.changeDetectorRef.detectChanges(), 1); + this.isScanSessionActive.set(false); + } + await this.showScanSessionAlert(); + this.nfcService.scannedTag$ + .pipe(takeUntil(this.cancel$), take(1), takeUntilDestroyed(this.destroyRef)) + .subscribe(async () => { + try { + const techType = Capacitor.getPlatform() === 'ios' ? NfcTagTechType.Iso7816 : NfcTagTechType.IsoDep; + // 1. Connect to the tag + await this.nfcService.connect(techType); + // 2. Select the application file (see https://github.com/astarub/campus_app/blob/da514b8ea02d369b34a46d69b6a4ddb42922a90b/android/app/src/main/kotlin/de/asta_bochum/campus_app/PopupActivity.kt#L110) + const appBytes = this.nfcHelperService.convertHexToBytes('0x905A0000035F841500'); + await this.nfcService.transceive(techType, appBytes); + // 3. Read the balance (see https://github.com/astarub/campus_app/blob/da514b8ea02d369b34a46d69b6a4ddb42922a90b/android/app/src/main/kotlin/de/asta_bochum/campus_app/PopupActivity.kt#L118) + const balanceBytes = this.nfcHelperService.convertHexToBytes('0x906C0000010100'); + const response = await this.nfcService.transceive(techType, balanceBytes); + // 4. Parse the balance + const balance = this.convertBytesToBalance(response); + this.balance.set(balance); + // 5. Stop the scan session + } finally { + await this.stopScanSession(); + } + }); + } + + private convertBytesToBalance(bytes: number[]): number { + let trimmedBytes = [...bytes]; + trimmedBytes.pop(); + trimmedBytes.pop(); + trimmedBytes.reverse(); + const hex = this.nfcHelperService.convertBytesToHex(trimmedBytes); + const balance = this.nfcHelperService.convertHexToNumber(hex); + return balance; + } + + private async showScanSessionAlert(): Promise { + const isIos = Capacitor.getPlatform() === 'ios'; + if (isIos) { + return; + } + this.activeWriterAlert = await this.dialogService.showAlert({ + header: 'Guthaben auslesen', + message: 'Bitte halte deine Karte zum Auslesen des Guthabens an die Rückseite deines Smartphones.', + backdropDismiss: false, + buttons: [ + { + text: 'Abbrechen', + role: 'cancel', + handler: () => { + void this.stopScanSession(); + }, + }, + ], + }); + } + + private async stopScanSession(): Promise { + this.isScanSessionActive.set(false); + void this.activeWriterAlert?.dismiss(); + this.cancelSubject.next(undefined); + await this.nfcService.close().catch(() => { + // Ignore errors + }); + await this.nfcService.stopScanSession(); + } +} diff --git a/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.html b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.html new file mode 100644 index 0000000..a13d707 --- /dev/null +++ b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.html @@ -0,0 +1,5 @@ + + + Guthaben auslesen + + diff --git a/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.scss b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.spec.ts b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.spec.ts new file mode 100644 index 0000000..7483003 --- /dev/null +++ b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedTestingModule } from '@tests/modules'; +import { CanteenMenuPopoverComponent } from './canteen-menu-popover.component'; + +describe('CanteenMenuPopoverComponent', () => { + let component: CanteenMenuPopoverComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CanteenMenuPopoverComponent], + imports: [SharedTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(CanteenMenuPopoverComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + afterEach(() => { + element.remove(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.ts b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.ts new file mode 100644 index 0000000..8e62f1c --- /dev/null +++ b/src/app/modules/canteen/components/canteen-menu-popover/canteen-menu-popover.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { DialogService } from '@app/core'; +import { CanteenCardBalanceModalComponent } from '../canteen-card-balance-modal/canteen-card-balance-modal.component'; + +@Component({ + selector: 'app-canteen-menu-popover', + templateUrl: './canteen-menu-popover.component.html', + styleUrls: ['./canteen-menu-popover.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CanteenMenuPopoverComponent { + constructor(private readonly dialogService: DialogService) {} + + public async showCanteenCardBalanceModal(): Promise { + await this.dialogService.showModal({ + component: CanteenCardBalanceModalComponent, + }); + } +} diff --git a/src/app/modules/canteen/components/index.ts b/src/app/modules/canteen/components/index.ts index d3ec71f..1d07928 100644 --- a/src/app/modules/canteen/components/index.ts +++ b/src/app/modules/canteen/components/index.ts @@ -1 +1,3 @@ +export * from './canteen-card-balance-modal/canteen-card-balance-modal.component'; export * from './canteen-dish-card/canteen-dish-card.component'; +export * from './canteen-menu-popover/canteen-menu-popover.component'; diff --git a/src/app/modules/canteen/pages/canteen/canteen.page.html b/src/app/modules/canteen/pages/canteen/canteen.page.html index f9c9ccd..741e335 100644 --- a/src/app/modules/canteen/pages/canteen/canteen.page.html +++ b/src/app/modules/canteen/pages/canteen/canteen.page.html @@ -4,6 +4,11 @@ Mensa + + + + + diff --git a/src/app/modules/canteen/pages/canteen/canteen.page.ts b/src/app/modules/canteen/pages/canteen/canteen.page.ts index 810b36e..d784fe6 100644 --- a/src/app/modules/canteen/pages/canteen/canteen.page.ts +++ b/src/app/modules/canteen/pages/canteen/canteen.page.ts @@ -8,6 +8,7 @@ import { StorageKey, StorageService, } from '@app/core'; +import { CanteenMenuPopoverComponent } from '../../components'; @Component({ selector: 'app-canteen', @@ -34,6 +35,13 @@ export class CanteenPage implements OnInit { void this.initMensaPage(); } + public async showMenuPopover(event: Event): Promise { + await this.dialogService.showPopover({ + component: CanteenMenuPopoverComponent, + event: event, + }); + } + public async onSegmentChange(event: CustomEvent): Promise { const index: number = event.detail.value; if (!this.slidesElementRef) {