Place creation: GMaps API integration

This commit is contained in:
itskovacs 2025-11-01 19:34:56 +01:00
parent 028b9ab64f
commit 0e3addeb94
7 changed files with 114 additions and 16 deletions

View File

@ -232,15 +232,48 @@
<div class="mt-6 flex justify-between items-center">
<div class="min-w-0 truncate">
<h1 class="font-semibold tracking-tight text-xl">TOTP</h1>
<div class="flex gap-2 items-center">
<h1 class="font-semibold tracking-tight text-xl">TOTP</h1>
@if (settings?.totp_enabled) {
<span
class="inline-flex items-center gap-x-1.5 bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded min-w-fit dark:bg-green-400">
<span class="size-1.5 inline-block rounded-full bg-green-800 dark:bg-green-500"></span>enabled</span>
} @else {
<span
class="inline-flex items-center gap-x-1.5 bg-red-100 text-red-800 text-xs font-medium px-2.5 py-0.5 rounded min-w-fit dark:bg-red-400">
<span class="size-1.5 inline-block rounded-full bg-red-800 dark:bg-red-500"></span>disabled</span>
}
</div>
<span class="text-xs text-gray-500 dark:text-gray-400">Add a second layer of security to your
account</span>
</div>
<p-button [icon]="settings?.totp_enabled ? 'pi pi-lock' : 'pi pi-lock-open'" (click)="toggleTOTP()"
[severity]="settings?.totp_enabled ? 'success' : 'danger'"
[pTooltip]="settings?.totp_enabled ? 'Click to disable' : 'Click to enable'"
[label]="settings?.totp_enabled ? 'Enabled' : 'Disabled'" text />
<p-button [icon]="settings?.totp_enabled ? 'pi pi-trash' : 'pi pi-lock'" (click)="toggleTOTP()"
[severity]="settings?.totp_enabled ? 'danger' : 'success'"
[label]="settings?.totp_enabled ? 'Disable' : 'Enable'" text />
</div>
<div [formGroup]="settingsForm">
@if (settings?.google_apikey) {
<div class="mt-4 flex justify-between items-center">
<div class="min-w-0 truncate">
<h1 class="font-semibold tracking-tight text-xl">Google API Key</h1>
</div>
<p-button icon="pi pi-trash" (click)="deleteGoogleApiKey()" severity="danger" label="Delete" text />
</div>
} @else {
<h1 class="mt-4 font-semibold tracking-tight text-xl">Google API Key</h1>
<p-floatlabel variant="in" class="mt-4">
<input id="_google_apikey" formControlName="_google_apikey" pInputText fluid />
<label for="_google_apikey">API Key</label>
</p-floatlabel>
<div class="mt-2 w-full text-right">
<p-button (click)="updateSettings()" label="Update" text
[disabled]="!settingsForm.valid || settingsForm.pristine" />
</div>
}
</div>
</p-tabpanel>
<p-tabpanel [value]="2">

View File

@ -133,6 +133,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
currency: ['', Validators.required],
do_not_display: [],
tile_layer: ['', Validators.required],
_google_apikey: [null, { validators: [Validators.pattern('AIza[0-9A-Za-z\\-]{35}')] }],
});
// HACK: Subscribe in constructor for takeUntilDestroyed
@ -666,7 +667,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
updateSettings() {
this.apiService
.putSettings(this.settingsForm.value)
.putSettings({ ...this.settingsForm.value, google_apikey: this.settingsForm.get('_google_apikey')?.value })
.pipe(take(1))
.subscribe({
next: (settings) => {
@ -980,4 +981,27 @@ export class DashboardComponent implements OnInit, AfterViewInit {
},
});
}
deleteGoogleApiKey() {
const modal = this.dialogService.open(YesNoModalComponent, {
header: 'Confirm',
modal: true,
closable: true,
dismissableMask: true,
breakpoints: {
'640px': '90vw',
},
data: 'Are you sure you want to delete GMaps API Key ?',
})!;
modal.onClose.subscribe({
next: (bool: boolean) => {
if (!bool) return;
this.apiService.putSettings({ google_apikey: null }).subscribe({
next: () => (this.settings!.google_apikey = false),
error: () => this.utilsService.toast('error', 'Error', 'Error deleting GMaps API key'),
});
},
});
}
}

View File

@ -1,9 +1,13 @@
<section>
<div pFocusTrap class="grid grid-cols-2 md:grid-cols-4 gap-4" [formGroup]="placeForm">
<p-floatlabel variant="in" class="col-span-2">
<input id="name" formControlName="name" pInputText autofocus fluid />
<label for="name">Name</label>
</p-floatlabel>
<div class="relative col-span-2">
<p-floatlabel variant="in">
<input id="name" formControlName="name" pInputText autofocus fluid />
<label for="name">Name</label>
</p-floatlabel>
<p-button icon="pi pi-sparkles" variant="text" [disabled]="!placeForm.get('name')!.value"
class="absolute right-2 top-1/2 -translate-y-1/2" pTooltip="Query GMaps API" (click)="gmapsSearchText()" />
</div>
<p-floatlabel variant="in">
<input id="lat" formControlName="lat" pInputText fluid placeholder="Lat or Lat, Lng" />
@ -17,7 +21,7 @@
<p-inputgroup class="col-span-2 lg:col-span-3">
<p-floatlabel variant="in">
<input id="place" formControlName="place" pInputText fluid placeholder="" />
<input id="place" formControlName="place" pInputText fluid />
<label for="place">Place</label>
</p-floatlabel>
<p-inputgroup-addon>
@ -70,10 +74,17 @@
</div>
<div class="grid col-span-full md:grid-cols-4">
<p-floatlabel variant="in" class="col-span-full md:col-span-3">
<textarea pTextarea id="description" formControlName="description" rows="3" autoResize fluid></textarea>
<label for="description">Description</label>
</p-floatlabel>
<div class="relative col-span-full md:col-span-3">
<p-floatlabel variant="in">
<textarea pTextarea id="description" formControlName="description" rows="3" #descriptionInput autoResize
fluid></textarea>
<label for="description">Description</label>
</p-floatlabel>
@if (descriptionInput.value) {
<p-button icon="pi pi-times" severity="danger" variant="text" class="absolute right-2 top-2"
(click)="placeForm.get('description')?.setValue('')" pTooltip="Clear" />
}
</div>
<div class="mt-4 md:mt-0 grid place-items-center col-span-full md:col-span-1">
@if (placeForm.get("image_id")?.value) {

View File

@ -203,4 +203,19 @@ export class PlaceCreateModalComponent {
this.placeForm.get('gpx')?.setValue(null);
this.placeForm.get('gpx')?.markAsDirty();
}
gmapsSearchText() {
const query = this.placeForm.get('name')?.value;
if (!query) return;
this.apiService.gmapsSearchText(query).subscribe({
next: (results) => {
if (results.length == 1) {
const r = results[0];
this.placeForm.patchValue({ ...r, lat: formatLatLng(r.lat), lng: formatLatLng(r.lng), place: r.name || '' });
this.placeForm.get('category')?.markAsDirty();
return;
}
},
});
}
}

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Category, Place } from '../types/poi';
import { Category, GooglePlaceResult, Place } from '../types/poi';
import { BehaviorSubject, map, Observable, shareReplay, tap } from 'rxjs';
import { Info } from '../types/info';
import { Backup, ImportResponse, Settings } from '../types/settings';
@ -321,4 +321,8 @@ export class ApiService {
verifyTOTP(code: string): Observable<any> {
return this.httpClient.post<any>(this.apiBaseUrl + '/settings/totp/verify', { code });
}
gmapsSearchText(q: string): Observable<GooglePlaceResult[]> {
return this.httpClient.get<GooglePlaceResult[]>(`${this.apiBaseUrl}/places/google-search`, { params: { q } });
}
}

View File

@ -25,3 +25,13 @@ export interface Place {
visited?: boolean;
favorite?: boolean;
}
export interface GooglePlaceResult {
name: string;
lat: number;
lng: number;
price: number;
types: string[];
allowdog: boolean;
description: string;
}

View File

@ -11,6 +11,7 @@ export interface Settings {
mode_dark?: boolean;
mode_gpx_in_place?: boolean;
totp_enabled?: boolean;
google_apikey?: boolean | null;
}
export interface ImportResponse {