From 97e3f235e8fa2d3f08f5ae37914e1d0bc5c204d3 Mon Sep 17 00:00:00 2001 From: Oriol Agost Batalla Date: Tue, 14 Nov 2023 17:55:49 +0100 Subject: [PATCH] feat: add basic refuge detail skeleton --- app/package-lock.json | 38 +++++ app/package.json | 5 +- app/src/app/app.module.ts | 19 ++- .../{admin.guard.ts => supervisor.guard.ts} | 0 app/src/app/pages/login/login.page.html | 2 +- .../refuge-detail/refuge-detail.page.html | 13 ++ .../refuge-detail/refuge-detail.page.scss | 0 .../refuge-detail/refuge-detail.page.spec.ts | 17 ++ .../refuge-detail/refuge-detail.page.ts | 146 ++++++++++++++++++ .../refuges/refuge-list/refuges-list.page.ts | 15 +- .../pages/refuges/refuges-routing.module.ts | 8 +- app/src/app/pages/refuges/refuges.module.ts | 3 +- .../language/device-language.service.spec.ts | 16 ++ .../language/device-language.service.ts | 48 ++++++ app/src/app/services/refuge/refuge.service.ts | 45 +++++- app/src/assets/i18n/en.json | 11 ++ app/src/environments/environment.prod.ts | 4 + app/src/schemas/refuge/get-refuge-schema.ts | 37 +++++ app/src/schemas/refuge/refuge.ts | 5 + 19 files changed, 421 insertions(+), 11 deletions(-) rename app/src/app/guards/{admin.guard.ts => supervisor.guard.ts} (100%) create mode 100644 app/src/app/pages/refuges/refuge-detail/refuge-detail.page.html create mode 100644 app/src/app/pages/refuges/refuge-detail/refuge-detail.page.scss create mode 100644 app/src/app/pages/refuges/refuge-detail/refuge-detail.page.spec.ts create mode 100644 app/src/app/pages/refuges/refuge-detail/refuge-detail.page.ts create mode 100644 app/src/app/services/language/device-language.service.spec.ts create mode 100644 app/src/app/services/language/device-language.service.ts create mode 100644 app/src/assets/i18n/en.json create mode 100644 app/src/environments/environment.prod.ts create mode 100644 app/src/schemas/refuge/get-refuge-schema.ts diff --git a/app/package-lock.json b/app/package-lock.json index 0e07bec..400b116 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -18,11 +18,14 @@ "@angular/router": "^16.0.0", "@capacitor/app": "5.0.6", "@capacitor/core": "5.5.1", + "@capacitor/device": "^5.0.6", "@capacitor/haptics": "5.0.6", "@capacitor/keyboard": "5.0.6", "@capacitor/preferences": "^5.0.6", "@capacitor/status-bar": "5.0.6", "@ionic/angular": "^7.0.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", "@types/uuid": "^9.0.5", "ionicons": "^7.0.0", "rxjs": "~7.8.0", @@ -2689,6 +2692,14 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/device": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/device/-/device-5.0.6.tgz", + "integrity": "sha512-tmjK0H8IKbDLMcmzZzJPbV+9yLkKJ76QOdz4A7fZAOYx2GnFHsFngxldq/wKotGAJuDX/ih3ZzHNrzVguzlv2g==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/haptics": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-5.0.6.tgz", @@ -3679,6 +3690,33 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/app/package.json b/app/package.json index 6616459..e42d876 100644 --- a/app/package.json +++ b/app/package.json @@ -37,7 +37,10 @@ "tslib": "^2.3.0", "zone.js": "~0.13.0", "@capacitor/preferences": "^5.0.6", - "@types/uuid": "^9.0.5" + "@types/uuid": "^9.0.5", + "@capacitor/device": "^5.0.6", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0" }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.0", diff --git a/app/src/app/app.module.ts b/app/src/app/app.module.ts index 7a974cc..d270a8a 100644 --- a/app/src/app/app.module.ts +++ b/app/src/app/app.module.ts @@ -6,8 +6,18 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { + HTTP_INTERCEPTORS, + HttpClient, + HttpClientModule, +} from '@angular/common/http'; import { AuthInterceptor } from './interceptors/auth.interceptor'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} @NgModule({ declarations: [AppComponent], @@ -16,6 +26,13 @@ import { AuthInterceptor } from './interceptors/auth.interceptor'; IonicModule.forRoot(), AppRoutingModule, HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient], + }, + }), ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/app/src/app/guards/admin.guard.ts b/app/src/app/guards/supervisor.guard.ts similarity index 100% rename from app/src/app/guards/admin.guard.ts rename to app/src/app/guards/supervisor.guard.ts diff --git a/app/src/app/pages/login/login.page.html b/app/src/app/pages/login/login.page.html index f553977..b7b73d9 100644 --- a/app/src/app/pages/login/login.page.html +++ b/app/src/app/pages/login/login.page.html @@ -21,7 +21,6 @@ {{errorMessage}} diff --git a/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.html b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.html new file mode 100644 index 0000000..4b3bec0 --- /dev/null +++ b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.html @@ -0,0 +1,13 @@ + + + refuge-detail + + + + + + + refuge-detail + + + diff --git a/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.scss b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.spec.ts b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.spec.ts new file mode 100644 index 0000000..0614b87 --- /dev/null +++ b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.spec.ts @@ -0,0 +1,17 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RefugeDetailPage } from './refuge-detail.page'; + +describe('RefugeDetailPage', () => { + let component: RefugeDetailPage; + let fixture: ComponentFixture; + + beforeEach(async(() => { + fixture = TestBed.createComponent(RefugeDetailPage); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.ts b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.ts new file mode 100644 index 0000000..6f1c61b --- /dev/null +++ b/app/src/app/pages/refuges/refuge-detail/refuge-detail.page.ts @@ -0,0 +1,146 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RefugeService } from '../../../services/refuge/refuge.service'; +import { AlertController, LoadingController } from '@ionic/angular'; +import { TranslateService } from '@ngx-translate/core'; +import { + GetRefugeFromIdErrors, + GetRefugeResponse, +} from '../../../../schemas/refuge/get-refuge-schema'; +import { match } from 'ts-pattern'; +import { Refuge } from '../../../../schemas/refuge/refuge'; + +@Component({ + selector: 'app-refuge-detail', + templateUrl: './refuge-detail.page.html', + styleUrls: ['./refuge-detail.page.scss'], +}) +export class RefugeDetailPage implements OnInit { + refuge?: Refuge; + + constructor( + private router: Router, + private route: ActivatedRoute, + private refugeService: RefugeService, + private alertController: AlertController, + private loadingController: LoadingController, + private translateService: TranslateService, + ) { + const refugeId = this.getRefugeIdFromUrl(); + this.fetchRefuge(refugeId).then(); + } + + ngOnInit() {} + + private async fetchRefuge(refugeId: string | null): Promise { + if (refugeId != null) this.fetchRefugeFromId(refugeId); + else this.router.navigate(['login']).then(); + } + + private getRefugeIdFromUrl(): string | null { + return this.route.snapshot.paramMap.get('id'); + } + + private fetchRefugeFromId(refugeId: string) { + this.refugeService.getRefugeFrom(refugeId).subscribe({ + next: (response: GetRefugeResponse) => + this.handleGetRefugeResponse(response), + error: () => this.handleClientError().then(), + }); + } + + private handleGetRefugeResponse(response: GetRefugeResponse) { + match(response) + .with({ status: 'correct' }, (response) => (this.refuge = response.data)) + .with({ status: 'error' }, (response) => { + this.handleGetError(response.error); + }) + .exhaustive(); + } + + private handleGetError(error: GetRefugeFromIdErrors) { + match(error) + .with(GetRefugeFromIdErrors.NOT_FOUND, () => this.handleNotFoundRefuge()) + .with(GetRefugeFromIdErrors.CLIENT_SEND_DATA_ERROR, () => + this.handleBadUserData(), + ) + .with(GetRefugeFromIdErrors.UNKNOWN_ERROR, () => + this.handleUnknownError(), + ) + .with( + GetRefugeFromIdErrors.SERVER_INCORRECT_DATA_FORMAT_ERROR, + GetRefugeFromIdErrors.PROGRAMMER_SEND_DATA_ERROR, + () => this.handleBadProgrammerData(), + ) + .exhaustive(); + } + + private async handleClientError() { + const alert = await this.alertController.create({ + header: this.translateService.instant('HOME.ERRORS.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant( + 'HOME.ERRORS.CLIENT_ERROR.SUBHEADER', + ), + message: this.translateService.instant( + 'HOME.ERRORS.CLIENT_ERROR.MESSAGE', + ), + buttons: [ + { + text: this.translateService.instant('HOME.ERRORS.CLIENT_ERROR.EXIT'), + handler: () => { + this.alertController.dismiss().then(); + this.fetchRefuge(this.getRefugeIdFromUrl()); + }, + }, + ], + }); + return await alert.present(); + } + + private handleNotFoundRefuge() { + this.finishLoadAnimAndExecute(() => + this.router + .navigate(['not-found'], { + skipLocationChange: true, + }) + .then(), + ).then(); + } + + private handleBadProgrammerData() { + this.finishLoadAnimAndExecute(() => + this.router + .navigate(['programming-error'], { + skipLocationChange: true, + }) + .then(), + ).then(); + } + + private handleBadUserData() { + this.finishLoadAnimAndExecute(() => + this.router + .navigate(['not-found-page'], { + skipLocationChange: true, + }) + .then(), + ).then(); + } + + private handleUnknownError() { + this.finishLoadAnimAndExecute(() => + this.router + .navigate(['internal-error-page'], { + skipLocationChange: true, + }) + .then(), + ).then(); + } + + private async finishLoadAnimAndExecute( + func: (() => void) | (() => Promise), + ) { + await this.loadingController.dismiss().then(); + await func(); + } +} diff --git a/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts b/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts index cf50692..37e9d8e 100644 --- a/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts +++ b/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts @@ -19,6 +19,7 @@ import { GetAllRefugesErrors, } from '../../../../schemas/refuge/get-all-refuges-schema'; import { RefugeService } from '../../../services/refuge/refuge.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-refuges', @@ -35,6 +36,7 @@ export class RefugesListPage implements OnInit { private router: Router, private refugeService: RefugeService, private alertController: AlertController, + private translateService: TranslateService, ) { this.errors = this.refugeService.getRefuges().pipe( filter( @@ -87,13 +89,16 @@ export class RefugesListPage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alert', - subHeader: 'The client is failing', - message: - 'Is your internet connection working? Maybe is our fault and our server is down.', + header: this.translateService.instant('HOME.ERRORS.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant( + 'HOME.ERRORS.CLIENT_ERROR.SUBHEADER', + ), + message: this.translateService.instant( + 'HOME.ERRORS.CLIENT_ERROR.MESSAGE', + ), buttons: [ { - text: 'OK', + text: this.translateService.instant('HOME.ERRORS.CLIENT_ERROR.EXIT'), handler: () => { this.alertController.dismiss().then(); }, diff --git a/app/src/app/pages/refuges/refuges-routing.module.ts b/app/src/app/pages/refuges/refuges-routing.module.ts index 668a844..99c7aa4 100644 --- a/app/src/app/pages/refuges/refuges-routing.module.ts +++ b/app/src/app/pages/refuges/refuges-routing.module.ts @@ -2,7 +2,8 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { RefugesListPage } from './refuge-list/refuges-list.page'; -import { supervisorGuard } from '../../guards/admin.guard'; +import { supervisorGuard } from '../../guards/supervisor.guard'; +import { RefugeDetailPage } from './refuge-detail/refuge-detail.page'; const routes: Routes = [ { @@ -10,6 +11,11 @@ const routes: Routes = [ component: RefugesListPage, canActivate: [supervisorGuard], }, + { + path: ':id', + component: RefugeDetailPage, + canActivate: [supervisorGuard], + }, ]; @NgModule({ diff --git a/app/src/app/pages/refuges/refuges.module.ts b/app/src/app/pages/refuges/refuges.module.ts index 2a90ce0..74fd5dd 100644 --- a/app/src/app/pages/refuges/refuges.module.ts +++ b/app/src/app/pages/refuges/refuges.module.ts @@ -7,6 +7,7 @@ import { IonicModule } from '@ionic/angular'; import { RefugesPageRoutingModule } from './refuges-routing.module'; import { RefugesListPage } from './refuge-list/refuges-list.page'; +import { RefugeDetailPage } from './refuge-detail/refuge-detail.page'; @NgModule({ imports: [ @@ -16,6 +17,6 @@ import { RefugesListPage } from './refuge-list/refuges-list.page'; RefugesPageRoutingModule, NgOptimizedImage, ], - declarations: [RefugesListPage], + declarations: [RefugesListPage, RefugeDetailPage], }) export class RefugesPageModule {} diff --git a/app/src/app/services/language/device-language.service.spec.ts b/app/src/app/services/language/device-language.service.spec.ts new file mode 100644 index 0000000..68e6fc6 --- /dev/null +++ b/app/src/app/services/language/device-language.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DeviceLanguageService } from './device-language.service'; + +describe('DeviceLanguageService', () => { + let service: DeviceLanguageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DeviceLanguageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/app/src/app/services/language/device-language.service.ts b/app/src/app/services/language/device-language.service.ts new file mode 100644 index 0000000..7870a4a --- /dev/null +++ b/app/src/app/services/language/device-language.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Device } from '@capacitor/device'; +import { distinctUntilChanged, map, mergeMap, Observable, timer } from 'rxjs'; +import { fromPromise } from 'rxjs/internal/observable/innerFrom'; +import { StorageService } from '../storage/storage.service'; +import { TranslateService } from '@ngx-translate/core'; + +const LANGUAGE_KEY = 'language'; + +@Injectable({ + providedIn: 'root', +}) +export class DeviceLanguageService { + constructor( + private storageService: StorageService, + private translateService: TranslateService, + ) {} + + async getCurrentLanguageCode(): Promise { + const languageCode = await this.storageService.get(LANGUAGE_KEY); + if (languageCode) return languageCode; + return Device.getLanguageCode().then((languageTag) => languageTag.value); + } + + /** + * Gets the language code of the device, fetching it every 3 seconds. + * TODO: This is a workaround for the fact that Capacitor's Device plugin + * doesn't have an observable for getting the language code, so we have to + * poll it every 3 seconds. + */ + getLanguageCode(): Observable { + return timer(0, 3_000).pipe( + mergeMap(() => fromPromise(this.getCurrentLanguageCode())), + distinctUntilChanged(), + ); + } + + async setLanguageCode(languageCode: string): Promise { + const languageCodes = await this.getLanguagesCodes(); + if (!languageCodes.includes(languageCode)) + throw new Error(`Language code ${languageCode} not supported`); + await this.storageService.set(LANGUAGE_KEY, languageCode); + } + + async getLanguagesCodes(): Promise { + return this.translateService.getLangs(); + } +} diff --git a/app/src/app/services/refuge/refuge.service.ts b/app/src/app/services/refuge/refuge.service.ts index 45efc8b..58f2306 100644 --- a/app/src/app/services/refuge/refuge.service.ts +++ b/app/src/app/services/refuge/refuge.service.ts @@ -16,9 +16,17 @@ import { GetAllRefugesErrors, GetAllRefugesResponse, } from '../../../schemas/refuge/get-all-refuges-schema'; -import { Refuge, RefugePattern } from '../../../schemas/refuge/refuge'; +import { + isValidId, + Refuge, + RefugePattern, +} from '../../../schemas/refuge/refuge'; import { isMatching } from 'ts-pattern'; import { environment } from '../../../environments/environment'; +import { + GetRefugeFromIdErrors, + GetRefugeResponse, +} from '../../../schemas/refuge/get-refuge-schema'; @Injectable({ providedIn: 'root', @@ -42,6 +50,15 @@ export class RefugeService { return this.getRefugesConnection; } + getRefugeFrom(id: string): Observable { + if (!isValidId(id)) + return of({ + status: 'error', + error: GetRefugeFromIdErrors.CLIENT_SEND_DATA_ERROR, + }); + return this.getRefugeFromApi(id); + } + getImageUrlFor(refuge: Refuge): string { return `${environment.API}/static/images/refuges/${refuge.image}`; } @@ -68,7 +85,33 @@ export class RefugeService { ); } + private getRefugeFromApi(id: string): Observable { + const endpoint = this.getRefugeFromIdEndpoint(id); + return this.http.get(endpoint).pipe( + map((refuge: Refuge) => { + if (isMatching(RefugePattern, refuge)) + return { status: 'correct', data: refuge }; + return { + status: 'error', + error: GetRefugeFromIdErrors.SERVER_INCORRECT_DATA_FORMAT_ERROR, + }; + }), + catchError>( + (err: HttpErrorResponse) => + of({ + status: 'error', + error: GetRefugeFromIdErrors.from(err), + }), + ), + retry(3), + ); + } + private getAllRefugesEndpoint(): string { return `${environment.API}/refuges/`; } + + private getRefugeFromIdEndpoint(id: string): string { + return `${environment.API}/refuges/${id}`; + } } diff --git a/app/src/assets/i18n/en.json b/app/src/assets/i18n/en.json new file mode 100644 index 0000000..ed31c62 --- /dev/null +++ b/app/src/assets/i18n/en.json @@ -0,0 +1,11 @@ +{ + "HOME": { + "CLIENT_ERROR": { + "HEADER": "Alert!", + "SUBHEADER": "Your device is failing!", + "MESSAGE": "Does your device have internet access? Maybe is our fault and our servers are down", + "TRY_AGAIN": "Try again", + "EXIT": "Go home" + } + } +} diff --git a/app/src/environments/environment.prod.ts b/app/src/environments/environment.prod.ts new file mode 100644 index 0000000..a017a16 --- /dev/null +++ b/app/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + API: 'https://backend.refuapp.online', +}; diff --git a/app/src/schemas/refuge/get-refuge-schema.ts b/app/src/schemas/refuge/get-refuge-schema.ts new file mode 100644 index 0000000..37c4d6b --- /dev/null +++ b/app/src/schemas/refuge/get-refuge-schema.ts @@ -0,0 +1,37 @@ +import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { match } from 'ts-pattern'; +import { Refuge } from './refuge'; + +export enum GetRefugeFromIdErrors { + NOT_FOUND = 'NOT_FOUND', + CLIENT_SEND_DATA_ERROR = 'CLIENT_SEND_DATA_ERROR', + PROGRAMMER_SEND_DATA_ERROR = 'PROGRAMMER_SEND_DATA_ERROR', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', + SERVER_INCORRECT_DATA_FORMAT_ERROR = 'SERVER_INCORRECT_DATA_FORMAT_ERROR', +} + +export type GetRefugeResponse = + | { + status: 'correct'; + data: Refuge; + } + | { + status: 'error'; + error: GetRefugeFromIdErrors; + }; + +export namespace GetRefugeFromIdErrors { + export function from(err: HttpErrorResponse): GetRefugeFromIdErrors | never { + return match(err.status) + .returnType() + .with(0, () => { + throw new Error('You are offline or the server is down.'); + }) + .with(HttpStatusCode.NotFound, () => GetRefugeFromIdErrors.NOT_FOUND) + .with( + HttpStatusCode.UnprocessableEntity, + () => GetRefugeFromIdErrors.PROGRAMMER_SEND_DATA_ERROR, + ) + .otherwise(() => GetRefugeFromIdErrors.UNKNOWN_ERROR); + } +} diff --git a/app/src/schemas/refuge/refuge.ts b/app/src/schemas/refuge/refuge.ts index c0c90e6..20625b8 100644 --- a/app/src/schemas/refuge/refuge.ts +++ b/app/src/schemas/refuge/refuge.ts @@ -1,4 +1,5 @@ import { P } from 'ts-pattern'; +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; export type Refuge = { id: string; @@ -17,3 +18,7 @@ export type Refuge = { }; export const RefugePattern: P.Pattern = {}; + +export function isValidId(id: string): boolean { + return uuidValidate(id) && uuidVersion(id) === 4; +}