🎨 Prettier setup
This commit is contained in:
parent
aa17b78a40
commit
42654fb9a1
@ -1 +1,9 @@
|
|||||||
{}
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"semi": true,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
import {
|
import { ApplicationConfig, provideZoneChangeDetection, isDevMode } from '@angular/core';
|
||||||
ApplicationConfig,
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
provideZoneChangeDetection,
|
import { provideRouter } from '@angular/router';
|
||||||
isDevMode,
|
import { routes } from './app.routes';
|
||||||
} from "@angular/core";
|
import { providePrimeNG } from 'primeng/config';
|
||||||
import { provideAnimationsAsync } from "@angular/platform-browser/animations/async";
|
import { TripThemePreset } from '../mytheme';
|
||||||
import { provideRouter } from "@angular/router";
|
import { MessageService } from 'primeng/api';
|
||||||
import { routes } from "./app.routes";
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { providePrimeNG } from "primeng/config";
|
import { Interceptor } from './services/interceptor.service';
|
||||||
import { TripThemePreset } from "../mytheme";
|
import { DialogService } from 'primeng/dynamicdialog';
|
||||||
import { MessageService } from "primeng/api";
|
import { provideServiceWorker } from '@angular/service-worker';
|
||||||
import { provideHttpClient, withInterceptors } from "@angular/common/http";
|
|
||||||
import { Interceptor } from "./services/interceptor.service";
|
|
||||||
import { DialogService } from "primeng/dynamicdialog";
|
|
||||||
import { provideServiceWorker } from "@angular/service-worker";
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -27,19 +23,19 @@ export const appConfig: ApplicationConfig = {
|
|||||||
theme: {
|
theme: {
|
||||||
preset: TripThemePreset,
|
preset: TripThemePreset,
|
||||||
options: {
|
options: {
|
||||||
darkModeSelector: ".dark",
|
darkModeSelector: '.dark',
|
||||||
cssLayer: {
|
cssLayer: {
|
||||||
name: "primeng",
|
name: 'primeng',
|
||||||
order: "tailwind, primeng",
|
order: 'tailwind, primeng',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
MessageService,
|
MessageService,
|
||||||
DialogService,
|
DialogService,
|
||||||
provideServiceWorker("ngsw-worker.js", {
|
provideServiceWorker('ngsw-worker.js', {
|
||||||
enabled: !isDevMode(),
|
enabled: !isDevMode(),
|
||||||
registrationStrategy: "registerWhenStable:30000",
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,62 +1,62 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { AuthComponent } from "./components/auth/auth.component";
|
import { AuthComponent } from './components/auth/auth.component';
|
||||||
|
|
||||||
import { DashboardComponent } from "./components/dashboard/dashboard.component";
|
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||||
import { AuthGuard } from "./services/auth.guard";
|
import { AuthGuard } from './services/auth.guard';
|
||||||
import { TripComponent } from "./components/trip/trip.component";
|
import { TripComponent } from './components/trip/trip.component';
|
||||||
import { TripsComponent } from "./components/trips/trips.component";
|
import { TripsComponent } from './components/trips/trips.component';
|
||||||
import { SharedTripComponent } from "./components/shared-trip/shared-trip.component";
|
import { SharedTripComponent } from './components/shared-trip/shared-trip.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "auth",
|
path: 'auth',
|
||||||
pathMatch: "full",
|
pathMatch: 'full',
|
||||||
component: AuthComponent,
|
component: AuthComponent,
|
||||||
title: "TRIP - Authentication",
|
title: 'TRIP - Authentication',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "s",
|
path: 's',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "t/:token",
|
path: 't/:token',
|
||||||
component: SharedTripComponent,
|
component: SharedTripComponent,
|
||||||
title: "TRIP - Shared Trip",
|
title: 'TRIP - Shared Trip',
|
||||||
},
|
},
|
||||||
|
|
||||||
{ path: "**", redirectTo: "/home", pathMatch: "full" },
|
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "",
|
path: '',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "home",
|
path: 'home',
|
||||||
component: DashboardComponent,
|
component: DashboardComponent,
|
||||||
title: "TRIP - Map",
|
title: 'TRIP - Map',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "trips",
|
path: 'trips',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: '',
|
||||||
component: TripsComponent,
|
component: TripsComponent,
|
||||||
title: "TRIP - Trips",
|
title: 'TRIP - Trips',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ':id',
|
||||||
component: TripComponent,
|
component: TripComponent,
|
||||||
title: "TRIP - Trip",
|
title: 'TRIP - Trip',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{ path: "**", redirectTo: "/home", pathMatch: "full" },
|
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{ path: "**", redirectTo: "/", pathMatch: "full" },
|
{ path: '**', redirectTo: '/', pathMatch: 'full' },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.cover-auth {
|
.cover-auth {
|
||||||
background: url("/cover.webp");
|
background: url('/cover.webp');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,19 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
FormGroup,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
ReactiveFormsModule,
|
import { ButtonModule } from 'primeng/button';
|
||||||
Validators,
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
} from "@angular/forms";
|
import { AuthParams, AuthService } from '../../services/auth.service';
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { MessageModule } from 'primeng/message';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
import { take } from 'rxjs';
|
||||||
import { AuthParams, AuthService } from "../../services/auth.service";
|
|
||||||
import { MessageModule } from "primeng/message";
|
|
||||||
import { HttpErrorResponse } from "@angular/common/http";
|
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
|
||||||
import { take } from "rxjs";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-auth",
|
selector: 'app-auth',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
FloatLabelModule,
|
FloatLabelModule,
|
||||||
@ -29,8 +24,8 @@ import { take } from "rxjs";
|
|||||||
FocusTrapModule,
|
FocusTrapModule,
|
||||||
MessageModule,
|
MessageModule,
|
||||||
],
|
],
|
||||||
templateUrl: "./auth.component.html",
|
templateUrl: './auth.component.html',
|
||||||
styleUrl: "./auth.component.scss",
|
styleUrl: './auth.component.scss',
|
||||||
})
|
})
|
||||||
export class AuthComponent implements OnInit {
|
export class AuthComponent implements OnInit {
|
||||||
readonly redirectURL: string;
|
readonly redirectURL: string;
|
||||||
@ -45,31 +40,29 @@ export class AuthComponent implements OnInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
) {
|
) {
|
||||||
this.redirectURL =
|
this.redirectURL = this.route.snapshot.queryParams['redirectURL'] || '/home';
|
||||||
this.route.snapshot.queryParams["redirectURL"] || "/home";
|
|
||||||
|
|
||||||
this.authForm = this.fb.group({
|
this.authForm = this.fb.group({
|
||||||
username: ["", { validators: Validators.required }],
|
username: ['', { validators: Validators.required }],
|
||||||
password: ["", { validators: Validators.required }],
|
password: ['', { validators: Validators.required }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||||
const code = params["code"];
|
const code = params['code'];
|
||||||
const state = params["state"];
|
const state = params['state'];
|
||||||
if (code && state) {
|
if (code && state) {
|
||||||
this.authService.oidcLogin(code, state).subscribe({
|
this.authService.oidcLogin(code, state).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
if (!data.access_token) {
|
if (!data.access_token) {
|
||||||
this.error = "Authentication failed";
|
this.error = 'Authentication failed';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.router.navigateByUrl(this.redirectURL);
|
this.router.navigateByUrl(this.redirectURL);
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this.error =
|
this.error = err.error.detail || 'Login failed, check console for details';
|
||||||
err.error.detail || "Login failed, check console for details";
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -92,9 +85,7 @@ export class AuthComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this.authForm.reset();
|
this.authForm.reset();
|
||||||
this.error =
|
this.error = err.error.detail || 'Registration failed, check console for details';
|
||||||
err.error.detail ||
|
|
||||||
"Registration failed, check console for details";
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -109,7 +100,7 @@ export class AuthComponent implements OnInit {
|
|||||||
this.authService.login(this.authForm.value).subscribe({
|
this.authService.login(this.authForm.value).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
if (!data.access_token) {
|
if (!data.access_token) {
|
||||||
this.error = "Authentication failed";
|
this.error = 'Authentication failed';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.router.navigateByUrl(this.redirectURL);
|
this.router.navigateByUrl(this.redirectURL);
|
||||||
|
|||||||
@ -1,48 +1,36 @@
|
|||||||
import { AfterViewInit, Component, OnInit } from "@angular/core";
|
import { AfterViewInit, Component, OnInit } from '@angular/core';
|
||||||
import { combineLatest, debounceTime, take, tap } from "rxjs";
|
import { combineLatest, debounceTime, take, tap } from 'rxjs';
|
||||||
import { Place, Category } from "../../types/poi";
|
import { Place, Category } from '../../types/poi';
|
||||||
import { ApiService } from "../../services/api.service";
|
import { ApiService } from '../../services/api.service';
|
||||||
import { PlaceBoxComponent } from "../../shared/place-box/place-box.component";
|
import { PlaceBoxComponent } from '../../shared/place-box/place-box.component';
|
||||||
import * as L from "leaflet";
|
import * as L from 'leaflet';
|
||||||
import "leaflet.markercluster";
|
import 'leaflet.markercluster';
|
||||||
import "leaflet-contextmenu";
|
import 'leaflet-contextmenu';
|
||||||
import {
|
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormControl,
|
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
FormGroup,
|
import { PlaceCreateModalComponent } from '../../modals/place-create-modal/place-create-modal.component';
|
||||||
FormsModule,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
ReactiveFormsModule,
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
Validators,
|
import { TabsModule } from 'primeng/tabs';
|
||||||
} from "@angular/forms";
|
import { ToggleSwitchModule } from 'primeng/toggleswitch';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { BatchCreateModalComponent } from '../../modals/batch-create-modal/batch-create-modal.component';
|
||||||
import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component";
|
import { UtilsService } from '../../services/utils.service';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { Info } from '../../types/info';
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { createMap, placeToMarker, createClusterGroup, gpxToPolyline } from '../../shared/map';
|
||||||
import { TabsModule } from "primeng/tabs";
|
import { Router } from '@angular/router';
|
||||||
import { ToggleSwitchModule } from "primeng/toggleswitch";
|
import { SelectModule } from 'primeng/select';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
import { BatchCreateModalComponent } from "../../modals/batch-create-modal/batch-create-modal.component";
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { UtilsService } from "../../services/utils.service";
|
import { Settings } from '../../types/settings';
|
||||||
import { Info } from "../../types/info";
|
import { SelectItemGroup } from 'primeng/api';
|
||||||
import {
|
import { YesNoModalComponent } from '../../modals/yes-no-modal/yes-no-modal.component';
|
||||||
createMap,
|
import { CategoryCreateModalComponent } from '../../modals/category-create-modal/category-create-modal.component';
|
||||||
placeToMarker,
|
import { AuthService } from '../../services/auth.service';
|
||||||
createClusterGroup,
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
gpxToPolyline,
|
import { PlaceGPXComponent } from '../../shared/place-gpx/place-gpx.component';
|
||||||
} from "../../shared/map";
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router } from "@angular/router";
|
|
||||||
import { SelectModule } from "primeng/select";
|
|
||||||
import { MultiSelectModule } from "primeng/multiselect";
|
|
||||||
import { TooltipModule } from "primeng/tooltip";
|
|
||||||
import { Settings } from "../../types/settings";
|
|
||||||
import { SelectItemGroup } from "primeng/api";
|
|
||||||
import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component";
|
|
||||||
import { CategoryCreateModalComponent } from "../../modals/category-create-modal/category-create-modal.component";
|
|
||||||
import { AuthService } from "../../services/auth.service";
|
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
||||||
import { PlaceGPXComponent } from "../../shared/place-gpx/place-gpx.component";
|
|
||||||
import { CommonModule } from "@angular/common";
|
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
text: string;
|
text: string;
|
||||||
@ -60,7 +48,7 @@ export interface MarkerOptions extends L.MarkerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-dashboard",
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
PlaceBoxComponent,
|
PlaceBoxComponent,
|
||||||
@ -78,11 +66,11 @@ export interface MarkerOptions extends L.MarkerOptions {
|
|||||||
ButtonModule,
|
ButtonModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
],
|
],
|
||||||
templateUrl: "./dashboard.component.html",
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrls: ["./dashboard.component.scss"],
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, AfterViewInit {
|
export class DashboardComponent implements OnInit, AfterViewInit {
|
||||||
searchInput = new FormControl("");
|
searchInput = new FormControl('');
|
||||||
info?: Info;
|
info?: Info;
|
||||||
isLowNet = false;
|
isLowNet = false;
|
||||||
isDarkMode = false;
|
isDarkMode = false;
|
||||||
@ -123,36 +111,29 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
) {
|
) {
|
||||||
this.settingsForm = this.fb.group({
|
this.settingsForm = this.fb.group({
|
||||||
map_lat: [
|
map_lat: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [Validators.required, Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)')],
|
||||||
Validators.required,
|
|
||||||
Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
map_lng: [
|
map_lng: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.pattern(
|
Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
|
||||||
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
currency: ["", Validators.required],
|
currency: ['', Validators.required],
|
||||||
do_not_display: [],
|
do_not_display: [],
|
||||||
tile_layer: ["", Validators.required],
|
tile_layer: ['', Validators.required],
|
||||||
});
|
});
|
||||||
|
|
||||||
// HACK: Subscribe in constructor for takeUntilDestroyed
|
// HACK: Subscribe in constructor for takeUntilDestroyed
|
||||||
this.searchInput.valueChanges
|
this.searchInput.valueChanges.pipe(debounceTime(200), takeUntilDestroyed()).subscribe({
|
||||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
next: () => this.setVisibleMarkers(),
|
||||||
.subscribe({
|
});
|
||||||
next: () => this.setVisibleMarkers(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -195,27 +176,24 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
initMap(): void {
|
initMap(): void {
|
||||||
if (!this.settings) return;
|
if (!this.settings) return;
|
||||||
const isTouch = "ontouchstart" in window;
|
const isTouch = 'ontouchstart' in window;
|
||||||
const contentMenuItems = [
|
const contentMenuItems = [
|
||||||
{
|
{
|
||||||
text: "Add Point of Interest",
|
text: 'Add Point of Interest',
|
||||||
icon: "add-location.png",
|
icon: 'add-location.png',
|
||||||
callback: (e: any) => {
|
callback: (e: any) => {
|
||||||
this.addPlaceModal(e);
|
this.addPlaceModal(e);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.map = createMap(
|
this.map = createMap(isTouch ? [] : contentMenuItems, this.settings?.tile_layer);
|
||||||
isTouch ? [] : contentMenuItems,
|
|
||||||
this.settings?.tile_layer,
|
|
||||||
);
|
|
||||||
if (isTouch) {
|
if (isTouch) {
|
||||||
this.map.on("contextmenu", (e: any) => {
|
this.map.on('contextmenu', (e: any) => {
|
||||||
this.addPlaceModal(e);
|
this.addPlaceModal(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.map.setView(L.latLng(this.settings.map_lat, this.settings.map_lng));
|
this.map.setView(L.latLng(this.settings.map_lat, this.settings.map_lng));
|
||||||
this.map.on("moveend zoomend", () => this.setVisibleMarkers());
|
this.map.on('moveend zoomend', () => this.setVisibleMarkers());
|
||||||
this.markerClusterGroup = createClusterGroup().addTo(this.map);
|
this.markerClusterGroup = createClusterGroup().addTo(this.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,30 +201,22 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
if (!this.viewMarkersList || !this.map) return;
|
if (!this.viewMarkersList || !this.map) return;
|
||||||
const bounds = this.map.getBounds();
|
const bounds = this.map.getBounds();
|
||||||
|
|
||||||
this.visiblePlaces = this.filteredPlaces.filter((p) =>
|
this.visiblePlaces = this.filteredPlaces.filter((p) => bounds.contains([p.lat, p.lng]));
|
||||||
bounds.contains([p.lat, p.lng]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchValue = this.searchInput.value?.toLowerCase() ?? "";
|
const searchValue = this.searchInput.value?.toLowerCase() ?? '';
|
||||||
if (searchValue)
|
if (searchValue)
|
||||||
this.visiblePlaces = this.visiblePlaces.filter(
|
this.visiblePlaces = this.visiblePlaces.filter(
|
||||||
(p) =>
|
(p) => p.name.toLowerCase().includes(searchValue) || p.description?.toLowerCase().includes(searchValue),
|
||||||
p.name.toLowerCase().includes(searchValue) ||
|
|
||||||
p.description?.toLowerCase().includes(searchValue),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.visiblePlaces.sort((a, b) =>
|
this.visiblePlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
this.filter_display_visited = false;
|
this.filter_display_visited = false;
|
||||||
this.filter_display_favorite_only = false;
|
this.filter_display_favorite_only = false;
|
||||||
this.activeCategories = new Set(this.categories.map((c) => c.name));
|
this.activeCategories = new Set(this.categories.map((c) => c.name));
|
||||||
this.settings?.do_not_display.forEach((c) =>
|
this.settings?.do_not_display.forEach((c) => this.activeCategories.delete(c));
|
||||||
this.activeCategories.delete(c),
|
|
||||||
);
|
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
if (this.viewMarkersList) this.setVisibleMarkers();
|
if (this.viewMarkersList) this.setVisibleMarkers();
|
||||||
}
|
}
|
||||||
@ -278,47 +248,38 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_placeToMarker(place: Place): L.Marker {
|
_placeToMarker(place: Place): L.Marker {
|
||||||
const marker = placeToMarker(
|
const marker = placeToMarker(place, this.isLowNet, place.visited, this.isGpxInPlaceMode);
|
||||||
place,
|
|
||||||
this.isLowNet,
|
|
||||||
place.visited,
|
|
||||||
this.isGpxInPlaceMode,
|
|
||||||
);
|
|
||||||
marker
|
marker
|
||||||
.on("click", (e) => {
|
.on('click', (e) => {
|
||||||
this.selectedPlace = { ...place };
|
this.selectedPlace = { ...place };
|
||||||
|
|
||||||
let toView = { ...e.latlng };
|
let toView = { ...e.latlng };
|
||||||
if ("ontouchstart" in window) toView.lat = toView.lat - 0.0175;
|
if ('ontouchstart' in window) toView.lat = toView.lat - 0.0175;
|
||||||
|
|
||||||
marker.closeTooltip();
|
marker.closeTooltip();
|
||||||
this.map?.setView(toView);
|
this.map?.setView(toView);
|
||||||
})
|
})
|
||||||
.on("contextmenu", () => {
|
.on('contextmenu', () => {
|
||||||
if (this.map && (this.map as any).contextmenu)
|
if (this.map && (this.map as any).contextmenu) (this.map as any).contextmenu.hide();
|
||||||
(this.map as any).contextmenu.hide();
|
|
||||||
});
|
});
|
||||||
return marker;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlaceModal(e?: any): void {
|
addPlaceModal(e?: any): void {
|
||||||
const opts = e ? { data: { place: e.latlng } } : {};
|
const opts = e ? { data: { place: e.latlng } } : {};
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(PlaceCreateModalComponent, {
|
||||||
PlaceCreateModalComponent,
|
header: 'Create Place',
|
||||||
{
|
modal: true,
|
||||||
header: "Create Place",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
width: '55vw',
|
||||||
dismissableMask: true,
|
breakpoints: {
|
||||||
width: "55vw",
|
'1920px': '70vw',
|
||||||
breakpoints: {
|
'1260px': '90vw',
|
||||||
"1920px": "70vw",
|
|
||||||
"1260px": "90vw",
|
|
||||||
},
|
|
||||||
...opts,
|
|
||||||
},
|
},
|
||||||
)!;
|
...opts,
|
||||||
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (place: Place | null) => {
|
next: (place: Place | null) => {
|
||||||
@ -329,9 +290,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (place: Place) => {
|
next: (place: Place) => {
|
||||||
this.places = [...this.places, place].sort((a, b) =>
|
this.places = [...this.places, place].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
}, 10);
|
}, 10);
|
||||||
@ -342,21 +301,18 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
batchAddModal() {
|
batchAddModal() {
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(BatchCreateModalComponent, {
|
||||||
BatchCreateModalComponent,
|
header: 'Create Places',
|
||||||
{
|
modal: true,
|
||||||
header: "Create Places",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
width: '55vw',
|
||||||
dismissableMask: true,
|
breakpoints: {
|
||||||
width: "55vw",
|
'1920px': '70vw',
|
||||||
breakpoints: {
|
'1260px': '90vw',
|
||||||
"1920px": "70vw",
|
|
||||||
"1260px": "90vw",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (places: string | null) => {
|
next: (places: string | null) => {
|
||||||
@ -367,7 +323,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
parsedPlaces = JSON.parse(places);
|
parsedPlaces = JSON.parse(places);
|
||||||
if (!Array.isArray(parsedPlaces)) throw new Error();
|
if (!Array.isArray(parsedPlaces)) throw new Error();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.utilsService.toast("error", "Error", "Content looks invalid");
|
this.utilsService.toast('error', 'Error', 'Content looks invalid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,9 +331,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.postPlaces(parsedPlaces)
|
.postPlaces(parsedPlaces)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((places) => {
|
.subscribe((places) => {
|
||||||
this.places = [...this.places, ...places].sort((a, b) =>
|
this.places = [...this.places, ...places].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
}, 10);
|
}, 10);
|
||||||
@ -388,7 +342,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
resetHoverPlace() {
|
resetHoverPlace() {
|
||||||
if (!this.hoveredElement) return;
|
if (!this.hoveredElement) return;
|
||||||
this.hoveredElement.classList.remove("list-hover");
|
this.hoveredElement.classList.remove('list-hover');
|
||||||
this.hoveredElement = undefined;
|
this.hoveredElement = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,17 +359,15 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
if (markerElement) {
|
if (markerElement) {
|
||||||
// marker, not clustered
|
// marker, not clustered
|
||||||
markerElement.classList.add("list-hover");
|
markerElement.classList.add('list-hover');
|
||||||
this.hoveredElement = markerElement;
|
this.hoveredElement = markerElement;
|
||||||
} else {
|
} else {
|
||||||
// marker is clustered
|
// marker is clustered
|
||||||
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
|
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(marker);
|
||||||
marker,
|
|
||||||
);
|
|
||||||
if (parentCluster) {
|
if (parentCluster) {
|
||||||
const clusterEl = parentCluster.getElement();
|
const clusterEl = parentCluster.getElement();
|
||||||
if (clusterEl) {
|
if (clusterEl) {
|
||||||
clusterEl.classList.add("list-hover");
|
clusterEl.classList.add('list-hover');
|
||||||
this.hoveredElement = clusterEl;
|
this.hoveredElement = clusterEl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -431,11 +383,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
const idx = this.places.findIndex(
|
const idx = this.places.findIndex((p) => p.id === this.selectedPlace!.id);
|
||||||
(p) => p.id === this.selectedPlace!.id,
|
if (idx !== -1) this.places[idx] = { ...this.places[idx], favorite: favoriteBool };
|
||||||
);
|
|
||||||
if (idx !== -1)
|
|
||||||
this.places[idx] = { ...this.places[idx], favorite: favoriteBool };
|
|
||||||
this.selectedPlace = { ...this.places[idx] };
|
this.selectedPlace = { ...this.places[idx] };
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
},
|
},
|
||||||
@ -451,11 +400,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
const idx = this.places.findIndex(
|
const idx = this.places.findIndex((p) => p.id === this.selectedPlace!.id);
|
||||||
(p) => p.id === this.selectedPlace!.id,
|
if (idx !== -1) this.places[idx] = { ...this.places[idx], visited: visitedBool };
|
||||||
);
|
|
||||||
if (idx !== -1)
|
|
||||||
this.places[idx] = { ...this.places[idx], visited: visitedBool };
|
|
||||||
this.selectedPlace = { ...this.places[idx] };
|
this.selectedPlace = { ...this.places[idx] };
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
},
|
},
|
||||||
@ -466,12 +412,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
if (!this.selectedPlace) return;
|
if (!this.selectedPlace) return;
|
||||||
|
|
||||||
const modal = this.dialogService.open(YesNoModalComponent, {
|
const modal = this.dialogService.open(YesNoModalComponent, {
|
||||||
header: "Confirm deletion",
|
header: 'Confirm deletion',
|
||||||
modal: true,
|
modal: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
dismissableMask: true,
|
dismissableMask: true,
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
"640px": "90vw",
|
'640px': '90vw',
|
||||||
},
|
},
|
||||||
data: `Delete ${this.selectedPlace.name} ?`,
|
data: `Delete ${this.selectedPlace.name} ?`,
|
||||||
})!;
|
})!;
|
||||||
@ -484,9 +430,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.places = this.places.filter(
|
this.places = this.places.filter((p) => p.id !== this.selectedPlace!.id);
|
||||||
(p) => p.id !== this.selectedPlace!.id,
|
|
||||||
);
|
|
||||||
this.closePlaceBox();
|
this.closePlaceBox();
|
||||||
this.updateMarkersAndClusters();
|
this.updateMarkersAndClusters();
|
||||||
if (this.viewMarkersList) this.setVisibleMarkers();
|
if (this.viewMarkersList) this.setVisibleMarkers();
|
||||||
@ -500,27 +444,24 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
if (!this.selectedPlace && !p) return;
|
if (!this.selectedPlace && !p) return;
|
||||||
const _placeToEdit: Place = { ...(this.selectedPlace ?? p)! };
|
const _placeToEdit: Place = { ...(this.selectedPlace ?? p)! };
|
||||||
|
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(PlaceCreateModalComponent, {
|
||||||
PlaceCreateModalComponent,
|
header: 'Edit Place',
|
||||||
{
|
modal: true,
|
||||||
header: "Edit Place",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
width: '55vw',
|
||||||
dismissableMask: true,
|
breakpoints: {
|
||||||
width: "55vw",
|
'1920px': '70vw',
|
||||||
breakpoints: {
|
'1260px': '90vw',
|
||||||
"1920px": "70vw",
|
},
|
||||||
"1260px": "90vw",
|
data: {
|
||||||
},
|
place: {
|
||||||
data: {
|
..._placeToEdit,
|
||||||
place: {
|
category: _placeToEdit.category.id,
|
||||||
..._placeToEdit,
|
|
||||||
category: _placeToEdit.category.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (place: Place | null) => {
|
next: (place: Place | null) => {
|
||||||
@ -534,9 +475,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
const places = [...this.places];
|
const places = [...this.places];
|
||||||
const idx = places.findIndex((p) => p.id == place.id);
|
const idx = places.findIndex((p) => p.id == place.id);
|
||||||
if (idx > -1) places.splice(idx, 1, place);
|
if (idx > -1) places.splice(idx, 1, place);
|
||||||
places.sort((a, b) =>
|
places.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
this.places = places;
|
this.places = places;
|
||||||
if (this.selectedPlace) this.selectedPlace = { ...place };
|
if (this.selectedPlace) this.selectedPlace = { ...place };
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -551,21 +490,20 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
displayGPXOnMap(gpx: string) {
|
displayGPXOnMap(gpx: string) {
|
||||||
if (!this.map || !this.selectedPlace) return;
|
if (!this.map || !this.selectedPlace) return;
|
||||||
if (!this.gpxLayerGroup)
|
if (!this.gpxLayerGroup) this.gpxLayerGroup = L.layerGroup().addTo(this.map);
|
||||||
this.gpxLayerGroup = L.layerGroup().addTo(this.map);
|
|
||||||
this.gpxLayerGroup.clearLayers();
|
this.gpxLayerGroup.clearLayers();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gpxPolyline = gpxToPolyline(gpx);
|
const gpxPolyline = gpxToPolyline(gpx);
|
||||||
const selectedPlaceWithGPX = { ...this.selectedPlace, gpx };
|
const selectedPlaceWithGPX = { ...this.selectedPlace, gpx };
|
||||||
|
|
||||||
gpxPolyline.on("click", () => {
|
gpxPolyline.on('click', () => {
|
||||||
this.selectedGPX = selectedPlaceWithGPX;
|
this.selectedGPX = selectedPlaceWithGPX;
|
||||||
});
|
});
|
||||||
this.gpxLayerGroup?.addLayer(gpxPolyline);
|
this.gpxLayerGroup?.addLayer(gpxPolyline);
|
||||||
this.map.fitBounds(gpxPolyline.getBounds(), { padding: [20, 20] });
|
this.map.fitBounds(gpxPolyline.getBounds(), { padding: [20, 20] });
|
||||||
} catch {
|
} catch {
|
||||||
this.utilsService.toast("error", "Error", "Couldn't parse GPX data");
|
this.utilsService.toast('error', 'Error', "Couldn't parse GPX data");
|
||||||
}
|
}
|
||||||
this.closePlaceBox();
|
this.closePlaceBox();
|
||||||
}
|
}
|
||||||
@ -578,11 +516,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (p) => {
|
next: (p) => {
|
||||||
if (!p.gpx) {
|
if (!p.gpx) {
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('error', 'Error', "Couldn't retrieve GPX data");
|
||||||
"error",
|
|
||||||
"Error",
|
|
||||||
"Couldn't retrieve GPX data",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.displayGPXOnMap(p.gpx);
|
this.displayGPXOnMap(p.gpx);
|
||||||
@ -597,7 +531,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
this.settingsForm.reset(this.settings);
|
this.settingsForm.reset(this.settings);
|
||||||
this.doNotDisplayOptions = [
|
this.doNotDisplayOptions = [
|
||||||
{
|
{
|
||||||
label: "Categories",
|
label: 'Categories',
|
||||||
items: this.categories.map((c) => ({ label: c.name, value: c.name })),
|
items: this.categories.map((c) => ({ label: c.name, value: c.name })),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -610,13 +544,13 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
toggleMarkersList() {
|
toggleMarkersList() {
|
||||||
this.viewMarkersList = !this.viewMarkersList;
|
this.viewMarkersList = !this.viewMarkersList;
|
||||||
this.viewMarkersListSearch = false;
|
this.viewMarkersListSearch = false;
|
||||||
this.searchInput.setValue("");
|
this.searchInput.setValue('');
|
||||||
if (this.viewMarkersList) this.setVisibleMarkers();
|
if (this.viewMarkersList) this.setVisibleMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMarkersListSearch() {
|
toggleMarkersListSearch() {
|
||||||
this.viewMarkersListSearch = !this.viewMarkersListSearch;
|
this.viewMarkersListSearch = !this.viewMarkersListSearch;
|
||||||
if (this.viewMarkersListSearch) this.searchInput.setValue("");
|
if (this.viewMarkersListSearch) this.searchInput.setValue('');
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapCenterToCurrent() {
|
setMapCenterToCurrent() {
|
||||||
@ -631,7 +565,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
if (!input.files?.length) return;
|
if (!input.files?.length) return;
|
||||||
|
|
||||||
const formdata = new FormData();
|
const formdata = new FormData();
|
||||||
formdata.append("file", input.files[0]);
|
formdata.append('file', input.files[0]);
|
||||||
|
|
||||||
this.apiService
|
this.apiService
|
||||||
.settingsUserImport(formdata)
|
.settingsUserImport(formdata)
|
||||||
@ -667,12 +601,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((resp: Object) => {
|
.subscribe((resp: Object) => {
|
||||||
const dataBlob = new Blob([JSON.stringify(resp, null, 2)], {
|
const dataBlob = new Blob([JSON.stringify(resp, null, 2)], {
|
||||||
type: "application/json",
|
type: 'application/json',
|
||||||
});
|
});
|
||||||
const downloadURL = URL.createObjectURL(dataBlob);
|
const downloadURL = URL.createObjectURL(dataBlob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement('a');
|
||||||
link.href = downloadURL;
|
link.href = downloadURL;
|
||||||
link.download = `TRIP_backup_${new Date().toISOString().split("T")[0]}.json`;
|
link.download = `TRIP_backup_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
URL.revokeObjectURL(downloadURL);
|
URL.revokeObjectURL(downloadURL);
|
||||||
@ -699,22 +633,19 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editCategory(c: Category) {
|
editCategory(c: Category) {
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(CategoryCreateModalComponent, {
|
||||||
CategoryCreateModalComponent,
|
header: 'Update Category',
|
||||||
{
|
modal: true,
|
||||||
header: "Update Category",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
data: { category: c },
|
||||||
dismissableMask: true,
|
width: '40vw',
|
||||||
data: { category: c },
|
breakpoints: {
|
||||||
width: "40vw",
|
'960px': '70vw',
|
||||||
breakpoints: {
|
'640px': '90vw',
|
||||||
"960px": "70vw",
|
|
||||||
"640px": "90vw",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (category: Category | null) => {
|
next: (category: Category | null) => {
|
||||||
@ -725,17 +656,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (updated) => {
|
next: (updated) => {
|
||||||
this.categories = this.categories.map((cat) =>
|
this.categories = this.categories.map((cat) => (cat.id === updated.id ? updated : cat));
|
||||||
cat.id === updated.id ? updated : cat,
|
|
||||||
);
|
|
||||||
this.sortCategories();
|
this.sortCategories();
|
||||||
|
|
||||||
this.activeCategories = new Set(
|
this.activeCategories = new Set(this.categories.map((c) => c.name));
|
||||||
this.categories.map((c) => c.name),
|
|
||||||
);
|
|
||||||
this.places = this.places.map((p) => {
|
this.places = this.places.map((p) => {
|
||||||
if (p.category.id == updated.id)
|
if (p.category.id == updated.id) return { ...p, category: updated };
|
||||||
return { ...p, category: updated };
|
|
||||||
return p;
|
return p;
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -748,21 +674,18 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addCategory() {
|
addCategory() {
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(CategoryCreateModalComponent, {
|
||||||
CategoryCreateModalComponent,
|
header: 'Create Category',
|
||||||
{
|
modal: true,
|
||||||
header: "Create Category",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
width: '40vw',
|
||||||
dismissableMask: true,
|
breakpoints: {
|
||||||
width: "40vw",
|
'960px': '70vw',
|
||||||
breakpoints: {
|
'640px': '90vw',
|
||||||
"960px": "70vw",
|
|
||||||
"640px": "90vw",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (category: Category | null) => {
|
next: (category: Category | null) => {
|
||||||
@ -774,9 +697,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (category: Category) => {
|
next: (category: Category) => {
|
||||||
this.categories.push(category);
|
this.categories.push(category);
|
||||||
this.categories.sort((a, b) =>
|
this.categories.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
this.activeCategories.add(category.name);
|
this.activeCategories.add(category.name);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -786,14 +707,14 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
deleteCategory(c_id: number) {
|
deleteCategory(c_id: number) {
|
||||||
const modal = this.dialogService.open(YesNoModalComponent, {
|
const modal = this.dialogService.open(YesNoModalComponent, {
|
||||||
header: "Confirm deletion",
|
header: 'Confirm deletion',
|
||||||
modal: true,
|
modal: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
dismissableMask: true,
|
dismissableMask: true,
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
"640px": "90vw",
|
'640px': '90vw',
|
||||||
},
|
},
|
||||||
data: "Delete this category ?",
|
data: 'Delete this category ?',
|
||||||
})!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
@ -806,9 +727,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
next: () => {
|
next: () => {
|
||||||
this.categories = this.categories.filter((c) => c.id !== c_id);
|
this.categories = this.categories.filter((c) => c.id !== c_id);
|
||||||
|
|
||||||
this.activeCategories = new Set(
|
this.activeCategories = new Set(this.categories.map((c) => c.name));
|
||||||
this.categories.map((c) => c.name),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -824,13 +743,11 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortCategories() {
|
sortCategories() {
|
||||||
this.categories = [...this.categories].sort((a, b) =>
|
this.categories = [...this.categories].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToTrips() {
|
navigateToTrips() {
|
||||||
this.router.navigateByUrl("/trips");
|
this.router.navigateByUrl('/trips');
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
@ -849,7 +766,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
if (!this.selectedGPX?.gpx) return;
|
if (!this.selectedGPX?.gpx) return;
|
||||||
const dataBlob = new Blob([this.selectedGPX.gpx]);
|
const dataBlob = new Blob([this.selectedGPX.gpx]);
|
||||||
const downloadURL = URL.createObjectURL(dataBlob);
|
const downloadURL = URL.createObjectURL(dataBlob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement('a');
|
||||||
link.href = downloadURL;
|
link.href = downloadURL;
|
||||||
link.download = `TRIP_${this.selectedGPX.name}.gpx`;
|
link.download = `TRIP_${this.selectedGPX.name}.gpx`;
|
||||||
link.click();
|
link.click();
|
||||||
@ -874,13 +791,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (remote_version) => {
|
next: (remote_version) => {
|
||||||
if (!remote_version)
|
if (!remote_version)
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('success', 'Latest version', "You're running the latest version of TRIP");
|
||||||
"success",
|
if (this.info && remote_version != this.info?.version) this.info.update = remote_version;
|
||||||
"Latest version",
|
|
||||||
"You're running the latest version of TRIP",
|
|
||||||
);
|
|
||||||
if (this.info && remote_version != this.info?.version)
|
|
||||||
this.info.update = remote_version;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,16 +8,16 @@
|
|||||||
<div class="mt-1 flex items-center">
|
<div class="mt-1 flex items-center">
|
||||||
<span
|
<span
|
||||||
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
|
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
|
||||||
trip.days.length }} {{ trip.days!.length > 1 ? 'days' : 'day'}}</span>
|
trip.days.length }} {{ trip.days!.length > 1 ? 'days' : 'day' }}</span>
|
||||||
<span
|
<span
|
||||||
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
|
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
|
||||||
(totalPrice | number:'1.0-2') || '-' }} {{ trip.currency }}</span>
|
(totalPrice | number: '1.0-2') || '-' }} {{ trip.currency }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden print:flex flex-col items-center">
|
<div class="hidden print:flex flex-col items-center">
|
||||||
<img src="favicon.png" class="size-20">
|
<img src="favicon.png" class="size-20" />
|
||||||
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
|
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 print:hidden">
|
<div class="flex items-center gap-2 print:hidden">
|
||||||
@ -44,9 +44,11 @@
|
|||||||
<p-button label="Expand" pTooltip="Expand table to full width" class="hidden lg:flex" icon="pi pi-arrows-h"
|
<p-button label="Expand" pTooltip="Expand table to full width" class="hidden lg:flex" icon="pi pi-arrows-h"
|
||||||
(click)="isExpanded = !isExpanded" text />
|
(click)="isExpanded = !isExpanded" text />
|
||||||
<p-button [label]="tableExpandableMode ? 'Ungroup' : 'Group'"
|
<p-button [label]="tableExpandableMode ? 'Ungroup' : 'Group'"
|
||||||
[pTooltip]="tableExpandableMode ? 'Switch table mode' : 'Switch table mode, allow column resizing'"
|
[pTooltip]="tableExpandableMode ? 'Switch table mode' : 'Switch table mode, allow column resizing'" [icon]="
|
||||||
[icon]="tableExpandableMode ? 'pi pi-arrow-up-right-and-arrow-down-left-from-center' : 'pi pi-arrow-down-left-and-arrow-up-right-to-center'"
|
tableExpandableMode
|
||||||
(click)="tableExpandableMode = !tableExpandableMode" text />
|
? 'pi pi-arrow-up-right-and-arrow-down-left-from-center'
|
||||||
|
: 'pi pi-arrow-down-left-and-arrow-up-right-to-center'
|
||||||
|
" (click)="tableExpandableMode = !tableExpandableMode" text />
|
||||||
<p-button label="GMaps" pTooltip="Open Google Maps directions" icon="pi pi-car" (click)="tripToNavigation()"
|
<p-button label="GMaps" pTooltip="Open Google Maps directions" icon="pi pi-car" (click)="tripToNavigation()"
|
||||||
text />
|
text />
|
||||||
<p-button label="Highlight" pTooltip="Show itinerary on map" icon="pi pi-directions"
|
<p-button label="Highlight" pTooltip="Show itinerary on map" icon="pi pi-directions"
|
||||||
@ -73,20 +75,37 @@
|
|||||||
@defer {
|
@defer {
|
||||||
@if (flattenedTripItems.length) {
|
@if (flattenedTripItems.length) {
|
||||||
<p-table [value]="flattenedTripItems" class="max-w-[85vw] md:max-w-full print-striped-rows"
|
<p-table [value]="flattenedTripItems" class="max-w-[85vw] md:max-w-full print-striped-rows"
|
||||||
[class.table-colored-resizer]="tableExpandableMode" [rowGroupMode]="tableExpandableMode ? 'subheader': 'rowspan'"
|
[class.table-colored-resizer]="tableExpandableMode" [rowGroupMode]="tableExpandableMode ? 'subheader' : 'rowspan'"
|
||||||
groupRowsBy="td_label" [resizableColumns]="tableExpandableMode">
|
groupRowsBy="td_label" [resizableColumns]="tableExpandableMode">
|
||||||
<ng-template #header>
|
<ng-template #header>
|
||||||
<tr>
|
<tr>
|
||||||
@if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {<th class="w-24" pResizableColumn>Day
|
@if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {
|
||||||
</th>}
|
<th class="w-24" pResizableColumn>Day</th>
|
||||||
@if (tripTableSelectedColumns.includes('time')) {<th class="w-12" pResizableColumn>Time</th>}
|
}
|
||||||
@if (tripTableSelectedColumns.includes('text')) {<th pResizableColumn>Text</th>}
|
@if (tripTableSelectedColumns.includes('time')) {
|
||||||
@if (tripTableSelectedColumns.includes('place')) {<th pResizableColumn>Place</th>}
|
<th class="w-12" pResizableColumn>Time</th>
|
||||||
@if (tripTableSelectedColumns.includes('comment')) {<th pResizableColumn>Comment</th>}
|
}
|
||||||
@if (tripTableSelectedColumns.includes('LatLng')) {<th class="w-12" pResizableColumn>LatLng</th>}
|
@if (tripTableSelectedColumns.includes('text')) {
|
||||||
@if (tripTableSelectedColumns.includes('price')) {<th class="w-12" pResizableColumn>Price</th>}
|
<th pResizableColumn>Text</th>
|
||||||
@if (tripTableSelectedColumns.includes('status')) {<th class="w-12" pResizableColumn>Status</th>}
|
}
|
||||||
@if (tripTableSelectedColumns.includes('distance')) {<th pResizableColumn>Distance (km)</th>}
|
@if (tripTableSelectedColumns.includes('place')) {
|
||||||
|
<th pResizableColumn>Place</th>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('comment')) {
|
||||||
|
<th pResizableColumn>Comment</th>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('LatLng')) {
|
||||||
|
<th class="w-12" pResizableColumn>LatLng</th>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('price')) {
|
||||||
|
<th class="w-12" pResizableColumn>Price</th>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('status')) {
|
||||||
|
<th class="w-12" pResizableColumn>Status</th>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('distance')) {
|
||||||
|
<th pResizableColumn>Distance (km)</th>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@if (tableExpandableMode) {
|
@if (tableExpandableMode) {
|
||||||
@ -95,9 +114,10 @@
|
|||||||
<td colspan="8">
|
<td colspan="8">
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
<button type="button" pButton pRipple [pRowToggler]="tripitem" text rounded plain
|
<button type="button" pButton pRipple [pRowToggler]="tripitem" text rounded plain
|
||||||
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'">
|
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
|
||||||
</button>
|
<span class="font-bold w-xs max-w-xs min-w-0 inline-block truncate">{{
|
||||||
<span class="font-bold w-xs max-w-xs min-w-0 inline-block truncate">{{ tripitem.td_label }}</span>
|
tripitem.td_label
|
||||||
|
}}</span>
|
||||||
|
|
||||||
<p-button class="ml-2" label="Highlight" pTooltip="Show itinerary on map" text icon="pi pi-directions"
|
<p-button class="ml-2" label="Highlight" pTooltip="Show itinerary on map" text icon="pi pi-directions"
|
||||||
[severity]="tripMapAntLayerDayID == tripitem.day_id ? 'help' : 'primary'"
|
[severity]="tripMapAntLayerDayID == tripitem.day_id ? 'help' : 'primary'"
|
||||||
@ -112,57 +132,87 @@
|
|||||||
<ng-template #expandedrow let-tripitem>
|
<ng-template #expandedrow let-tripitem>
|
||||||
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
||||||
(click)="onRowClick(tripitem)">
|
(click)="onRowClick(tripitem)">
|
||||||
@if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
|
@if (tripTableSelectedColumns.includes('time')) {
|
||||||
@if (tripTableSelectedColumns.includes('text')) {<td class="relative">
|
<td class="font-mono text-sm">{{ tripitem.time }}</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('text')) {
|
||||||
|
<td class="relative">
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
@if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
|
@if (tripitem.status) {
|
||||||
[style.background]="tripitem.status.color"></div>}
|
<div class="block absolute top-3 left-1.5 size-2 rounded-full" [style.background]="tripitem.status.color">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{{ tripitem.text }}
|
{{ tripitem.text }}
|
||||||
</div>
|
</div>
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('place')) {<td class="relative">
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('place')) {
|
||||||
|
<td class="relative">
|
||||||
@if (tripitem.place) {
|
@if (tripitem.place) {
|
||||||
<div class="ml-7 print:ml-0 truncate print:whitespace-normal">
|
<div class="ml-7 print:ml-0 truncate print:whitespace-normal">
|
||||||
<img [src]="tripitem.place.image || tripitem.place.category.image"
|
<img [src]="tripitem.place.image || tripitem.place.category.image"
|
||||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" /> {{
|
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" />
|
||||||
tripitem.place.name }}
|
{{ tripitem.place.name }}
|
||||||
</div>
|
</div>
|
||||||
} @else {-}
|
} @else {
|
||||||
</td>}
|
-
|
||||||
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('comment')) {
|
||||||
|
<td class="relative">
|
||||||
@if (tripitem.image) {
|
@if (tripitem.image) {
|
||||||
<div
|
<div
|
||||||
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||||
<img [src]="tripitem.image"
|
<img [src]="tripitem.image"
|
||||||
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover" /> {{
|
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover" />
|
||||||
tripitem.comment }}
|
{{ tripitem.comment }}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||||
{{ tripitem.comment || '-' }}
|
{{ tripitem.comment || '-' }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('LatLng')) {
|
||||||
|
<td class="font-mono text-sm">
|
||||||
<div class="print:max-w-full truncate">
|
<div class="print:max-w-full truncate">
|
||||||
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
|
@if (tripitem.lat) {
|
||||||
@else {-}
|
{{ tripitem.lat }}, {{ tripitem.lng }}
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
|
}
|
||||||
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
|
@if (tripTableSelectedColumns.includes('price')) {
|
||||||
tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</span>}</td>}
|
<td class="truncate">
|
||||||
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
|
@if (tripitem.price) {
|
||||||
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
|
<span class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ tripitem.price }}
|
||||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
@if (tripitem.price) {
|
||||||
tripitem.status.label }}</span>}</td>}
|
{{ trip.currency }}
|
||||||
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('status')) {
|
||||||
|
<td class="truncate">
|
||||||
|
@if (tripitem.status) {
|
||||||
|
<span [style.background]="tripitem.status.color + '1A'" [style.color]="tripitem.status.color"
|
||||||
|
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ tripitem.status.label }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('distance')) {
|
||||||
|
<td class="text-sm">
|
||||||
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
||||||
</td>}
|
</td>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
}
|
} @else {
|
||||||
@else {
|
|
||||||
<ng-template #body let-tripitem let-rowgroup="rowgroup" let-rowspan="rowspan">
|
<ng-template #body let-tripitem let-rowgroup="rowgroup" let-rowspan="rowspan">
|
||||||
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
|
||||||
(click)="onRowClick(tripitem)">
|
(click)="onRowClick(tripitem)">
|
||||||
@ -170,18 +220,25 @@
|
|||||||
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
|
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
|
||||||
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
|
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
|
||||||
(click)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
|
(click)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
|
||||||
<div class="truncate">{{tripitem.td_label }}</div>
|
<div class="truncate">{{ tripitem.td_label }}</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
|
@if (tripTableSelectedColumns.includes('time')) {
|
||||||
@if (tripTableSelectedColumns.includes('text')) {<td class="relative max-w-60">
|
<td class="font-mono text-sm">{{ tripitem.time }}</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('text')) {
|
||||||
|
<td class="relative max-w-60">
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
@if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
|
@if (tripitem.status) {
|
||||||
[style.background]="tripitem.status.color"></div>}
|
<div class="block absolute top-3 left-1.5 size-2 rounded-full" [style.background]="tripitem.status.color">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{{ tripitem.text }}
|
{{ tripitem.text }}
|
||||||
</div>
|
</div>
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('place')) {<td>
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('place')) {
|
||||||
|
<td>
|
||||||
@if (tripitem.place) {
|
@if (tripitem.place) {
|
||||||
<div [style.background]="tripitem.place.category.color + '1A'"
|
<div [style.background]="tripitem.place.category.color + '1A'"
|
||||||
class="inline-flex items-center gap-2 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
class="inline-flex items-center gap-2 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full">
|
||||||
@ -190,38 +247,62 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="text-sm truncate min-w-0">{{ tripitem.place.name }}</span>
|
<span class="text-sm truncate min-w-0">{{ tripitem.place.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
} @else {-}
|
} @else {
|
||||||
</td>}
|
-
|
||||||
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('comment')) {
|
||||||
|
<td class="relative">
|
||||||
@if (tripitem.image) {
|
@if (tripitem.image) {
|
||||||
<div
|
<div
|
||||||
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||||
<img [src]="tripitem.image"
|
<img [src]="tripitem.image"
|
||||||
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
|
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" />
|
||||||
tripitem.comment }}
|
{{ tripitem.comment }}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||||
{{ tripitem.comment || '-' }}
|
{{ tripitem.comment || '-' }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('LatLng')) {
|
||||||
|
<td class="font-mono text-sm">
|
||||||
<div class="max-w-20 print:max-w-full truncate">
|
<div class="max-w-20 print:max-w-full truncate">
|
||||||
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
|
@if (tripitem.lat) {
|
||||||
@else {-}
|
{{ tripitem.lat }}, {{ tripitem.lng }}
|
||||||
|
} @else {
|
||||||
|
-
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>}
|
</td>
|
||||||
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
|
}
|
||||||
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
|
@if (tripTableSelectedColumns.includes('price')) {
|
||||||
tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</span>}</td>}
|
<td class="truncate">
|
||||||
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
|
@if (tripitem.price) {
|
||||||
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
|
<span class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ tripitem.price }}
|
||||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
@if (tripitem.price) {
|
||||||
tripitem.status.label }}</span>}</td>}
|
{{ trip.currency }}
|
||||||
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('status')) {
|
||||||
|
<td class="truncate">
|
||||||
|
@if (tripitem.status) {
|
||||||
|
<span [style.background]="tripitem.status.color + '1A'" [style.color]="tripitem.status.color"
|
||||||
|
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ tripitem.status.label }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (tripTableSelectedColumns.includes('distance')) {
|
||||||
|
<td class="text-sm">
|
||||||
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
||||||
</td>}
|
</td>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
}
|
}
|
||||||
@ -234,9 +315,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">
|
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">No Trip</div>
|
||||||
No Trip
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
} @placeholder (minimum 0.4s) {
|
} @placeholder (minimum 0.4s) {
|
||||||
<div class="h-[400px] w-full">
|
<div class="h-[400px] w-full">
|
||||||
@ -257,8 +336,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2 w-full">
|
<div class="flex items-center justify-between gap-2 w-full">
|
||||||
<h1 class="text-start font-semibold mb-0 line-clamp-1 md:text-xl">{{ selectedItem.text }}
|
<h1 class="text-start font-semibold mb-0 line-clamp-1 md:text-xl">{{ selectedItem.text }}</h1>
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 flex-none">
|
<div class="flex items-center gap-2 flex-none">
|
||||||
@if (selectedItem.gpx) {
|
@if (selectedItem.gpx) {
|
||||||
@ -302,16 +380,23 @@
|
|||||||
<div class="rounded-md shadow p-4">
|
<div class="rounded-md shadow p-4">
|
||||||
<p class="font-bold mb-1 truncate">Latitude, Longitude</p>
|
<p class="font-bold mb-1 truncate">Latitude, Longitude</p>
|
||||||
<p class="text-sm text-gray-500 truncate cursor-copy"
|
<p class="text-sm text-gray-500 truncate cursor-copy"
|
||||||
[cdkCopyToClipboard]="selectedItem.lat + ',' + selectedItem.lng">{{
|
[cdkCopyToClipboard]="selectedItem.lat + ',' + selectedItem.lng">
|
||||||
selectedItem.lat }}, {{ selectedItem.lng }}</p>
|
{{ selectedItem.lat }}, {{ selectedItem.lng }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (selectedItem.price) {
|
@if (selectedItem.price) {
|
||||||
<div class="rounded-md shadow p-4">
|
<div class="rounded-md shadow p-4">
|
||||||
<p class="font-bold mb-1">Price</p>
|
<p class="font-bold mb-1">Price</p>
|
||||||
<p class="text-sm text-gray-500">{{ selectedItem.price }} @if (selectedItem.price) { {{ trip.currency }} }
|
<p class="text-sm text-gray-500">
|
||||||
@if (selectedItem.paid_by) {<span class="text-xs text-gray-500">(by {{ selectedItem.paid_by }})</span>}
|
{{ selectedItem.price }}
|
||||||
|
@if (selectedItem.price) {
|
||||||
|
{{ trip.currency }}
|
||||||
|
}
|
||||||
|
@if (selectedItem.paid_by) {
|
||||||
|
<span class="text-xs text-gray-500">(by {{ selectedItem.paid_by }})</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -319,9 +404,8 @@
|
|||||||
@if (selectedItem.status) {
|
@if (selectedItem.status) {
|
||||||
<div class="rounded-md shadow p-4">
|
<div class="rounded-md shadow p-4">
|
||||||
<p class="font-bold mb-1">Status</p>
|
<p class="font-bold mb-1">Status</p>
|
||||||
<span [style.background]="selectedItem.status.color+'1A'" [style.color]="selectedItem.status.color"
|
<span [style.background]="selectedItem.status.color + '1A'" [style.color]="selectedItem.status.color"
|
||||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ selectedItem.status.label }}</span>
|
||||||
selectedItem.status.label }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,8 +433,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="map" [class.fullscreen-map]="isMapFullscreen" class="w-full rounded-md min-h-96 h-1/3 max-h-full">
|
<div id="map" [class.fullscreen-map]="isMapFullscreen" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!selectedItem) {
|
@if (!selectedItem) {
|
||||||
@ -370,7 +453,8 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@defer {
|
@defer {
|
||||||
<span class="bg-blue-100 text-blue-800 text-sm me-2 px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{
|
<span class="bg-blue-100 text-blue-800 text-sm me-2 px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{
|
||||||
places.length }}</span>
|
places.length
|
||||||
|
}}</span>
|
||||||
} @placeholder (minimum 0.4s) {
|
} @placeholder (minimum 0.4s) {
|
||||||
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
|
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
|
||||||
}
|
}
|
||||||
@ -383,7 +467,7 @@
|
|||||||
@for (p of places; track p.id) {
|
@for (p of places; track p.id) {
|
||||||
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto dark:hover:bg-gray-800"
|
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto dark:hover:bg-gray-800"
|
||||||
(mouseenter)="placeHighlightMarker(p)" (mouseleave)="resetPlaceHighlightMarker()">
|
(mouseenter)="placeHighlightMarker(p)" (mouseleave)="resetPlaceHighlightMarker()">
|
||||||
<img [src]="p.image || p.category.image" class="w-12 rounded-full object-fit">
|
<img [src]="p.image || p.category.image" class="w-12 rounded-full object-fit" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 truncate">
|
<div class="flex flex-col gap-1 truncate">
|
||||||
<h1 class="tracking-tight truncate dark:text-surface-300">{{ p.name }}</h1>
|
<h1 class="tracking-tight truncate dark:text-surface-300">{{ p.name }}</h1>
|
||||||
@ -404,8 +488,11 @@
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{
|
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{
|
||||||
p.price || '-'
|
p.price || '-' }}
|
||||||
}} @if (p.price) { {{ trip.currency }} }</span>
|
@if (p.price) {
|
||||||
|
{{ trip.currency }}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
@if (trip.collaborators.length) {
|
@if (trip.collaborators.length) {
|
||||||
<span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user
|
<span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user
|
||||||
@ -419,7 +506,7 @@
|
|||||||
}
|
}
|
||||||
} @placeholder (minimum 0.4s) {
|
} @placeholder (minimum 0.4s) {
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@for (_ of [1,2,3]; track _) {
|
@for (_ of [1, 2, 3]; track _) {
|
||||||
<div class="h-16">
|
<div class="h-16">
|
||||||
<p-skeleton height="100%" />
|
<p-skeleton height="100%" />
|
||||||
</div>
|
</div>
|
||||||
@ -454,9 +541,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 flex-none">
|
<div class="flex items-center gap-2 flex-none">
|
||||||
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{
|
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{
|
||||||
getDayStats(d).price || '-' }} @if (getDayStats(d).price) { {{ trip.currency }} }</span>
|
getDayStats(d).price || '-' }}
|
||||||
|
@if (getDayStats(d).price) {
|
||||||
|
{{ trip.currency }}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<span class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{
|
<span class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{
|
||||||
getDayStats(d).places }}</span>
|
getDayStats(d).places
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
@ -474,16 +566,19 @@
|
|||||||
<div class="p-4 shadow rounded-md w-full min-h-20">
|
<div class="p-4 shadow rounded-md w-full min-h-20">
|
||||||
<div class="p-2 mb-2 flex justify-between items-center">
|
<div class="p-2 mb-2 flex justify-between items-center">
|
||||||
<h1 class="font-semibold tracking-tight text-xl">About</h1>
|
<h1 class="font-semibold tracking-tight text-xl">About</h1>
|
||||||
<div class="flex items-center gap-1 text-gray-500"><p-button text label="itskovacs/trip" icon="pi pi-github"
|
<div class="flex items-center gap-1 text-gray-500">
|
||||||
(click)="toGithub()" /></div>
|
<p-button text label="itskovacs/trip" icon="pi pi-github" (click)="toGithub()" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-[85%] text-center mx-auto flex flex-col items-center gap-4">
|
<div class="max-w-[85%] text-center mx-auto flex flex-col items-center gap-4">
|
||||||
<div>TRIP is free and always will be. Building it takes a lot of time and effort, and since I'm committed to
|
<div>
|
||||||
keeping it free, your support simply helps keep the project going. Thank you! ❤️</div>
|
TRIP is free and always will be. Building it takes a lot of time and effort, and since I'm committed to
|
||||||
|
keeping it free, your support simply helps keep the project going. Thank you! ❤️
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="https://ko-fi.com/itskovacs" target="_blank" class="custom-button flex items-center">Buy me
|
<a href="https://ko-fi.com/itskovacs" target="_blank" class="custom-button flex items-center">Buy me a
|
||||||
a coffee</a>
|
coffee</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -505,12 +600,17 @@
|
|||||||
[class.prettyprint]="isPrinting">
|
[class.prettyprint]="isPrinting">
|
||||||
@if (isMapFullscreenDays) {
|
@if (isMapFullscreenDays) {
|
||||||
<div animate.enter="expand" animate.leave="collapse" class="overflow-y-auto max-h-[calc(30vh-5rem)] p-2 space-y-1">
|
<div animate.enter="expand" animate.leave="collapse" class="overflow-y-auto max-h-[calc(30vh-5rem)] p-2 space-y-1">
|
||||||
@for(day of trip.days; track day.id) {
|
@for (day of trip.days; track day.id) {
|
||||||
<button (click)="toggleTripDayHighlight(day.id)"
|
<button (click)="toggleTripDayHighlight(day.id)" [ngClass]="
|
||||||
[ngClass]="tripMapAntLayerDayID === day.id ? 'shadow-md bg-blue-500' : 'hover:bg-blue-50 dark:hover:bg-blue-950/20'"
|
tripMapAntLayerDayID === day.id
|
||||||
class="w-full flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all">
|
? 'shadow-md bg-blue-500'
|
||||||
<span class="flex-shrink-0 w-9 h-9 flex items-center justify-center rounded-lg font-semibold text-sm"
|
: 'hover:bg-blue-50 dark:hover:bg-blue-950/20'
|
||||||
[ngClass]="tripMapAntLayerDayID === day.id ? 'bg-white text-blue-600' : 'bg-blue-100 text-blue-800 dark:bg-blue-900'">
|
" class="w-full flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all">
|
||||||
|
<span class="flex-shrink-0 w-9 h-9 flex items-center justify-center rounded-lg font-semibold text-sm" [ngClass]="
|
||||||
|
tripMapAntLayerDayID === day.id
|
||||||
|
? 'bg-white text-blue-600'
|
||||||
|
: 'bg-blue-100 text-blue-800 dark:bg-blue-900'
|
||||||
|
">
|
||||||
{{ day.items.length }}
|
{{ day.items.length }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 text-left text-sm font-medium truncate"
|
<span class="flex-1 text-left text-sm font-medium truncate"
|
||||||
@ -537,7 +637,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<img class="w-full lg:h-[32rem] h-80 md:h-96 rounded-lg object-cover" src="cover.webp">
|
<img class="w-full lg:h-[32rem] h-80 md:h-96 rounded-lg object-cover" src="cover.webp" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p-button text label="itskovacs/trip" icon="pi pi-github" (click)="toGithub()" />
|
<p-button text label="itskovacs/trip" icon="pi pi-github" (click)="toGithub()" />
|
||||||
@ -559,7 +659,9 @@
|
|||||||
class="flex items-center gap-2 w-full cursor-pointer">
|
class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
<p-checkbox disabled [binary]="true" [inputId]="item.id.toString()" [(ngModel)]="item.packed" />
|
<p-checkbox disabled [binary]="true" [inputId]="item.id.toString()" [(ngModel)]="item.packed" />
|
||||||
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
@if (item.qt) {<span class="text-gray-400 mr-0.5">{{ item.qt }}</span>}
|
@if (item.qt) {
|
||||||
|
<span class="text-gray-400 mr-0.5">{{ item.qt }}</span>
|
||||||
|
}
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@ -593,8 +695,10 @@
|
|||||||
<div class="flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5">
|
<div class="flex items-center gap-3 rounded-md p-2 hover:bg-gray-100 dark:hover:bg-white/5">
|
||||||
<label [for]="item.id" class="flex items-center gap-2 w-full">
|
<label [for]="item.id" class="flex items-center gap-2 w-full">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full"
|
@if (item.status) {
|
||||||
[style.background]="item.status.color"></div>}
|
<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full" [style.background]="item.status.color">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<p-checkbox disabled />
|
<p-checkbox disabled />
|
||||||
</div>
|
</div>
|
||||||
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
<div class="pr-6 md:pr-0 truncate select-none flex-1">
|
||||||
@ -612,7 +716,7 @@
|
|||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<div class="flex flex-col items-center max-w-[55vw] md:max-w-full">
|
<div class="flex flex-col items-center max-w-[55vw] md:max-w-full">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<img src="favicon.png" class="size-20">
|
<img src="favicon.png" class="size-20" />
|
||||||
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
|
<div class="flex gap-2 items-center text-xs text-gray-500"><i class="pi pi-github"></i>itskovacs/trip</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -620,13 +724,11 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span
|
<span
|
||||||
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-1 rounded min-w-fit flex items-center gap-2"><i
|
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-1 rounded min-w-fit flex items-center gap-2"><i
|
||||||
class="pi pi-calendar text-xs"></i> {{
|
class="pi pi-calendar text-xs"></i> {{ trip?.days?.length }}
|
||||||
trip?.days?.length }} {{ trip?.days!.length > 1 ? 'days' :
|
{{ (trip?.days)!.length > 1 ? 'days' : 'day' }}</span>
|
||||||
'day'}}</span>
|
|
||||||
<span
|
<span
|
||||||
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-1 rounded min-w-fit flex items-center gap-2"><i
|
class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-1 rounded min-w-fit flex items-center gap-2"><i
|
||||||
class="pi pi-wallet text-xs"></i> {{
|
class="pi pi-wallet text-xs"></i> {{ (totalPrice | number: '1.0-2') || '-' }} {{ trip?.currency }}</span>
|
||||||
(totalPrice | number:'1.0-2') || '-' }} {{ trip?.currency }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -635,8 +737,7 @@
|
|||||||
<div class="text-2xl font-semibold">Notes</div>
|
<div class="text-2xl font-semibold">Notes</div>
|
||||||
|
|
||||||
<div class="mt-4 border-l-3 border-gray-900 pl-6 py-2">
|
<div class="mt-4 border-l-3 border-gray-900 pl-6 py-2">
|
||||||
<p class="text-sm leading-relaxed text-gray-800 whitespace-pre-line">{{ trip?.notes || 'Nothing there.'
|
<p class="text-sm leading-relaxed text-gray-800 whitespace-pre-line">{{ trip?.notes || 'Nothing there.' }}</p>
|
||||||
}}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -680,9 +781,7 @@
|
|||||||
@if (item.status) {
|
@if (item.status) {
|
||||||
<span class="text-xs font-medium px-2.5 py-1 rounded min-w-fit"
|
<span class="text-xs font-medium px-2.5 py-1 rounded min-w-fit"
|
||||||
[style.background]="statusToTripStatus(item.status)?.color + '1A'"
|
[style.background]="statusToTripStatus(item.status)?.color + '1A'"
|
||||||
[style.color]="statusToTripStatus(item.status)?.color">{{
|
[style.color]="statusToTripStatus(item.status)?.color">{{ statusToTripStatus(item.status)?.label }}</span>
|
||||||
statusToTripStatus(item.status)?.label
|
|
||||||
}}</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -700,7 +799,7 @@
|
|||||||
<div class="text-2xl font-semibold">📍 Places</div>
|
<div class="text-2xl font-semibold">📍 Places</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@for(place of trip?.places; track place.id) {
|
@for (place of trip?.places; track place.id) {
|
||||||
<div class="inline-flex items-center gap-2 bg-gray-100 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full m-1">
|
<div class="inline-flex items-center gap-2 bg-gray-100 text-gray-800 font-medium px-1 py-1 pr-3 rounded-full m-1">
|
||||||
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
<div class="size-6 flex items-center justify-center bg-white rounded-full overflow-hidden flex-shrink-0">
|
||||||
<img [src]="place.image" class="size-full object-cover" />
|
<img [src]="place.image" class="size-full object-cover" />
|
||||||
|
|||||||
@ -1,47 +1,33 @@
|
|||||||
import { AfterViewInit, Component } from "@angular/core";
|
import { AfterViewInit, Component } from '@angular/core';
|
||||||
import { ApiService } from "../../services/api.service";
|
import { ApiService } from '../../services/api.service';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import * as L from "leaflet";
|
import * as L from 'leaflet';
|
||||||
import { antPath } from "leaflet-ant-path";
|
import { antPath } from 'leaflet-ant-path';
|
||||||
import { TableModule } from "primeng/table";
|
import { TableModule } from 'primeng/table';
|
||||||
import {
|
import { Trip, FlattenedTripItem, TripDay, TripItem, TripStatus, PackingItem, ChecklistItem } from '../../types/trip';
|
||||||
Trip,
|
import { Place } from '../../types/poi';
|
||||||
FlattenedTripItem,
|
import { createMap, placeToMarker, createClusterGroup, tripDayMarker, gpxToPolyline } from '../../shared/map';
|
||||||
TripDay,
|
import { ActivatedRoute } from '@angular/router';
|
||||||
TripItem,
|
import { debounceTime, take, tap } from 'rxjs';
|
||||||
TripStatus,
|
import { UtilsService } from '../../services/utils.service';
|
||||||
PackingItem,
|
import { CommonModule, DecimalPipe } from '@angular/common';
|
||||||
ChecklistItem,
|
import { MenuItem } from 'primeng/api';
|
||||||
} from "../../types/trip";
|
import { MenuModule } from 'primeng/menu';
|
||||||
import { Place } from "../../types/poi";
|
import { LinkifyPipe } from '../../shared/linkify.pipe';
|
||||||
import {
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
createMap,
|
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
placeToMarker,
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
createClusterGroup,
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
tripDayMarker,
|
import { DialogModule } from 'primeng/dialog';
|
||||||
gpxToPolyline,
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
} from "../../shared/map";
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { debounceTime, take, tap } from "rxjs";
|
import { calculateDistanceBetween } from '../../shared/haversine';
|
||||||
import { UtilsService } from "../../services/utils.service";
|
import { orderByPipe } from '../../shared/order-by.pipe';
|
||||||
import { CommonModule, DecimalPipe } from "@angular/common";
|
|
||||||
import { MenuItem } from "primeng/api";
|
|
||||||
import { MenuModule } from "primeng/menu";
|
|
||||||
import { LinkifyPipe } from "../../shared/linkify.pipe";
|
|
||||||
import { TooltipModule } from "primeng/tooltip";
|
|
||||||
import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
||||||
import { MultiSelectModule } from "primeng/multiselect";
|
|
||||||
import { DialogModule } from "primeng/dialog";
|
|
||||||
import { CheckboxModule } from "primeng/checkbox";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { ClipboardModule } from "@angular/cdk/clipboard";
|
|
||||||
import { calculateDistanceBetween } from "../../shared/haversine";
|
|
||||||
import { orderByPipe } from "../../shared/order-by.pipe";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-shared-trip",
|
selector: 'app-shared-trip',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -61,8 +47,8 @@ import { orderByPipe } from "../../shared/order-by.pipe";
|
|||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
orderByPipe,
|
orderByPipe,
|
||||||
],
|
],
|
||||||
templateUrl: "./shared-trip.component.html",
|
templateUrl: './shared-trip.component.html',
|
||||||
styleUrls: ["./shared-trip.component.scss"],
|
styleUrls: ['./shared-trip.component.scss'],
|
||||||
})
|
})
|
||||||
export class SharedTripComponent implements AfterViewInit {
|
export class SharedTripComponent implements AfterViewInit {
|
||||||
token?: string;
|
token?: string;
|
||||||
@ -98,18 +84,18 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
readonly menuTripActionsItems: MenuItem[] = [
|
readonly menuTripActionsItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
label: "Lists",
|
label: 'Lists',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Checklist",
|
label: 'Checklist',
|
||||||
icon: "pi pi-check-square",
|
icon: 'pi pi-check-square',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.openChecklist();
|
this.openChecklist();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Packing",
|
label: 'Packing',
|
||||||
icon: "pi pi-briefcase",
|
icon: 'pi pi-briefcase',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.openPackingList();
|
this.openPackingList();
|
||||||
},
|
},
|
||||||
@ -117,11 +103,11 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Trip",
|
label: 'Trip',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Pretty Print",
|
label: 'Pretty Print',
|
||||||
icon: "pi pi-print",
|
icon: 'pi pi-print',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.togglePrint();
|
this.togglePrint();
|
||||||
},
|
},
|
||||||
@ -131,11 +117,11 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
];
|
];
|
||||||
readonly menuTripTableActionsItems: MenuItem[] = [
|
readonly menuTripTableActionsItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
label: "Actions",
|
label: 'Actions',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Pretty Print",
|
label: 'Pretty Print',
|
||||||
icon: "pi pi-print",
|
icon: 'pi pi-print',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.togglePrint();
|
this.togglePrint();
|
||||||
},
|
},
|
||||||
@ -143,18 +129,18 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Table",
|
label: 'Table',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Filter",
|
label: 'Filter',
|
||||||
icon: "pi pi-filter",
|
icon: 'pi pi-filter',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.toggleFiltering();
|
this.toggleFiltering();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Group",
|
label: 'Group',
|
||||||
icon: "pi pi-arrow-down-left-and-arrow-up-right-to-center",
|
icon: 'pi pi-arrow-down-left-and-arrow-up-right-to-center',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.tableExpandableMode = !this.tableExpandableMode;
|
this.tableExpandableMode = !this.tableExpandableMode;
|
||||||
},
|
},
|
||||||
@ -162,18 +148,18 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Directions",
|
label: 'Directions',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: "Highlight",
|
label: 'Highlight',
|
||||||
icon: "pi pi-directions",
|
icon: 'pi pi-directions',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.toggleTripDaysHighlight();
|
this.toggleTripDaysHighlight();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "GMaps itinerary",
|
label: 'GMaps itinerary',
|
||||||
icon: "pi pi-car",
|
icon: 'pi pi-car',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.tripToNavigation();
|
this.tripToNavigation();
|
||||||
},
|
},
|
||||||
@ -182,24 +168,18 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
readonly tripTableColumns: string[] = [
|
readonly tripTableColumns: string[] = [
|
||||||
"day",
|
'day',
|
||||||
"time",
|
'time',
|
||||||
"text",
|
'text',
|
||||||
"place",
|
'place',
|
||||||
"comment",
|
'comment',
|
||||||
"LatLng",
|
'LatLng',
|
||||||
"price",
|
'price',
|
||||||
"status",
|
'status',
|
||||||
"distance",
|
'distance',
|
||||||
];
|
];
|
||||||
tripTableSelectedColumns: string[] = [
|
tripTableSelectedColumns: string[] = ['day', 'time', 'text', 'place', 'comment'];
|
||||||
"day",
|
tripTableSearchInput = new FormControl('');
|
||||||
"time",
|
|
||||||
"text",
|
|
||||||
"place",
|
|
||||||
"comment",
|
|
||||||
];
|
|
||||||
tripTableSearchInput = new FormControl("");
|
|
||||||
|
|
||||||
dayStatsCache = new Map<number, { price: number; places: number }>();
|
dayStatsCache = new Map<number, { price: number; places: number }>();
|
||||||
placesUsedInTable = new Set<number>();
|
placesUsedInTable = new Set<number>();
|
||||||
@ -210,14 +190,12 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
this.statuses = this.utilsService.statuses;
|
this.statuses = this.utilsService.statuses;
|
||||||
this.tripTableSearchInput.valueChanges
|
this.tripTableSearchInput.valueChanges.pipe(debounceTime(300), takeUntilDestroyed()).subscribe({
|
||||||
.pipe(debounceTime(300), takeUntilDestroyed())
|
next: (value) => {
|
||||||
.subscribe({
|
if (value) this.flattenTripDayItems(value.toLowerCase());
|
||||||
next: (value) => {
|
else this.flattenTripDayItems();
|
||||||
if (value) this.flattenTripDayItems(value.toLowerCase());
|
},
|
||||||
else this.flattenTripDayItems();
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
@ -225,7 +203,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
tap((params) => {
|
tap((params) => {
|
||||||
const token = params.get("token");
|
const token = params.get('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.loadTripData(token);
|
this.loadTripData(token);
|
||||||
@ -252,12 +230,10 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
initMap(): void {
|
initMap(): void {
|
||||||
const contentMenuItems = [
|
const contentMenuItems = [
|
||||||
{
|
{
|
||||||
text: "Copy coordinates",
|
text: 'Copy coordinates',
|
||||||
callback: (e: any) => {
|
callback: (e: any) => {
|
||||||
const latlng = e.latlng;
|
const latlng = e.latlng;
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(`${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`);
|
||||||
`${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -305,11 +281,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
if (!this.trip?.days) return [];
|
if (!this.trip?.days) return [];
|
||||||
|
|
||||||
return this.trip.days
|
return this.trip.days
|
||||||
.flatMap((day) =>
|
.flatMap((day) => day.items.filter((item) => ['constraint', 'pending'].includes(item.status as string)))
|
||||||
day.items.filter((item) =>
|
|
||||||
["constraint", "pending"].includes(item.status as string),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
status: this.statusToTripStatus(item.status as string),
|
status: this.statusToTripStatus(item.status as string),
|
||||||
@ -383,9 +355,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
setPlacesAndMarkers() {
|
setPlacesAndMarkers() {
|
||||||
this.computePlacesUsedInTable();
|
this.computePlacesUsedInTable();
|
||||||
this.places = [...(this.trip?.places ?? [])].sort((a, b) =>
|
this.places = [...(this.trip?.places ?? [])].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
this.markerClusterGroup?.clearLayers();
|
this.markerClusterGroup?.clearLayers();
|
||||||
this.places.forEach((p) => {
|
this.places.forEach((p) => {
|
||||||
const marker = this._placeToMarker(p);
|
const marker = this._placeToMarker(p);
|
||||||
@ -394,12 +364,8 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_placeToMarker(place: Place): L.Marker {
|
_placeToMarker(place: Place): L.Marker {
|
||||||
const marker = placeToMarker(
|
const marker = placeToMarker(place, false, !this.placesUsedInTable.has(place.id));
|
||||||
place,
|
marker.on('click', () => {
|
||||||
false,
|
|
||||||
!this.placesUsedInTable.has(place.id),
|
|
||||||
);
|
|
||||||
marker.on("click", () => {
|
|
||||||
this.onMapMarkerClick(place.id);
|
this.onMapMarkerClick(place.id);
|
||||||
marker.closeTooltip();
|
marker.closeTooltip();
|
||||||
});
|
});
|
||||||
@ -409,9 +375,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
resetMapBounds() {
|
resetMapBounds() {
|
||||||
if (!this.places.length) {
|
if (!this.places.length) {
|
||||||
this.map?.fitBounds(
|
this.map?.fitBounds(
|
||||||
this.flattenedTripItems
|
this.flattenedTripItems.filter((i) => i.lat != null && i.lng != null).map((i) => [i.lat!, i.lng!]),
|
||||||
.filter((i) => i.lat != null && i.lng != null)
|
|
||||||
.map((i) => [i.lat!, i.lng!]),
|
|
||||||
{ padding: [30, 30] },
|
{ padding: [30, 30] },
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -425,7 +389,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
toggleMapFullscreen() {
|
toggleMapFullscreen() {
|
||||||
this.isMapFullscreen = !this.isMapFullscreen;
|
this.isMapFullscreen = !this.isMapFullscreen;
|
||||||
document.body.classList.toggle("overflow-hidden");
|
document.body.classList.toggle('overflow-hidden');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.map?.invalidateSize();
|
this.map?.invalidateSize();
|
||||||
@ -444,14 +408,12 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.totalPrice =
|
this.totalPrice =
|
||||||
this.trip?.days
|
this.trip?.days.flatMap((d) => d.items).reduce((price, item) => price + (item.price ?? 0), 0) ?? 0;
|
||||||
.flatMap((d) => d.items)
|
|
||||||
.reduce((price, item) => price + (item.price ?? 0), 0) ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPlaceHighlightMarker() {
|
resetPlaceHighlightMarker() {
|
||||||
if (this.tripMapHoveredElement) {
|
if (this.tripMapHoveredElement) {
|
||||||
this.tripMapHoveredElement.classList.remove("list-hover");
|
this.tripMapHoveredElement.classList.remove('list-hover');
|
||||||
this.tripMapHoveredElement = undefined;
|
this.tripMapHoveredElement = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,8 +430,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
placeHighlightMarker(item: any) {
|
placeHighlightMarker(item: any) {
|
||||||
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker)
|
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker) this.resetPlaceHighlightMarker();
|
||||||
this.resetPlaceHighlightMarker();
|
|
||||||
|
|
||||||
let marker: L.Marker | undefined;
|
let marker: L.Marker | undefined;
|
||||||
this.markerClusterGroup?.eachLayer((layer: any) => {
|
this.markerClusterGroup?.eachLayer((layer: any) => {
|
||||||
@ -487,10 +448,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
// TripItem without place, but latlng
|
// TripItem without place, but latlng
|
||||||
this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!);
|
this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!);
|
||||||
if (this.tripMapGpxLayer) {
|
if (this.tripMapGpxLayer) {
|
||||||
this.map?.fitBounds(
|
this.map?.fitBounds([[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()], { padding: [30, 30] });
|
||||||
[[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()],
|
|
||||||
{ padding: [30, 30] },
|
|
||||||
);
|
|
||||||
} else this.map?.fitBounds([[item.lat, item.lng]], { padding: [60, 60] });
|
} else this.map?.fitBounds([[item.lat, item.lng]], { padding: [60, 60] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -499,18 +457,16 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
const markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster
|
const markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster
|
||||||
if (markerElement) {
|
if (markerElement) {
|
||||||
// marker, not clustered
|
// marker, not clustered
|
||||||
markerElement.classList.add("list-hover");
|
markerElement.classList.add('list-hover');
|
||||||
this.tripMapHoveredElement = markerElement;
|
this.tripMapHoveredElement = markerElement;
|
||||||
targetLatLng = marker.getLatLng();
|
targetLatLng = marker.getLatLng();
|
||||||
} else {
|
} else {
|
||||||
// marker is clustered
|
// marker is clustered
|
||||||
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
|
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(marker);
|
||||||
marker,
|
|
||||||
);
|
|
||||||
if (parentCluster) {
|
if (parentCluster) {
|
||||||
const clusterEl = parentCluster.getElement();
|
const clusterEl = parentCluster.getElement();
|
||||||
if (clusterEl) {
|
if (clusterEl) {
|
||||||
clusterEl.classList.add("list-hover");
|
clusterEl.classList.add('list-hover');
|
||||||
this.tripMapHoveredElement = clusterEl;
|
this.tripMapHoveredElement = clusterEl;
|
||||||
}
|
}
|
||||||
targetLatLng = parentCluster.getLatLng();
|
targetLatLng = parentCluster.getLatLng();
|
||||||
@ -574,11 +530,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
.filter((n) => n !== undefined);
|
.filter((n) => n !== undefined);
|
||||||
|
|
||||||
if (items.length < 2) {
|
if (items.length < 2) {
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('info', 'Info', 'Not enough values to map an itinerary');
|
||||||
"info",
|
|
||||||
"Info",
|
|
||||||
"Not enough values to map an itinerary",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,20 +542,20 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
const layGroup = L.featureGroup();
|
const layGroup = L.featureGroup();
|
||||||
const COLORS: string[] = [
|
const COLORS: string[] = [
|
||||||
"#e6194b",
|
'#e6194b',
|
||||||
"#3cb44b",
|
'#3cb44b',
|
||||||
"#ffe119",
|
'#ffe119',
|
||||||
"#4363d8",
|
'#4363d8',
|
||||||
"#9a6324",
|
'#9a6324',
|
||||||
"#f58231",
|
'#f58231',
|
||||||
"#911eb4",
|
'#911eb4',
|
||||||
"#46f0f0",
|
'#46f0f0',
|
||||||
"#f032e6",
|
'#f032e6',
|
||||||
"#bcf60c",
|
'#bcf60c',
|
||||||
"#fabebe",
|
'#fabebe',
|
||||||
"#008080",
|
'#008080',
|
||||||
"#e6beff",
|
'#e6beff',
|
||||||
"#808000",
|
'#808000',
|
||||||
];
|
];
|
||||||
let prevPoint: [number, number] | null = null;
|
let prevPoint: [number, number] | null = null;
|
||||||
|
|
||||||
@ -614,7 +566,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
dashArray: [10, 20],
|
dashArray: [10, 20],
|
||||||
weight: 5,
|
weight: 5,
|
||||||
color: COLORS[idx % COLORS.length],
|
color: COLORS[idx % COLORS.length],
|
||||||
pulseColor: "#FFFFFF",
|
pulseColor: '#FFFFFF',
|
||||||
paused: false,
|
paused: false,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
hardwareAccelerated: true,
|
hardwareAccelerated: true,
|
||||||
@ -665,9 +617,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
|
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
|
||||||
if (!this.trip || idx === undefined || idx == -1) return;
|
if (!this.trip || idx === undefined || idx == -1) return;
|
||||||
const data = this.trip.days[idx].items.sort((a, b) =>
|
const data = this.trip.days[idx].items.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
|
||||||
a.time < b.time ? -1 : a.time > b.time ? 1 : 0,
|
|
||||||
);
|
|
||||||
const items = data
|
const items = data
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.lat && item.lng)
|
if (item.lat && item.lng)
|
||||||
@ -693,11 +643,7 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
.filter((n) => n !== undefined);
|
.filter((n) => n !== undefined);
|
||||||
|
|
||||||
if (items.length < 2) {
|
if (items.length < 2) {
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('info', 'Info', 'Not enough values to map an itinerary');
|
||||||
"info",
|
|
||||||
"Info",
|
|
||||||
"Not enough values to map an itinerary",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -712,8 +658,8 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
delay: 400,
|
delay: 400,
|
||||||
dashArray: [10, 20],
|
dashArray: [10, 20],
|
||||||
weight: 5,
|
weight: 5,
|
||||||
color: "#0000FF",
|
color: '#0000FF',
|
||||||
pulseColor: "#FFFFFF",
|
pulseColor: '#FFFFFF',
|
||||||
paused: false,
|
paused: false,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
hardwareAccelerated: true,
|
hardwareAccelerated: true,
|
||||||
@ -751,15 +697,9 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMapMarkerClick(place_id: number) {
|
onMapMarkerClick(place_id: number) {
|
||||||
const item = this.flattenedTripItems.find(
|
const item = this.flattenedTripItems.find((i) => i.place && i.place.id == place_id);
|
||||||
(i) => i.place && i.place.id == place_id,
|
|
||||||
);
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('info', 'Place not used', 'The place is not used in the table');
|
||||||
"info",
|
|
||||||
"Place not used",
|
|
||||||
"The place is not used in the table",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,14 +713,14 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
// TODO: More services
|
// TODO: More services
|
||||||
// const url = `http://maps.apple.com/?daddr=${this.selectedItem.lat},${this.selectedItem.lng}`;
|
// const url = `http://maps.apple.com/?daddr=${this.selectedItem.lat},${this.selectedItem.lng}`;
|
||||||
const url = `https://www.google.com/maps/dir/?api=1&destination=${this.selectedItem.lat},${this.selectedItem.lng}`;
|
const url = `https://www.google.com/maps/dir/?api=1&destination=${this.selectedItem.lat},${this.selectedItem.lng}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadItemGPX() {
|
downloadItemGPX() {
|
||||||
if (!this.selectedItem?.gpx) return;
|
if (!this.selectedItem?.gpx) return;
|
||||||
const dataBlob = new Blob([this.selectedItem.gpx]);
|
const dataBlob = new Blob([this.selectedItem.gpx]);
|
||||||
const downloadURL = URL.createObjectURL(dataBlob);
|
const downloadURL = URL.createObjectURL(dataBlob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement('a');
|
||||||
link.href = downloadURL;
|
link.href = downloadURL;
|
||||||
link.download = `TRIP_${this.trip?.name}_${this.selectedItem.text}.gpx`;
|
link.download = `TRIP_${this.trip?.name}_${this.selectedItem.text}.gpx`;
|
||||||
link.click();
|
link.click();
|
||||||
@ -791,27 +731,23 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
tripDayToNavigation(day_id: number) {
|
tripDayToNavigation(day_id: number) {
|
||||||
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
|
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
|
||||||
if (!this.trip || idx === undefined || idx == -1) return;
|
if (!this.trip || idx === undefined || idx == -1) return;
|
||||||
const data = this.trip.days[idx].items.sort((a, b) =>
|
const data = this.trip.days[idx].items.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
|
||||||
a.time < b.time ? -1 : a.time > b.time ? 1 : 0,
|
|
||||||
);
|
|
||||||
const items = data.filter((item) => item.lat && item.lng);
|
const items = data.filter((item) => item.lat && item.lng);
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
|
|
||||||
const waypoints = items.map((item) => `${item.lat},${item.lng}`).join("/");
|
const waypoints = items.map((item) => `${item.lat},${item.lng}`).join('/');
|
||||||
const url = `https://www.google.com/maps/dir/${waypoints}`;
|
const url = `https://www.google.com/maps/dir/${waypoints}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
tripToNavigation() {
|
tripToNavigation() {
|
||||||
// TODO: More services
|
// TODO: More services
|
||||||
const items = this.flattenedTripItems.filter(
|
const items = this.flattenedTripItems.filter((item) => item.lat && item.lng);
|
||||||
(item) => item.lat && item.lng,
|
|
||||||
);
|
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
|
|
||||||
const waypoints = items.map((item) => `${item.lat},${item.lng}`).join("/");
|
const waypoints = items.map((item) => `${item.lat},${item.lng}`).join('/');
|
||||||
const url = `https://www.google.com/maps/dir/${waypoints}`;
|
const url = `https://www.google.com/maps/dir/${waypoints}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPackingList() {
|
openPackingList() {
|
||||||
@ -832,24 +768,13 @@ export class SharedTripComponent implements AfterViewInit {
|
|||||||
|
|
||||||
computeDispPackingList() {
|
computeDispPackingList() {
|
||||||
const sorted: PackingItem[] = [...this.packingList].sort((a, b) =>
|
const sorted: PackingItem[] = [...this.packingList].sort((a, b) =>
|
||||||
a.packed !== b.packed
|
a.packed !== b.packed ? (a.packed ? 1 : -1) : a.text < b.text ? -1 : a.text > b.text ? 1 : 0,
|
||||||
? a.packed
|
|
||||||
? 1
|
|
||||||
: -1
|
|
||||||
: a.text < b.text
|
|
||||||
? -1
|
|
||||||
: a.text > b.text
|
|
||||||
? 1
|
|
||||||
: 0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.dispPackingList = sorted.reduce<Record<string, PackingItem[]>>(
|
this.dispPackingList = sorted.reduce<Record<string, PackingItem[]>>((acc, item) => {
|
||||||
(acc, item) => {
|
(acc[item.category] ??= []).push(item);
|
||||||
(acc[item.category] ??= []).push(item);
|
return acc;
|
||||||
return acc;
|
}, {});
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openChecklist() {
|
openChecklist() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,14 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ApiService } from "../../services/api.service";
|
import { ApiService } from '../../services/api.service';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import { TripBase, TripInvitation } from "../../types/trip";
|
import { TripBase, TripInvitation } from '../../types/trip';
|
||||||
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
|
import { TripCreateModalComponent } from '../../modals/trip-create-modal/trip-create-modal.component';
|
||||||
import { Router } from "@angular/router";
|
import { Router } from '@angular/router';
|
||||||
import { forkJoin, take } from "rxjs";
|
import { forkJoin, take } from 'rxjs';
|
||||||
import { DatePipe } from "@angular/common";
|
import { DatePipe } from '@angular/common';
|
||||||
import { DialogModule } from "primeng/dialog";
|
import { DialogModule } from 'primeng/dialog';
|
||||||
|
|
||||||
interface TripBaseWithDates extends TripBase {
|
interface TripBaseWithDates extends TripBase {
|
||||||
from?: Date;
|
from?: Date;
|
||||||
@ -16,11 +16,11 @@ interface TripBaseWithDates extends TripBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trips",
|
selector: 'app-trips',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SkeletonModule, ButtonModule, DialogModule, DatePipe],
|
imports: [SkeletonModule, ButtonModule, DialogModule, DatePipe],
|
||||||
templateUrl: "./trips.component.html",
|
templateUrl: './trips.component.html',
|
||||||
styleUrls: ["./trips.component.scss"],
|
styleUrls: ['./trips.component.scss'],
|
||||||
})
|
})
|
||||||
export class TripsComponent implements OnInit {
|
export class TripsComponent implements OnInit {
|
||||||
trips: TripBase[] = [];
|
trips: TripBase[] = [];
|
||||||
@ -53,7 +53,7 @@ export class TripsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gotoMap() {
|
gotoMap() {
|
||||||
this.router.navigateByUrl("/");
|
this.router.navigateByUrl('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
viewTrip(id: number) {
|
viewTrip(id: number) {
|
||||||
@ -61,20 +61,17 @@ export class TripsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addTrip() {
|
addTrip() {
|
||||||
const modal: DynamicDialogRef = this.dialogService.open(
|
const modal: DynamicDialogRef = this.dialogService.open(TripCreateModalComponent, {
|
||||||
TripCreateModalComponent,
|
header: 'Create Trip',
|
||||||
{
|
modal: true,
|
||||||
header: "Create Trip",
|
appendTo: 'body',
|
||||||
modal: true,
|
closable: true,
|
||||||
appendTo: "body",
|
dismissableMask: true,
|
||||||
closable: true,
|
width: '50vw',
|
||||||
dismissableMask: true,
|
breakpoints: {
|
||||||
width: "50vw",
|
'960px': '80vw',
|
||||||
breakpoints: {
|
|
||||||
"960px": "80vw",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)!;
|
})!;
|
||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (trip: TripBaseWithDates | null) => {
|
next: (trip: TripBaseWithDates | null) => {
|
||||||
@ -85,12 +82,8 @@ export class TripsComponent implements OnInit {
|
|||||||
let dayCount = 0;
|
let dayCount = 0;
|
||||||
|
|
||||||
if (trip.from && trip.to) {
|
if (trip.from && trip.to) {
|
||||||
const obs$ = this.generateDaysLabel(trip.from!, trip.to!).map(
|
const obs$ = this.generateDaysLabel(trip.from!, trip.to!).map((label) =>
|
||||||
(label) =>
|
this.apiService.postTripDay({ id: -1, label: label, items: [] }, new_trip.id),
|
||||||
this.apiService.postTripDay(
|
|
||||||
{ id: -1, label: label, items: [] },
|
|
||||||
new_trip.id,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
dayCount = obs$.length;
|
dayCount = obs$.length;
|
||||||
forkJoin(obs$).pipe(take(1)).subscribe();
|
forkJoin(obs$).pipe(take(1)).subscribe();
|
||||||
@ -116,32 +109,17 @@ export class TripsComponent implements OnInit {
|
|||||||
|
|
||||||
generateDaysLabel(from: Date, to: Date): string[] {
|
generateDaysLabel(from: Date, to: Date): string[] {
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
const sameMonth =
|
const sameMonth = from.getFullYear() === to.getFullYear() && from.getMonth() === to.getMonth();
|
||||||
from.getFullYear() === to.getFullYear() &&
|
|
||||||
from.getMonth() === to.getMonth();
|
|
||||||
|
|
||||||
const months = [
|
const months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
|
||||||
"Jan.",
|
|
||||||
"Feb.",
|
|
||||||
"Mar.",
|
|
||||||
"Apr.",
|
|
||||||
"May.",
|
|
||||||
"Jun.",
|
|
||||||
"Jul.",
|
|
||||||
"Aug.",
|
|
||||||
"Sep.",
|
|
||||||
"Oct.",
|
|
||||||
"Nov.",
|
|
||||||
"Dec.",
|
|
||||||
];
|
|
||||||
|
|
||||||
const current = new Date(from);
|
const current = new Date(from);
|
||||||
while (current <= to) {
|
while (current <= to) {
|
||||||
let label = "";
|
let label = '';
|
||||||
if (sameMonth) {
|
if (sameMonth) {
|
||||||
label = `${current.getDate().toString().padStart(2, "0")} ${months[current.getMonth()]}`;
|
label = `${current.getDate().toString().padStart(2, '0')} ${months[current.getMonth()]}`;
|
||||||
} else {
|
} else {
|
||||||
label = `${(current.getMonth() + 1).toString().padStart(2, "0")}/${current.getDate().toString().padStart(2, "0")}`;
|
label = `${(current.getMonth() + 1).toString().padStart(2, '0')}/${current.getDate().toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
current.setDate(current.getDate() + 1);
|
current.setDate(current.getDate() + 1);
|
||||||
|
|||||||
2
src/src/app/decs.d.ts
vendored
2
src/src/app/decs.d.ts
vendored
@ -1 +1 @@
|
|||||||
declare module "leaflet-ant-path";
|
declare module 'leaflet-ant-path';
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-batch-create-modal",
|
selector: 'app-batch-create-modal',
|
||||||
imports: [FloatLabelModule, ButtonModule, ReactiveFormsModule, TextareaModule],
|
imports: [FloatLabelModule, ButtonModule, ReactiveFormsModule, TextareaModule],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./batch-create-modal.component.html",
|
templateUrl: './batch-create-modal.component.html',
|
||||||
styleUrl: "./batch-create-modal.component.scss",
|
styleUrl: './batch-create-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class BatchCreateModalComponent {
|
export class BatchCreateModalComponent {
|
||||||
batchInput = new FormControl("");
|
batchInput = new FormControl('');
|
||||||
|
|
||||||
constructor(private ref: DynamicDialogRef) {}
|
constructor(private ref: DynamicDialogRef) {}
|
||||||
|
|
||||||
|
|||||||
@ -1,31 +1,19 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ColorPickerModule } from 'primeng/colorpicker';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { Category } from '../../types/poi';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
|
||||||
import { ColorPickerModule } from "primeng/colorpicker";
|
|
||||||
import { Category } from "../../types/poi";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-category-create-modal",
|
selector: 'app-category-create-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ColorPickerModule, ReactiveFormsModule, FocusTrapModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
|
||||||
ColorPickerModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FocusTrapModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./category-create-modal.component.html",
|
templateUrl: './category-create-modal.component.html',
|
||||||
styleUrl: "./category-create-modal.component.scss",
|
styleUrl: './category-create-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class CategoryCreateModalComponent {
|
export class CategoryCreateModalComponent {
|
||||||
categoryForm: FormGroup;
|
categoryForm: FormGroup;
|
||||||
@ -38,14 +26,11 @@ export class CategoryCreateModalComponent {
|
|||||||
) {
|
) {
|
||||||
this.categoryForm = this.fb.group({
|
this.categoryForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
name: ["", Validators.required],
|
name: ['', Validators.required],
|
||||||
color: [
|
color: [
|
||||||
"#000000",
|
'#000000',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [Validators.required, Validators.pattern('\#[abcdefABCDEF0-9]{6}')],
|
||||||
Validators.required,
|
|
||||||
Validators.pattern("\#[abcdefABCDEF0-9]{6}"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: null,
|
image: null,
|
||||||
@ -58,8 +43,8 @@ export class CategoryCreateModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.categoryForm.value;
|
let ret = this.categoryForm.value;
|
||||||
if (!ret["name"]) return;
|
if (!ret['name']) return;
|
||||||
if (!this.updatedImage) delete ret["image"];
|
if (!this.updatedImage) delete ret['image'];
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,8 +55,8 @@ export class CategoryCreateModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
this.categoryForm.get("image")?.setValue(e.target?.result as string);
|
this.categoryForm.get('image')?.setValue(e.target?.result as string);
|
||||||
this.categoryForm.get("image")?.markAsDirty();
|
this.categoryForm.get('image')?.markAsDirty();
|
||||||
this.updatedImage = true;
|
this.updatedImage = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,7 +65,7 @@ export class CategoryCreateModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearImage() {
|
clearImage() {
|
||||||
this.categoryForm.get("image")?.setValue(null);
|
this.categoryForm.get('image')?.setValue(null);
|
||||||
this.updatedImage = false;
|
this.updatedImage = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,27 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { SelectModule } from 'primeng/select';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { Observable } from 'rxjs';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { AsyncPipe } from '@angular/common';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { InputGroupModule } from 'primeng/inputgroup';
|
||||||
import { SelectModule } from "primeng/select";
|
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { ApiService } from '../../services/api.service';
|
||||||
import { Observable } from "rxjs";
|
import { UtilsService } from '../../services/utils.service';
|
||||||
import { AsyncPipe } from "@angular/common";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
import { InputGroupModule } from "primeng/inputgroup";
|
import { Category, Place } from '../../types/poi';
|
||||||
import { InputGroupAddonModule } from "primeng/inputgroupaddon";
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
import { ApiService } from "../../services/api.service";
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { UtilsService } from "../../services/utils.service";
|
import { checkAndParseLatLng, formatLatLng } from '../../shared/latlng-parser';
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
import { InputNumberModule } from 'primeng/inputnumber';
|
||||||
import { Category, Place } from "../../types/poi";
|
|
||||||
import { CheckboxModule } from "primeng/checkbox";
|
|
||||||
import { TooltipModule } from "primeng/tooltip";
|
|
||||||
import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser";
|
|
||||||
import { InputNumberModule } from "primeng/inputnumber";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-place-create-modal",
|
selector: 'app-place-create-modal',
|
||||||
imports: [
|
imports: [
|
||||||
FloatLabelModule,
|
FloatLabelModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
@ -43,8 +38,8 @@ import { InputNumberModule } from "primeng/inputnumber";
|
|||||||
FocusTrapModule,
|
FocusTrapModule,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./place-create-modal.component.html",
|
templateUrl: './place-create-modal.component.html',
|
||||||
styleUrl: "./place-create-modal.component.scss",
|
styleUrl: './place-create-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class PlaceCreateModalComponent {
|
export class PlaceCreateModalComponent {
|
||||||
placeForm: FormGroup;
|
placeForm: FormGroup;
|
||||||
@ -66,32 +61,27 @@ export class PlaceCreateModalComponent {
|
|||||||
|
|
||||||
this.placeForm = this.fb.group({
|
this.placeForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
name: ["", Validators.required],
|
name: ['', Validators.required],
|
||||||
place: ["", { validators: Validators.required, updateOn: "blur" }],
|
place: ['', { validators: Validators.required, updateOn: 'blur' }],
|
||||||
lat: [
|
lat: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [Validators.required, Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)')],
|
||||||
Validators.required,
|
updateOn: 'blur',
|
||||||
Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
|
|
||||||
],
|
|
||||||
updateOn: "blur",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lng: [
|
lng: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.pattern(
|
Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
|
||||||
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
category: [null, Validators.required],
|
category: [null, Validators.required],
|
||||||
description: null,
|
description: null,
|
||||||
duration: [null, Validators.pattern("\\d+")],
|
duration: [null, Validators.pattern('\\d+')],
|
||||||
price: null,
|
price: null,
|
||||||
allowdog: false,
|
allowdog: false,
|
||||||
visited: false,
|
visited: false,
|
||||||
@ -104,12 +94,11 @@ export class PlaceCreateModalComponent {
|
|||||||
if (patchValue) this.placeForm.patchValue(patchValue);
|
if (patchValue) this.placeForm.patchValue(patchValue);
|
||||||
|
|
||||||
this.placeForm
|
this.placeForm
|
||||||
.get("place")
|
.get('place')
|
||||||
?.valueChanges.pipe(takeUntilDestroyed())
|
?.valueChanges.pipe(takeUntilDestroyed())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (value: string) => {
|
next: (value: string) => {
|
||||||
const isGoogleMapsURL =
|
const isGoogleMapsURL = /^(https?:\/\/)?(www\.)?google\.[a-z.]+\/maps/.test(value);
|
||||||
/^(https?:\/\/)?(www\.)?google\.[a-z.]+\/maps/.test(value);
|
|
||||||
if (isGoogleMapsURL) {
|
if (isGoogleMapsURL) {
|
||||||
this.parseGoogleMapsUrl(value);
|
this.parseGoogleMapsUrl(value);
|
||||||
}
|
}
|
||||||
@ -117,7 +106,7 @@ export class PlaceCreateModalComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.placeForm
|
this.placeForm
|
||||||
.get("lat")
|
.get('lat')
|
||||||
?.valueChanges.pipe(takeUntilDestroyed())
|
?.valueChanges.pipe(takeUntilDestroyed())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (value: string) => {
|
next: (value: string) => {
|
||||||
@ -125,8 +114,8 @@ export class PlaceCreateModalComponent {
|
|||||||
if (!result) return;
|
if (!result) return;
|
||||||
const [lat, lng] = result;
|
const [lat, lng] = result;
|
||||||
|
|
||||||
const latControl = this.placeForm.get("lat");
|
const latControl = this.placeForm.get('lat');
|
||||||
const lngControl = this.placeForm.get("lng");
|
const lngControl = this.placeForm.get('lng');
|
||||||
|
|
||||||
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
|
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
|
||||||
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
|
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
|
||||||
@ -140,15 +129,15 @@ export class PlaceCreateModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.placeForm.value;
|
let ret = this.placeForm.value;
|
||||||
ret["category_id"] = ret["category"];
|
ret['category_id'] = ret['category'];
|
||||||
delete ret["category"];
|
delete ret['category'];
|
||||||
if (ret["image_id"]) {
|
if (ret['image_id']) {
|
||||||
delete ret["image"];
|
delete ret['image'];
|
||||||
delete ret["image_id"];
|
delete ret['image_id'];
|
||||||
}
|
}
|
||||||
if (ret["gpx"] == "1") delete ret["gpx"];
|
if (ret['gpx'] == '1') delete ret['gpx'];
|
||||||
ret["lat"] = +ret["lat"];
|
ret['lat'] = +ret['lat'];
|
||||||
ret["lng"] = +ret["lng"];
|
ret['lng'] = +ret['lng'];
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,13 +145,12 @@ export class PlaceCreateModalComponent {
|
|||||||
const [place, latlng] = this.utilsService.parseGoogleMapsUrl(url);
|
const [place, latlng] = this.utilsService.parseGoogleMapsUrl(url);
|
||||||
if (!place || !latlng) return;
|
if (!place || !latlng) return;
|
||||||
|
|
||||||
const [lat, lng] = latlng.split(",");
|
const [lat, lng] = latlng.split(',');
|
||||||
this.placeForm.get("place")?.setValue(place);
|
this.placeForm.get('place')?.setValue(place);
|
||||||
this.placeForm.get("lat")?.setValue(lat);
|
this.placeForm.get('lat')?.setValue(lat);
|
||||||
this.placeForm.get("lng")?.setValue(lng);
|
this.placeForm.get('lng')?.setValue(lng);
|
||||||
|
|
||||||
if (!this.placeForm.get("name")?.value)
|
if (!this.placeForm.get('name')?.value) this.placeForm.get('name')?.setValue(place);
|
||||||
this.placeForm.get("name")?.setValue(place);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageSelected(event: Event) {
|
onImageSelected(event: Event) {
|
||||||
@ -172,14 +160,14 @@ export class PlaceCreateModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (this.placeForm.get("image_id")?.value) {
|
if (this.placeForm.get('image_id')?.value) {
|
||||||
this.previous_image_id = this.placeForm.get("image_id")?.value;
|
this.previous_image_id = this.placeForm.get('image_id')?.value;
|
||||||
this.previous_image = this.placeForm.get("image")?.value;
|
this.previous_image = this.placeForm.get('image')?.value;
|
||||||
this.placeForm.get("image_id")?.setValue(null);
|
this.placeForm.get('image_id')?.setValue(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.placeForm.get("image")?.setValue(e.target?.result as string);
|
this.placeForm.get('image')?.setValue(e.target?.result as string);
|
||||||
this.placeForm.get("image")?.markAsDirty();
|
this.placeForm.get('image')?.markAsDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -187,12 +175,12 @@ export class PlaceCreateModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearImage() {
|
clearImage() {
|
||||||
this.placeForm.get("image")?.setValue(null);
|
this.placeForm.get('image')?.setValue(null);
|
||||||
this.placeForm.get("image_id")?.setValue(null);
|
this.placeForm.get('image_id')?.setValue(null);
|
||||||
|
|
||||||
if (this.previous_image && this.previous_image_id) {
|
if (this.previous_image && this.previous_image_id) {
|
||||||
this.placeForm.get("image_id")?.setValue(this.previous_image_id);
|
this.placeForm.get('image_id')?.setValue(this.previous_image_id);
|
||||||
this.placeForm.get("image")?.setValue(this.previous_image);
|
this.placeForm.get('image')?.setValue(this.previous_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,8 +191,8 @@ export class PlaceCreateModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
this.placeForm.get("gpx")?.setValue(e.target?.result as string);
|
this.placeForm.get('gpx')?.setValue(e.target?.result as string);
|
||||||
this.placeForm.get("gpx")?.markAsDirty();
|
this.placeForm.get('gpx')?.markAsDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@ -212,7 +200,7 @@ export class PlaceCreateModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearGPX() {
|
clearGPX() {
|
||||||
this.placeForm.get("gpx")?.setValue(null);
|
this.placeForm.get('gpx')?.setValue(null);
|
||||||
this.placeForm.get("gpx")?.markAsDirty();
|
this.placeForm.get('gpx')?.markAsDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,20 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { Trip } from "../../types/trip";
|
import { Trip } from '../../types/trip';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-archive-modal",
|
selector: 'app-trip-archive-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, TextareaModule, ButtonModule, ReactiveFormsModule],
|
||||||
FloatLabelModule,
|
|
||||||
TextareaModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-archive-modal.component.html",
|
templateUrl: './trip-archive-modal.component.html',
|
||||||
styleUrl: "./trip-archive-modal.component.scss",
|
styleUrl: './trip-archive-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripArchiveModalComponent {
|
export class TripArchiveModalComponent {
|
||||||
review = new FormControl("");
|
review = new FormControl('');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private ref: DynamicDialogRef,
|
private ref: DynamicDialogRef,
|
||||||
@ -34,16 +29,13 @@ export class TripArchiveModalComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!trip.days.length) return;
|
if (!trip.days.length) return;
|
||||||
let placeholder = "General feedback:\n\n";
|
let placeholder = 'General feedback:\n\n';
|
||||||
trip.days.forEach((day, index) => {
|
trip.days.forEach((day, index) => {
|
||||||
placeholder += `\nDay ${index + 1} (${day.label})\n`;
|
placeholder += `\nDay ${index + 1} (${day.label})\n`;
|
||||||
if (!day.items.length) placeholder += " No activities.\n";
|
if (!day.items.length) placeholder += ' No activities.\n';
|
||||||
else
|
else day.items.forEach((item) => (placeholder += ` - ${item.time} | ${item.text}\n`));
|
||||||
day.items.forEach(
|
|
||||||
(item) => (placeholder += ` - ${item.time} | ${item.text}\n`),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
placeholder += "\nAnything else?";
|
placeholder += '\nAnything else?';
|
||||||
this.review.setValue(placeholder);
|
this.review.setValue(placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,17 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
import { ButtonModule } from "primeng/button";
|
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-checklist-modal",
|
selector: 'app-trip-create-checklist-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FocusTrapModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-checklist-modal.component.html",
|
templateUrl: './trip-create-checklist-modal.component.html',
|
||||||
styleUrl: "./trip-create-checklist-modal.component.scss",
|
styleUrl: './trip-create-checklist-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreateChecklistModalComponent {
|
export class TripCreateChecklistModalComponent {
|
||||||
checklistForm: FormGroup;
|
checklistForm: FormGroup;
|
||||||
@ -33,7 +22,7 @@ export class TripCreateChecklistModalComponent {
|
|||||||
) {
|
) {
|
||||||
this.checklistForm = this.fb.group({
|
this.checklistForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
text: ["", { validators: Validators.required }],
|
text: ['', { validators: Validators.required }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const patchValue = this.config.data?.packing;
|
const patchValue = this.config.data?.packing;
|
||||||
|
|||||||
@ -1,30 +1,25 @@
|
|||||||
import { Component, ViewChild } from "@angular/core";
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { TripDay, TripMember, TripStatus } from '../../types/trip';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { Place } from '../../types/poi';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { SelectModule } from 'primeng/select';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { TripDay, TripMember, TripStatus } from "../../types/trip";
|
import { UtilsService } from '../../services/utils.service';
|
||||||
import { Place } from "../../types/poi";
|
import { checkAndParseLatLng, formatLatLng } from '../../shared/latlng-parser';
|
||||||
import { SelectModule } from "primeng/select";
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { InputNumberModule } from 'primeng/inputnumber';
|
||||||
import { InputMaskModule } from "primeng/inputmask";
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
import { UtilsService } from "../../services/utils.service";
|
import { InputGroupModule } from 'primeng/inputgroup';
|
||||||
import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser";
|
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { Popover, PopoverModule } from 'primeng/popover';
|
||||||
import { InputNumberModule } from "primeng/inputnumber";
|
|
||||||
import { MultiSelectModule } from "primeng/multiselect";
|
|
||||||
import { InputGroupModule } from "primeng/inputgroup";
|
|
||||||
import { InputGroupAddonModule } from "primeng/inputgroupaddon";
|
|
||||||
import { Popover, PopoverModule } from "primeng/popover";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-day-item-modal",
|
selector: 'app-trip-create-day-item-modal',
|
||||||
imports: [
|
imports: [
|
||||||
FloatLabelModule,
|
FloatLabelModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
@ -44,11 +39,11 @@ import { Popover, PopoverModule } from "primeng/popover";
|
|||||||
PopoverModule,
|
PopoverModule,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-day-item-modal.component.html",
|
templateUrl: './trip-create-day-item-modal.component.html',
|
||||||
styleUrl: "./trip-create-day-item-modal.component.scss",
|
styleUrl: './trip-create-day-item-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreateDayItemModalComponent {
|
export class TripCreateDayItemModalComponent {
|
||||||
@ViewChild("op") op!: Popover;
|
@ViewChild('op') op!: Popover;
|
||||||
members: TripMember[] = [];
|
members: TripMember[] = [];
|
||||||
itemForm: FormGroup;
|
itemForm: FormGroup;
|
||||||
days: TripDay[] = [];
|
days: TripDay[] = [];
|
||||||
@ -68,16 +63,13 @@ export class TripCreateDayItemModalComponent {
|
|||||||
this.itemForm = this.fb.group({
|
this.itemForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
time: [
|
time: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [Validators.required, Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/)],
|
||||||
Validators.required,
|
|
||||||
Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
text: ["", Validators.required],
|
text: ['', Validators.required],
|
||||||
comment: "",
|
comment: '',
|
||||||
day_id: [null, Validators.required],
|
day_id: [null, Validators.required],
|
||||||
place: null,
|
place: null,
|
||||||
status: null,
|
status: null,
|
||||||
@ -86,18 +78,16 @@ export class TripCreateDayItemModalComponent {
|
|||||||
image_id: null,
|
image_id: null,
|
||||||
gpx: null,
|
gpx: null,
|
||||||
lat: [
|
lat: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
|
validators: Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)'),
|
||||||
updateOn: "blur",
|
updateOn: 'blur',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lng: [
|
lng: [
|
||||||
"",
|
'',
|
||||||
{
|
{
|
||||||
validators: Validators.pattern(
|
validators: Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
|
||||||
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
paid_by: null,
|
paid_by: null,
|
||||||
@ -115,36 +105,34 @@ export class TripCreateDayItemModalComponent {
|
|||||||
place: data.item.place?.id ?? null,
|
place: data.item.place?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.selectedDay)
|
if (data.selectedDay) this.itemForm.get('day_id')?.setValue([data.selectedDay]);
|
||||||
this.itemForm.get("day_id")?.setValue([data.selectedDay]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.itemForm
|
this.itemForm
|
||||||
.get("place")
|
.get('place')
|
||||||
?.valueChanges.pipe(takeUntilDestroyed())
|
?.valueChanges.pipe(takeUntilDestroyed())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (value?: number) => {
|
next: (value?: number) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
this.itemForm.get("lat")?.setValue("");
|
this.itemForm.get('lat')?.setValue('');
|
||||||
this.itemForm.get("lng")?.setValue("");
|
this.itemForm.get('lng')?.setValue('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const p: Place = this.places.find((p) => p.id === value) as Place;
|
const p: Place = this.places.find((p) => p.id === value) as Place;
|
||||||
if (p) {
|
if (p) {
|
||||||
this.itemForm.get("lat")?.setValue(p.lat);
|
this.itemForm.get('lat')?.setValue(p.lat);
|
||||||
this.itemForm.get("lng")?.setValue(p.lng);
|
this.itemForm.get('lng')?.setValue(p.lng);
|
||||||
this.itemForm.get("price")?.setValue(p.price || 0);
|
this.itemForm.get('price')?.setValue(p.price || 0);
|
||||||
if (!this.itemForm.get("text")?.value)
|
if (!this.itemForm.get('text')?.value) this.itemForm.get('text')?.setValue(p.name);
|
||||||
this.itemForm.get("text")?.setValue(p.name);
|
if (p.description && !this.itemForm.get('comment')?.value)
|
||||||
if (p.description && !this.itemForm.get("comment")?.value)
|
this.itemForm.get('comment')?.setValue(p.description);
|
||||||
this.itemForm.get("comment")?.setValue(p.description);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.itemForm
|
this.itemForm
|
||||||
.get("lat")
|
.get('lat')
|
||||||
?.valueChanges.pipe(takeUntilDestroyed())
|
?.valueChanges.pipe(takeUntilDestroyed())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (value: string) => {
|
next: (value: string) => {
|
||||||
@ -152,8 +140,8 @@ export class TripCreateDayItemModalComponent {
|
|||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
const [lat, lng] = result;
|
const [lat, lng] = result;
|
||||||
const latControl = this.itemForm.get("lat");
|
const latControl = this.itemForm.get('lat');
|
||||||
const lngControl = this.itemForm.get("lng");
|
const lngControl = this.itemForm.get('lng');
|
||||||
|
|
||||||
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
|
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
|
||||||
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
|
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
|
||||||
@ -167,16 +155,16 @@ export class TripCreateDayItemModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.itemForm.value;
|
let ret = this.itemForm.value;
|
||||||
if (!ret["lat"]) {
|
if (!ret['lat']) {
|
||||||
ret["lat"] = null;
|
ret['lat'] = null;
|
||||||
ret["lng"] = null;
|
ret['lng'] = null;
|
||||||
}
|
}
|
||||||
if (ret["image_id"]) {
|
if (ret['image_id']) {
|
||||||
delete ret["image"];
|
delete ret['image'];
|
||||||
delete ret["image_id"];
|
delete ret['image_id'];
|
||||||
}
|
}
|
||||||
if (ret["gpx"] == "1") delete ret["gpx"];
|
if (ret['gpx'] == '1') delete ret['gpx'];
|
||||||
if (!ret["place"]) delete ret["place"];
|
if (!ret['place']) delete ret['place'];
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +173,7 @@ export class TripCreateDayItemModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get paidByControl(): any {
|
get paidByControl(): any {
|
||||||
return this.itemForm.get("paid_by");
|
return this.itemForm.get('paid_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPriceMember(member: any) {
|
selectPriceMember(member: any) {
|
||||||
@ -206,14 +194,14 @@ export class TripCreateDayItemModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (this.itemForm.get("image_id")?.value) {
|
if (this.itemForm.get('image_id')?.value) {
|
||||||
this.previous_image_id = this.itemForm.get("image_id")?.value;
|
this.previous_image_id = this.itemForm.get('image_id')?.value;
|
||||||
this.previous_image = this.itemForm.get("image")?.value;
|
this.previous_image = this.itemForm.get('image')?.value;
|
||||||
this.itemForm.get("image_id")?.setValue(null);
|
this.itemForm.get('image_id')?.setValue(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.itemForm.get("image")?.setValue(e.target?.result as string);
|
this.itemForm.get('image')?.setValue(e.target?.result as string);
|
||||||
this.itemForm.get("image")?.markAsDirty();
|
this.itemForm.get('image')?.markAsDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -221,13 +209,13 @@ export class TripCreateDayItemModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearImage() {
|
clearImage() {
|
||||||
this.itemForm.get("image")?.setValue(null);
|
this.itemForm.get('image')?.setValue(null);
|
||||||
this.itemForm.get("image_id")?.setValue(null);
|
this.itemForm.get('image_id')?.setValue(null);
|
||||||
this.itemForm.markAsDirty();
|
this.itemForm.markAsDirty();
|
||||||
|
|
||||||
if (this.previous_image && this.previous_image_id) {
|
if (this.previous_image && this.previous_image_id) {
|
||||||
this.itemForm.get("image_id")?.setValue(this.previous_image_id);
|
this.itemForm.get('image_id')?.setValue(this.previous_image_id);
|
||||||
this.itemForm.get("image")?.setValue(this.previous_image);
|
this.itemForm.get('image')?.setValue(this.previous_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,8 +226,8 @@ export class TripCreateDayItemModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
this.itemForm.get("gpx")?.setValue(e.target?.result as string);
|
this.itemForm.get('gpx')?.setValue(e.target?.result as string);
|
||||||
this.itemForm.get("gpx")?.markAsDirty();
|
this.itemForm.get('gpx')?.markAsDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@ -247,7 +235,7 @@ export class TripCreateDayItemModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearGPX() {
|
clearGPX() {
|
||||||
this.itemForm.get("gpx")?.setValue(null);
|
this.itemForm.get('gpx')?.setValue(null);
|
||||||
this.itemForm.get("gpx")?.markAsDirty();
|
this.itemForm.get('gpx')?.markAsDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,17 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { TripDay } from '../../types/trip';
|
||||||
import { ButtonModule } from "primeng/button";
|
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { TripDay } from "../../types/trip";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-day-modal",
|
selector: 'app-trip-create-day-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-day-modal.component.html",
|
templateUrl: './trip-create-day-modal.component.html',
|
||||||
styleUrl: "./trip-create-day-modal.component.scss",
|
styleUrl: './trip-create-day-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreateDayModalComponent {
|
export class TripCreateDayModalComponent {
|
||||||
dayForm: FormGroup;
|
dayForm: FormGroup;
|
||||||
@ -34,7 +24,7 @@ export class TripCreateDayModalComponent {
|
|||||||
) {
|
) {
|
||||||
this.dayForm = this.fb.group({
|
this.dayForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
label: ["", Validators.required],
|
label: ['', Validators.required],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.config.data) {
|
if (this.config.data) {
|
||||||
@ -46,7 +36,7 @@ export class TripCreateDayModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.dayForm.value;
|
let ret = this.dayForm.value;
|
||||||
if (!ret["label"]) return;
|
if (!ret['label']) return;
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { TripDay, TripItem } from "../../types/trip";
|
import { TripDay, TripItem } from '../../types/trip';
|
||||||
import { SelectModule } from "primeng/select";
|
import { SelectModule } from 'primeng/select';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-items-modal",
|
selector: 'app-trip-create-items-modal',
|
||||||
imports: [FloatLabelModule, ButtonModule, SelectModule, ReactiveFormsModule, TextareaModule],
|
imports: [FloatLabelModule, ButtonModule, SelectModule, ReactiveFormsModule, TextareaModule],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-items-modal.component.html",
|
templateUrl: './trip-create-items-modal.component.html',
|
||||||
styleUrl: "./trip-create-items-modal.component.scss",
|
styleUrl: './trip-create-items-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreateItemsModalComponent {
|
export class TripCreateItemsModalComponent {
|
||||||
itemBatchForm: FormGroup;
|
itemBatchForm: FormGroup;
|
||||||
pholder = "eg.\n14h Just an item example\n15:10 Another format for an item\n16h30 Another item here";
|
pholder = 'eg.\n14h Just an item example\n15:10 Another format for an item\n16h30 Another item here';
|
||||||
days: TripDay[] = [];
|
days: TripDay[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private ref: DynamicDialogRef,
|
private ref: DynamicDialogRef,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private config: DynamicDialogConfig
|
private config: DynamicDialogConfig,
|
||||||
) {
|
) {
|
||||||
this.itemBatchForm = this.fb.group({
|
this.itemBatchForm = this.fb.group({
|
||||||
batch: ["", Validators.required],
|
batch: ['', Validators.required],
|
||||||
day_id: [null, Validators.required],
|
day_id: [null, Validators.required],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,15 +37,15 @@ export class TripCreateItemsModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
const ret = this.itemBatchForm.value;
|
const ret = this.itemBatchForm.value;
|
||||||
const day_id = ret.day_id;
|
const day_id = ret.day_id;
|
||||||
const lines: string[] = ret.batch.trim().split("\n");
|
const lines: string[] = ret.batch.trim().split('\n');
|
||||||
const tripItems: Partial<TripItem>[] = [];
|
const tripItems: Partial<TripItem>[] = [];
|
||||||
|
|
||||||
lines.forEach((l) => {
|
lines.forEach((l) => {
|
||||||
const match = l.match(/^(\d{1,2})(?:h|:)?(\d{0,2})?\s+(.+)$/);
|
const match = l.match(/^(\d{1,2})(?:h|:)?(\d{0,2})?\s+(.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const [_, hoursStr, minutesStr = "", text] = match;
|
const [_, hoursStr, minutesStr = '', text] = match;
|
||||||
const hours = hoursStr.padStart(2, "0");
|
const hours = hoursStr.padStart(2, '0');
|
||||||
const minutes = minutesStr.padStart(2, "0") || "00";
|
const minutes = minutesStr.padStart(2, '0') || '00';
|
||||||
const time = `${hours}:${minutes}`;
|
const time = `${hours}:${minutes}`;
|
||||||
tripItems.push({ time: time, text: text, day_id: day_id });
|
tripItems.push({ time: time, text: text, day_id: day_id });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,18 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { DatePickerModule } from 'primeng/datepicker';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
|
||||||
import { DatePickerModule } from "primeng/datepicker";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-modal",
|
selector: 'app-trip-create-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, DatePickerModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
DatePickerModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FocusTrapModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-modal.component.html",
|
templateUrl: './trip-create-modal.component.html',
|
||||||
styleUrl: "./trip-create-modal.component.scss",
|
styleUrl: './trip-create-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreateModalComponent {
|
export class TripCreateModalComponent {
|
||||||
tripForm: FormGroup;
|
tripForm: FormGroup;
|
||||||
@ -38,8 +26,8 @@ export class TripCreateModalComponent {
|
|||||||
) {
|
) {
|
||||||
this.tripForm = this.fb.group({
|
this.tripForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
name: ["", Validators.required],
|
name: ['', Validators.required],
|
||||||
image: "",
|
image: '',
|
||||||
currency: null,
|
currency: null,
|
||||||
image_id: null,
|
image_id: null,
|
||||||
from: null,
|
from: null,
|
||||||
@ -48,7 +36,7 @@ export class TripCreateModalComponent {
|
|||||||
|
|
||||||
const patchValue = this.config.data?.trip;
|
const patchValue = this.config.data?.trip;
|
||||||
if (patchValue) {
|
if (patchValue) {
|
||||||
if (!patchValue.image_id) delete patchValue["image"];
|
if (!patchValue.image_id) delete patchValue['image'];
|
||||||
this.tripForm.patchValue(patchValue);
|
this.tripForm.patchValue(patchValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,10 +44,10 @@ export class TripCreateModalComponent {
|
|||||||
closeDialog() {
|
closeDialog() {
|
||||||
// Normalize data for API POST
|
// Normalize data for API POST
|
||||||
let ret = this.tripForm.value;
|
let ret = this.tripForm.value;
|
||||||
if (!ret["name"]) return;
|
if (!ret['name']) return;
|
||||||
if (ret["image_id"]) {
|
if (ret['image_id']) {
|
||||||
delete ret["image"];
|
delete ret['image'];
|
||||||
delete ret["image_id"];
|
delete ret['image_id'];
|
||||||
}
|
}
|
||||||
this.ref.close(ret);
|
this.ref.close(ret);
|
||||||
}
|
}
|
||||||
@ -71,14 +59,14 @@ export class TripCreateModalComponent {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (this.tripForm.get("image_id")?.value) {
|
if (this.tripForm.get('image_id')?.value) {
|
||||||
this.previous_image_id = this.tripForm.get("image_id")?.value;
|
this.previous_image_id = this.tripForm.get('image_id')?.value;
|
||||||
this.previous_image = this.tripForm.get("image")?.value;
|
this.previous_image = this.tripForm.get('image')?.value;
|
||||||
this.tripForm.get("image_id")?.setValue(null);
|
this.tripForm.get('image_id')?.setValue(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tripForm.get("image")?.setValue(e.target?.result as string);
|
this.tripForm.get('image')?.setValue(e.target?.result as string);
|
||||||
this.tripForm.get("image")?.markAsDirty();
|
this.tripForm.get('image')?.markAsDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -86,11 +74,11 @@ export class TripCreateModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearImage() {
|
clearImage() {
|
||||||
this.tripForm.get("image")?.setValue(null);
|
this.tripForm.get('image')?.setValue(null);
|
||||||
|
|
||||||
if (this.previous_image && this.previous_image_id) {
|
if (this.previous_image && this.previous_image_id) {
|
||||||
this.tripForm.get("image_id")?.setValue(this.previous_image_id);
|
this.tripForm.get('image_id')?.setValue(this.previous_image_id);
|
||||||
this.tripForm.get("image")?.setValue(this.previous_image);
|
this.tripForm.get('image')?.setValue(this.previous_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import {
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
FormBuilder,
|
import { ButtonModule } from 'primeng/button';
|
||||||
FormGroup,
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
ReactiveFormsModule,
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
Validators,
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
} from "@angular/forms";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { SelectModule } from 'primeng/select';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { InputNumberModule } from 'primeng/inputnumber';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
|
||||||
import { SelectModule } from "primeng/select";
|
|
||||||
import { InputNumberModule } from "primeng/inputnumber";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-create-packing-modal",
|
selector: 'app-trip-create-packing-modal',
|
||||||
imports: [
|
imports: [
|
||||||
FloatLabelModule,
|
FloatLabelModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
@ -25,17 +20,17 @@ import { InputNumberModule } from "primeng/inputnumber";
|
|||||||
InputNumberModule,
|
InputNumberModule,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-create-packing-modal.component.html",
|
templateUrl: './trip-create-packing-modal.component.html',
|
||||||
styleUrl: "./trip-create-packing-modal.component.scss",
|
styleUrl: './trip-create-packing-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripCreatePackingModalComponent {
|
export class TripCreatePackingModalComponent {
|
||||||
packingForm: FormGroup;
|
packingForm: FormGroup;
|
||||||
readonly packingCategories = [
|
readonly packingCategories = [
|
||||||
{ value: "clothes", dispValue: "Clothes" },
|
{ value: 'clothes', dispValue: 'Clothes' },
|
||||||
{ value: "toiletries", dispValue: "Toiletries" },
|
{ value: 'toiletries', dispValue: 'Toiletries' },
|
||||||
{ value: "tech", dispValue: "Tech" },
|
{ value: 'tech', dispValue: 'Tech' },
|
||||||
{ value: "documents", dispValue: "Documents" },
|
{ value: 'documents', dispValue: 'Documents' },
|
||||||
{ value: "other", dispValue: "Other" },
|
{ value: 'other', dispValue: 'Other' },
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -46,8 +41,8 @@ export class TripCreatePackingModalComponent {
|
|||||||
this.packingForm = this.fb.group({
|
this.packingForm = this.fb.group({
|
||||||
id: -1,
|
id: -1,
|
||||||
qt: null,
|
qt: null,
|
||||||
text: ["", { validators: Validators.required }],
|
text: ['', { validators: Validators.required }],
|
||||||
category: ["", { validators: Validators.required }],
|
category: ['', { validators: Validators.required }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const patchValue = this.config.data?.packing;
|
const patchValue = this.config.data?.packing;
|
||||||
|
|||||||
@ -1,26 +1,20 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { FocusTrapModule } from "primeng/focustrap";
|
import { FocusTrapModule } from 'primeng/focustrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-invite-member-modal",
|
selector: 'app-trip-invite-member-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FocusTrapModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-invite-member-modal.component.html",
|
templateUrl: './trip-invite-member-modal.component.html',
|
||||||
styleUrl: "./trip-invite-member-modal.component.scss",
|
styleUrl: './trip-invite-member-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripInviteMemberModalComponent {
|
export class TripInviteMemberModalComponent {
|
||||||
memberForm = new FormControl("");
|
memberForm = new FormControl('');
|
||||||
constructor(private ref: DynamicDialogRef) {}
|
constructor(private ref: DynamicDialogRef) {}
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
|
|||||||
@ -1,24 +1,19 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { TextareaModule } from "primeng/textarea";
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-notes-modal",
|
selector: 'app-trip-notes-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, TextareaModule, ButtonModule, ReactiveFormsModule],
|
||||||
FloatLabelModule,
|
|
||||||
TextareaModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-notes-modal.component.html",
|
templateUrl: './trip-notes-modal.component.html',
|
||||||
styleUrl: "./trip-notes-modal.component.scss",
|
styleUrl: './trip-notes-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripNotesModalComponent {
|
export class TripNotesModalComponent {
|
||||||
notes = new FormControl("");
|
notes = new FormControl('');
|
||||||
isEditing: boolean = false;
|
isEditing: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@ -1,28 +1,22 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from "primeng/floatlabel";
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { InputTextModule } from "primeng/inputtext";
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { Place } from "../../types/poi";
|
import { Place } from '../../types/poi';
|
||||||
import { ApiService } from "../../services/api.service";
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SkeletonModule } from "primeng/skeleton";
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-trip-place-select-modal",
|
selector: 'app-trip-place-select-modal',
|
||||||
imports: [
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule],
|
||||||
FloatLabelModule,
|
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
SkeletonModule,
|
|
||||||
],
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "./trip-place-select-modal.component.html",
|
templateUrl: './trip-place-select-modal.component.html',
|
||||||
styleUrl: "./trip-place-select-modal.component.scss",
|
styleUrl: './trip-place-select-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripPlaceSelectModalComponent {
|
export class TripPlaceSelectModalComponent {
|
||||||
searchInput = new FormControl("");
|
searchInput = new FormControl('');
|
||||||
|
|
||||||
selectedPlaces: Place[] = [];
|
selectedPlaces: Place[] = [];
|
||||||
showSelectedPlaces: boolean = false;
|
showSelectedPlaces: boolean = false;
|
||||||
@ -38,9 +32,7 @@ export class TripPlaceSelectModalComponent {
|
|||||||
) {
|
) {
|
||||||
this.apiService.getPlaces().subscribe({
|
this.apiService.getPlaces().subscribe({
|
||||||
next: (places) => {
|
next: (places) => {
|
||||||
this.places = places.sort((a, b) =>
|
this.places = places.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
this.displayedPlaces = places;
|
this.displayedPlaces = places;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -60,9 +52,7 @@ export class TripPlaceSelectModalComponent {
|
|||||||
|
|
||||||
const v = value.toLowerCase();
|
const v = value.toLowerCase();
|
||||||
this.displayedPlaces = this.places.filter(
|
this.displayedPlaces = this.places.filter(
|
||||||
(p) =>
|
(p) => p.name.toLowerCase().includes(v) || p.description?.toLowerCase().includes(v),
|
||||||
p.name.toLowerCase().includes(v) ||
|
|
||||||
p.description?.toLowerCase().includes(v),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -80,9 +70,7 @@ export class TripPlaceSelectModalComponent {
|
|||||||
|
|
||||||
this.selectedPlacesID.push(p.id);
|
this.selectedPlacesID.push(p.id);
|
||||||
this.selectedPlaces.push(p);
|
this.selectedPlaces.push(p);
|
||||||
this.selectedPlaces.sort((a, b) =>
|
this.selectedPlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Category, Place } from "../types/poi";
|
import { Category, Place } from '../types/poi';
|
||||||
import { BehaviorSubject, map, Observable, shareReplay, tap } from "rxjs";
|
import { BehaviorSubject, map, Observable, shareReplay, tap } from 'rxjs';
|
||||||
import { Info } from "../types/info";
|
import { Info } from '../types/info';
|
||||||
import { ImportResponse, Settings } from "../types/settings";
|
import { ImportResponse, Settings } from '../types/settings';
|
||||||
import {
|
import {
|
||||||
ChecklistItem,
|
ChecklistItem,
|
||||||
PackingItem,
|
PackingItem,
|
||||||
@ -14,37 +14,31 @@ import {
|
|||||||
TripInvitation,
|
TripInvitation,
|
||||||
TripItem,
|
TripItem,
|
||||||
TripMember,
|
TripMember,
|
||||||
} from "../types/trip";
|
} from '../types/trip';
|
||||||
|
|
||||||
const NO_AUTH_HEADER = {
|
const NO_AUTH_HEADER = {
|
||||||
no_auth: "1",
|
no_auth: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ApiService {
|
export class ApiService {
|
||||||
public readonly apiBaseUrl: string = "/api";
|
public readonly apiBaseUrl: string = '/api';
|
||||||
|
|
||||||
private categoriesSubject = new BehaviorSubject<Category[] | null>(null);
|
private categoriesSubject = new BehaviorSubject<Category[] | null>(null);
|
||||||
public categories$: Observable<Category[] | null> =
|
public categories$: Observable<Category[] | null> = this.categoriesSubject.asObservable();
|
||||||
this.categoriesSubject.asObservable();
|
|
||||||
|
|
||||||
private settingsSubject = new BehaviorSubject<Settings | null>(null);
|
private settingsSubject = new BehaviorSubject<Settings | null>(null);
|
||||||
public settings$: Observable<Settings | null> =
|
public settings$: Observable<Settings | null> = this.settingsSubject.asObservable();
|
||||||
this.settingsSubject.asObservable();
|
|
||||||
private httpClient = inject(HttpClient);
|
private httpClient = inject(HttpClient);
|
||||||
|
|
||||||
getInfo(): Observable<Info> {
|
getInfo(): Observable<Info> {
|
||||||
return this.httpClient.get<Info>(this.apiBaseUrl + "/info");
|
return this.httpClient.get<Info>(this.apiBaseUrl + '/info');
|
||||||
}
|
}
|
||||||
|
|
||||||
_categoriesSubjectNext(categories: Category[]) {
|
_categoriesSubjectNext(categories: Category[]) {
|
||||||
this.categoriesSubject.next(
|
this.categoriesSubject.next([...categories].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)));
|
||||||
[...categories].sort((a, b) =>
|
|
||||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategories(): Observable<Category[]> {
|
getCategories(): Observable<Category[]> {
|
||||||
@ -58,46 +52,35 @@ export class ApiService {
|
|||||||
|
|
||||||
postCategory(c: Category): Observable<Category> {
|
postCategory(c: Category): Observable<Category> {
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.post<Category>(this.apiBaseUrl + "/categories", c)
|
.post<Category>(this.apiBaseUrl + '/categories', c)
|
||||||
.pipe(
|
.pipe(tap((category) => this._categoriesSubjectNext([...(this.categoriesSubject.value || []), category])));
|
||||||
tap((category) =>
|
|
||||||
this._categoriesSubjectNext([
|
|
||||||
...(this.categoriesSubject.value || []),
|
|
||||||
category,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putCategory(c_id: number, c: Partial<Category>): Observable<Category> {
|
putCategory(c_id: number, c: Partial<Category>): Observable<Category> {
|
||||||
return this.httpClient
|
return this.httpClient.put<Category>(this.apiBaseUrl + `/categories/${c_id}`, c).pipe(
|
||||||
.put<Category>(this.apiBaseUrl + `/categories/${c_id}`, c)
|
tap((category) => {
|
||||||
.pipe(
|
const categories = this.categoriesSubject.value || [];
|
||||||
tap((category) => {
|
const idx = categories?.findIndex((c) => c.id == c_id) || -1;
|
||||||
const categories = this.categoriesSubject.value || [];
|
if (idx > -1) {
|
||||||
const idx = categories?.findIndex((c) => c.id == c_id) || -1;
|
const updated = [...categories];
|
||||||
if (idx > -1) {
|
updated[idx] = category;
|
||||||
const updated = [...categories];
|
this._categoriesSubjectNext(updated);
|
||||||
updated[idx] = category;
|
}
|
||||||
this._categoriesSubjectNext(updated);
|
}),
|
||||||
}
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCategory(category_id: number): Observable<{}> {
|
deleteCategory(category_id: number): Observable<{}> {
|
||||||
return this.httpClient
|
return this.httpClient.delete<{}>(this.apiBaseUrl + `/categories/${category_id}`).pipe(
|
||||||
.delete<{}>(this.apiBaseUrl + `/categories/${category_id}`)
|
tap(() => {
|
||||||
.pipe(
|
const categories = this.categoriesSubject.value || [];
|
||||||
tap(() => {
|
const idx = categories?.findIndex((c) => c.id == category_id) || -1;
|
||||||
const categories = this.categoriesSubject.value || [];
|
if (idx > -1) {
|
||||||
const idx = categories?.findIndex((c) => c.id == category_id) || -1;
|
const updated = categories.filter((_, i) => i != idx);
|
||||||
if (idx > -1) {
|
this._categoriesSubjectNext(updated);
|
||||||
const updated = categories.filter((_, i) => i != idx);
|
}
|
||||||
this._categoriesSubjectNext(updated);
|
}),
|
||||||
}
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaces(): Observable<Place[]> {
|
getPlaces(): Observable<Place[]> {
|
||||||
@ -109,23 +92,15 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
postPlaces(places: Partial<Place[]>): Observable<Place[]> {
|
postPlaces(places: Partial<Place[]>): Observable<Place[]> {
|
||||||
return this.httpClient.post<Place[]>(
|
return this.httpClient.post<Place[]>(`${this.apiBaseUrl}/places/batch`, places);
|
||||||
`${this.apiBaseUrl}/places/batch`,
|
|
||||||
places,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putPlace(place_id: number, place: Partial<Place>): Observable<Place> {
|
putPlace(place_id: number, place: Partial<Place>): Observable<Place> {
|
||||||
return this.httpClient.put<Place>(
|
return this.httpClient.put<Place>(`${this.apiBaseUrl}/places/${place_id}`, place);
|
||||||
`${this.apiBaseUrl}/places/${place_id}`,
|
|
||||||
place,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePlace(place_id: number): Observable<null> {
|
deletePlace(place_id: number): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/places/${place_id}`);
|
||||||
`${this.apiBaseUrl}/places/${place_id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaceGPX(place_id: number): Observable<Place> {
|
getPlaceGPX(place_id: number): Observable<Place> {
|
||||||
@ -141,9 +116,7 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTripBalance(id: number): Observable<{ [user: string]: number }> {
|
getTripBalance(id: number): Observable<{ [user: string]: number }> {
|
||||||
return this.httpClient.get<{ [user: string]: number }>(
|
return this.httpClient.get<{ [user: string]: number }>(`${this.apiBaseUrl}/trips/${id}/balance`);
|
||||||
`${this.apiBaseUrl}/trips/${id}/balance`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postTrip(trip: TripBase): Observable<TripBase> {
|
postTrip(trip: TripBase): Observable<TripBase> {
|
||||||
@ -155,79 +128,42 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
putTrip(trip: Partial<Trip>, trip_id: number): Observable<Trip> {
|
putTrip(trip: Partial<Trip>, trip_id: number): Observable<Trip> {
|
||||||
return this.httpClient.put<Trip>(
|
return this.httpClient.put<Trip>(`${this.apiBaseUrl}/trips/${trip_id}`, trip);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}`,
|
|
||||||
trip,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postTripDay(tripDay: TripDay, trip_id: number): Observable<TripDay> {
|
postTripDay(tripDay: TripDay, trip_id: number): Observable<TripDay> {
|
||||||
return this.httpClient.post<TripDay>(
|
return this.httpClient.post<TripDay>(`${this.apiBaseUrl}/trips/${trip_id}/days`, tripDay);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days`,
|
|
||||||
tripDay,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putTripDay(tripDay: Partial<TripDay>, trip_id: number): Observable<TripDay> {
|
putTripDay(tripDay: Partial<TripDay>, trip_id: number): Observable<TripDay> {
|
||||||
return this.httpClient.put<TripDay>(
|
return this.httpClient.put<TripDay>(`${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`, tripDay);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`,
|
|
||||||
tripDay,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTripDay(trip_id: number, day_id: number): Observable<null> {
|
deleteTripDay(trip_id: number, day_id: number): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postTripDayItem(
|
postTripDayItem(item: TripItem, trip_id: number, day_id: number): Observable<TripItem> {
|
||||||
item: TripItem,
|
return this.httpClient.post<TripItem>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`, item);
|
||||||
trip_id: number,
|
|
||||||
day_id: number,
|
|
||||||
): Observable<TripItem> {
|
|
||||||
return this.httpClient.post<TripItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putTripDayItem(
|
putTripDayItem(item: Partial<TripItem>, trip_id: number, day_id: number, item_id: number): Observable<TripItem> {
|
||||||
item: Partial<TripItem>,
|
return this.httpClient.put<TripItem>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`, item);
|
||||||
trip_id: number,
|
|
||||||
day_id: number,
|
|
||||||
item_id: number,
|
|
||||||
): Observable<TripItem> {
|
|
||||||
return this.httpClient.put<TripItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTripDayItem(
|
deleteTripDayItem(trip_id: number, day_id: number, item_id: number): Observable<null> {
|
||||||
trip_id: number,
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`);
|
||||||
day_id: number,
|
|
||||||
item_id: number,
|
|
||||||
): Observable<null> {
|
|
||||||
return this.httpClient.delete<null>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSharedTrip(token: string): Observable<Trip> {
|
getSharedTrip(token: string): Observable<Trip> {
|
||||||
return this.httpClient.get<Trip>(
|
return this.httpClient.get<Trip>(`${this.apiBaseUrl}/trips/shared/${token}`, { headers: NO_AUTH_HEADER });
|
||||||
`${this.apiBaseUrl}/trips/shared/${token}`,
|
|
||||||
{ headers: NO_AUTH_HEADER },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSharedTripURL(trip_id: number): Observable<string> {
|
getSharedTripURL(trip_id: number): Observable<string> {
|
||||||
return this.httpClient
|
return this.httpClient.get<SharedTripURL>(`${this.apiBaseUrl}/trips/${trip_id}/share`).pipe(
|
||||||
.get<SharedTripURL>(`${this.apiBaseUrl}/trips/${trip_id}/share`)
|
map((t) => window.location.origin + t.url),
|
||||||
.pipe(
|
shareReplay(),
|
||||||
map((t) => window.location.origin + t.url),
|
);
|
||||||
shareReplay(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createSharedTrip(trip_id: number): Observable<string> {
|
createSharedTrip(trip_id: number): Observable<string> {
|
||||||
@ -237,138 +173,79 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteSharedTrip(trip_id: number): Observable<null> {
|
deleteSharedTrip(trip_id: number): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/share`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/share`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPackingList(trip_id: number): Observable<PackingItem[]> {
|
getPackingList(trip_id: number): Observable<PackingItem[]> {
|
||||||
return this.httpClient.get<PackingItem[]>(
|
return this.httpClient.get<PackingItem[]>(`${this.apiBaseUrl}/trips/${trip_id}/packing`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSharedTripPackingList(token: string): Observable<PackingItem[]> {
|
getSharedTripPackingList(token: string): Observable<PackingItem[]> {
|
||||||
return this.httpClient.get<PackingItem[]>(
|
return this.httpClient.get<PackingItem[]>(`${this.apiBaseUrl}/trips/shared/${token}/packing`);
|
||||||
`${this.apiBaseUrl}/trips/shared/${token}/packing`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postPackingItem(
|
postPackingItem(trip_id: number, p_item: PackingItem): Observable<PackingItem> {
|
||||||
trip_id: number,
|
return this.httpClient.post<PackingItem>(`${this.apiBaseUrl}/trips/${trip_id}/packing`, p_item);
|
||||||
p_item: PackingItem,
|
|
||||||
): Observable<PackingItem> {
|
|
||||||
return this.httpClient.post<PackingItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
|
|
||||||
p_item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putPackingItem(
|
putPackingItem(trip_id: number, p_id: number, p_item: Partial<PackingItem>): Observable<PackingItem> {
|
||||||
trip_id: number,
|
return this.httpClient.put<PackingItem>(`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`, p_item);
|
||||||
p_id: number,
|
|
||||||
p_item: Partial<PackingItem>,
|
|
||||||
): Observable<PackingItem> {
|
|
||||||
return this.httpClient.put<PackingItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
|
|
||||||
p_item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePackingItem(trip_id: number, p_id: number): Observable<null> {
|
deletePackingItem(trip_id: number, p_id: number): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getChecklist(trip_id: number): Observable<ChecklistItem[]> {
|
getChecklist(trip_id: number): Observable<ChecklistItem[]> {
|
||||||
return this.httpClient.get<ChecklistItem[]>(
|
return this.httpClient.get<ChecklistItem[]>(`${this.apiBaseUrl}/trips/${trip_id}/checklist`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSharedTripChecklist(token: string): Observable<ChecklistItem[]> {
|
getSharedTripChecklist(token: string): Observable<ChecklistItem[]> {
|
||||||
return this.httpClient.get<ChecklistItem[]>(
|
return this.httpClient.get<ChecklistItem[]>(`${this.apiBaseUrl}/trips/shared/${token}/checklist`);
|
||||||
`${this.apiBaseUrl}/trips/shared/${token}/checklist`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postChecklistItem(
|
postChecklistItem(trip_id: number, item: ChecklistItem): Observable<ChecklistItem> {
|
||||||
trip_id: number,
|
return this.httpClient.post<ChecklistItem>(`${this.apiBaseUrl}/trips/${trip_id}/checklist`, item);
|
||||||
item: ChecklistItem,
|
|
||||||
): Observable<ChecklistItem> {
|
|
||||||
return this.httpClient.post<ChecklistItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putChecklistItem(
|
putChecklistItem(trip_id: number, id: number, item: Partial<ChecklistItem>): Observable<ChecklistItem> {
|
||||||
trip_id: number,
|
return this.httpClient.put<ChecklistItem>(`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`, item);
|
||||||
id: number,
|
|
||||||
item: Partial<ChecklistItem>,
|
|
||||||
): Observable<ChecklistItem> {
|
|
||||||
return this.httpClient.put<ChecklistItem>(
|
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteChecklistItem(trip_id: number, id: number): Observable<null> {
|
deleteChecklistItem(trip_id: number, id: number): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHasTripsInvitations(): Observable<boolean> {
|
getHasTripsInvitations(): Observable<boolean> {
|
||||||
return this.httpClient.get<boolean>(
|
return this.httpClient.get<boolean>(`${this.apiBaseUrl}/trips/invitations/pending`);
|
||||||
`${this.apiBaseUrl}/trips/invitations/pending`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTripsInvitations(): Observable<TripInvitation[]> {
|
getTripsInvitations(): Observable<TripInvitation[]> {
|
||||||
return this.httpClient.get<TripInvitation[]>(
|
return this.httpClient.get<TripInvitation[]>(`${this.apiBaseUrl}/trips/invitations`);
|
||||||
`${this.apiBaseUrl}/trips/invitations`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTripMembers(trip_id: number): Observable<TripMember[]> {
|
getTripMembers(trip_id: number): Observable<TripMember[]> {
|
||||||
return this.httpClient.get<TripMember[]>(
|
return this.httpClient.get<TripMember[]>(`${this.apiBaseUrl}/trips/${trip_id}/members`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/members`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTripMember(trip_id: number, username: string): Observable<null> {
|
deleteTripMember(trip_id: number, username: string): Observable<null> {
|
||||||
return this.httpClient.delete<null>(
|
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/${username}`);
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/members/${username}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteTripMember(trip_id: number, user: string): Observable<TripMember> {
|
inviteTripMember(trip_id: number, user: string): Observable<TripMember> {
|
||||||
return this.httpClient.post<TripMember>(
|
return this.httpClient.post<TripMember>(`${this.apiBaseUrl}/trips/${trip_id}/members`, { user });
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/members`,
|
|
||||||
{ user },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptTripMemberInvite(trip_id: number): Observable<null> {
|
acceptTripMemberInvite(trip_id: number): Observable<null> {
|
||||||
return this.httpClient.post<null>(
|
return this.httpClient.post<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/accept`, {});
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/members/accept`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declineTripMemberInvite(trip_id: number): Observable<null> {
|
declineTripMemberInvite(trip_id: number): Observable<null> {
|
||||||
return this.httpClient.post<null>(
|
return this.httpClient.post<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/decline`, {});
|
||||||
`${this.apiBaseUrl}/trips/${trip_id}/members/decline`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkVersion(): Observable<string> {
|
checkVersion(): Observable<string> {
|
||||||
return this.httpClient.get<string>(
|
return this.httpClient.get<string>(`${this.apiBaseUrl}/settings/checkversion`);
|
||||||
`${this.apiBaseUrl}/settings/checkversion`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings(): Observable<Settings> {
|
getSettings(): Observable<Settings> {
|
||||||
@ -392,7 +269,7 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settingsUserImport(formdata: FormData): Observable<ImportResponse> {
|
settingsUserImport(formdata: FormData): Observable<ImportResponse> {
|
||||||
const headers = { enctype: "multipart/form-data" };
|
const headers = { enctype: 'multipart/form-data' };
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.post<ImportResponse>(`${this.apiBaseUrl}/settings/import`, formdata, {
|
.post<ImportResponse>(`${this.apiBaseUrl}/settings/import`, formdata, {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { inject } from "@angular/core";
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from "@angular/router";
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
import { UtilsService } from "./utils.service";
|
import { UtilsService } from './utils.service';
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from './auth.service';
|
||||||
import { of, switchMap, take } from "rxjs";
|
import { of, switchMap, take } from 'rxjs';
|
||||||
|
|
||||||
export const AuthGuard: CanActivateFn = (_, state) => {
|
export const AuthGuard: CanActivateFn = (_, state) => {
|
||||||
const router: Router = inject(Router);
|
const router: Router = inject(Router);
|
||||||
@ -14,14 +14,9 @@ export const AuthGuard: CanActivateFn = (_, state) => {
|
|||||||
take(1),
|
take(1),
|
||||||
switchMap((authenticated) => {
|
switchMap((authenticated) => {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
const redirectURL =
|
const redirectURL = state.url === '/auth' ? '' : `redirectURL=${state.url}`;
|
||||||
state.url === "/auth" ? "" : `redirectURL=${state.url}`;
|
|
||||||
const urlTree = router.parseUrl(`auth?${redirectURL}`);
|
const urlTree = router.parseUrl(`auth?${redirectURL}`);
|
||||||
utilsService.toast(
|
utilsService.toast('warn', 'Authentication required', 'You must be authenticated');
|
||||||
"warn",
|
|
||||||
"Authentication required",
|
|
||||||
"You must be authenticated",
|
|
||||||
);
|
|
||||||
return of(urlTree);
|
return of(urlTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from '@angular/core';
|
||||||
import { Router } from "@angular/router";
|
import { Router } from '@angular/router';
|
||||||
import { Observable, of, ReplaySubject } from "rxjs";
|
import { Observable, of, ReplaySubject } from 'rxjs';
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from 'rxjs/operators';
|
||||||
import { ApiService } from "./api.service";
|
import { ApiService } from './api.service';
|
||||||
import { UtilsService } from "./utils.service";
|
import { UtilsService } from './utils.service';
|
||||||
|
|
||||||
export interface Token {
|
export interface Token {
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
@ -16,11 +16,11 @@ export interface AuthParams {
|
|||||||
oidc?: string;
|
oidc?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JWT_TOKEN = "TRIP_AT";
|
const JWT_TOKEN = 'TRIP_AT';
|
||||||
const REFRESH_TOKEN = "TRIP_RT";
|
const REFRESH_TOKEN = 'TRIP_RT';
|
||||||
const JWT_USER = "TRIP_USER";
|
const JWT_USER = 'TRIP_USER';
|
||||||
|
|
||||||
@Injectable({ providedIn: "root" })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public readonly apiBaseUrl: string;
|
public readonly apiBaseUrl: string;
|
||||||
private refreshInProgressLock$: ReplaySubject<Token> | null = null;
|
private refreshInProgressLock$: ReplaySubject<Token> | null = null;
|
||||||
@ -39,7 +39,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get loggedUser(): string {
|
get loggedUser(): string {
|
||||||
return localStorage.getItem(JWT_USER) ?? "";
|
return localStorage.getItem(JWT_USER) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
set accessToken(token: string) {
|
set accessToken(token: string) {
|
||||||
@ -47,7 +47,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get accessToken(): string {
|
get accessToken(): string {
|
||||||
return localStorage.getItem(JWT_TOKEN) ?? "";
|
return localStorage.getItem(JWT_TOKEN) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
set refreshToken(token: string) {
|
set refreshToken(token: string) {
|
||||||
@ -55,11 +55,11 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get refreshToken(): string {
|
get refreshToken(): string {
|
||||||
return localStorage.getItem(REFRESH_TOKEN) ?? "";
|
return localStorage.getItem(REFRESH_TOKEN) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
authParams(): Observable<AuthParams> {
|
authParams(): Observable<AuthParams> {
|
||||||
return this.httpClient.get<AuthParams>(this.apiBaseUrl + "/auth/params");
|
return this.httpClient.get<AuthParams>(this.apiBaseUrl + '/auth/params');
|
||||||
}
|
}
|
||||||
|
|
||||||
storeTokens(tokens: Token): void {
|
storeTokens(tokens: Token): void {
|
||||||
@ -81,7 +81,7 @@ export class AuthService {
|
|||||||
this.refreshInProgressLock$ = new ReplaySubject(1);
|
this.refreshInProgressLock$ = new ReplaySubject(1);
|
||||||
|
|
||||||
this.httpClient
|
this.httpClient
|
||||||
.post<Token>(this.apiBaseUrl + "/auth/refresh", {
|
.post<Token>(this.apiBaseUrl + '/auth/refresh', {
|
||||||
refresh_token: this.refreshToken,
|
refresh_token: this.refreshToken,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -103,60 +103,47 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
login(authForm: { username: string; password: string }): Observable<Token> {
|
login(authForm: { username: string; password: string }): Observable<Token> {
|
||||||
return this.httpClient
|
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/login', authForm).pipe(
|
||||||
.post<Token>(this.apiBaseUrl + "/auth/login", authForm)
|
tap((tokens: Token) => {
|
||||||
.pipe(
|
this.loggedUser = authForm.username;
|
||||||
tap((tokens: Token) => {
|
this.storeTokens(tokens);
|
||||||
this.loggedUser = authForm.username;
|
}),
|
||||||
this.storeTokens(tokens);
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
register(authForm: {
|
register(authForm: { username: string; password: string }): Observable<Token> {
|
||||||
username: string;
|
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/register', authForm).pipe(
|
||||||
password: string;
|
tap((tokens: Token) => {
|
||||||
}): Observable<Token> {
|
this.loggedUser = authForm.username;
|
||||||
return this.httpClient
|
this.storeTokens(tokens);
|
||||||
.post<Token>(this.apiBaseUrl + "/auth/register", authForm)
|
}),
|
||||||
.pipe(
|
);
|
||||||
tap((tokens: Token) => {
|
|
||||||
this.loggedUser = authForm.username;
|
|
||||||
this.storeTokens(tokens);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcLogin(code: string, state: string): Observable<Token> {
|
oidcLogin(code: string, state: string): Observable<Token> {
|
||||||
return this.httpClient
|
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/oidc/login', { code, state }).pipe(
|
||||||
.post<Token>(this.apiBaseUrl + "/auth/oidc/login", { code, state })
|
tap((data: any) => {
|
||||||
.pipe(
|
if (data.access_token && data.refresh_token) {
|
||||||
tap((data: any) => {
|
this.loggedUser = this._getTokenUsername(data.access_token);
|
||||||
if (data.access_token && data.refresh_token) {
|
this.storeTokens(data);
|
||||||
this.loggedUser = this._getTokenUsername(data.access_token);
|
}
|
||||||
this.storeTokens(data);
|
}),
|
||||||
}
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(custom_msg: string = "", is_error = false): void {
|
logout(custom_msg: string = '', is_error = false): void {
|
||||||
this.loggedUser = "";
|
this.loggedUser = '';
|
||||||
this.removeTokens();
|
this.removeTokens();
|
||||||
|
|
||||||
if (custom_msg) {
|
if (custom_msg) {
|
||||||
if (is_error) {
|
if (is_error) {
|
||||||
this.utilsService.toast(
|
this.utilsService.toast('error', 'You must be authenticated', custom_msg);
|
||||||
"error",
|
|
||||||
"You must be authenticated",
|
|
||||||
custom_msg,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.utilsService.toast("success", "Success", custom_msg);
|
this.utilsService.toast('success', 'Success', custom_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(["/auth"]);
|
this.router.navigate(['/auth']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeTokens(): void {
|
private removeTokens(): void {
|
||||||
@ -167,7 +154,7 @@ export class AuthService {
|
|||||||
|
|
||||||
isTokenExpired(token: string, offsetSeconds?: number): boolean {
|
isTokenExpired(token: string, offsetSeconds?: number): boolean {
|
||||||
// Return if there is no token
|
// Return if there is no token
|
||||||
if (!token || token === "") {
|
if (!token || token === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,25 +173,19 @@ export class AuthService {
|
|||||||
private _b64DecodeUnicode(str: any): string {
|
private _b64DecodeUnicode(str: any): string {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
Array.prototype.map
|
Array.prototype.map
|
||||||
.call(
|
.call(this._b64decode(str), (c: any) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||||
this._b64decode(str),
|
.join(''),
|
||||||
(c: any) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2),
|
|
||||||
)
|
|
||||||
.join(""),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _b64decode(str: string): string {
|
private _b64decode(str: string): string {
|
||||||
const chars =
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
let output = '';
|
||||||
let output = "";
|
|
||||||
|
|
||||||
str = String(str).replace(/=+$/, "");
|
str = String(str).replace(/=+$/, '');
|
||||||
|
|
||||||
if (str.length % 4 === 1) {
|
if (str.length % 4 === 1) {
|
||||||
throw new Error(
|
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
|
||||||
"'atob' failed: The string to be decoded is not correctly encoded.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
@ -223,21 +204,21 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _urlBase64Decode(str: string): string {
|
private _urlBase64Decode(str: string): string {
|
||||||
let output = str.replace(/-/g, "+").replace(/_/g, "/");
|
let output = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
switch (output.length % 4) {
|
switch (output.length % 4) {
|
||||||
case 0: {
|
case 0: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
output += "==";
|
output += '==';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 3: {
|
case 3: {
|
||||||
output += "=";
|
output += '=';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw Error("Illegal base64url string!");
|
throw Error('Illegal base64url string!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this._b64DecodeUnicode(output);
|
return this._b64DecodeUnicode(output);
|
||||||
@ -247,11 +228,11 @@ export class AuthService {
|
|||||||
const decodedToken = this._decodeToken(token);
|
const decodedToken = this._decodeToken(token);
|
||||||
|
|
||||||
if (decodedToken === null) {
|
if (decodedToken === null) {
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decodedToken.hasOwnProperty("sub")) {
|
if (!decodedToken.hasOwnProperty('sub')) {
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodedToken.sub;
|
return decodedToken.sub;
|
||||||
@ -262,7 +243,7 @@ export class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = token.split(".");
|
const parts = token.split('.');
|
||||||
|
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
return null;
|
return null;
|
||||||
@ -284,7 +265,7 @@ export class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decodedToken.hasOwnProperty("exp")) {
|
if (!decodedToken.hasOwnProperty('exp')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +1,52 @@
|
|||||||
import {
|
import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
|
||||||
HttpErrorResponse,
|
import { inject } from '@angular/core';
|
||||||
HttpEvent,
|
import { catchError, Observable, switchMap, take, throwError } from 'rxjs';
|
||||||
HttpHandlerFn,
|
import { AuthService } from './auth.service';
|
||||||
HttpRequest,
|
import { UtilsService } from './utils.service';
|
||||||
} from "@angular/common/http";
|
|
||||||
import { inject } from "@angular/core";
|
|
||||||
import { catchError, Observable, switchMap, take, throwError } from "rxjs";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
import { UtilsService } from "./utils.service";
|
|
||||||
|
|
||||||
const ERROR_CONFIG: Record<number, { title: string; detail: string }> = {
|
const ERROR_CONFIG: Record<number, { title: string; detail: string }> = {
|
||||||
400: {
|
400: {
|
||||||
title: "Bad Request",
|
title: 'Bad Request',
|
||||||
detail: "Unknown error, check console for details",
|
detail: 'Unknown error, check console for details',
|
||||||
},
|
},
|
||||||
403: { title: "Forbidden", detail: "You are not allowed to do this" },
|
403: { title: 'Forbidden', detail: 'You are not allowed to do this' },
|
||||||
409: { title: "Conflict", detail: "Conflict on resource" },
|
409: { title: 'Conflict', detail: 'Conflict on resource' },
|
||||||
413: { title: "Request Entity Too Large", detail: "The resource is too big" },
|
413: { title: 'Request Entity Too Large', detail: 'The resource is too big' },
|
||||||
422: {
|
422: {
|
||||||
title: "Unprocessable Entity",
|
title: 'Unprocessable Entity',
|
||||||
detail: "The resource you sent was unprocessable",
|
detail: 'The resource you sent was unprocessable',
|
||||||
},
|
},
|
||||||
502: {
|
502: {
|
||||||
title: "Bad Gateway",
|
title: 'Bad Gateway',
|
||||||
detail: "Check your connectivity and ensure the server is up",
|
detail: 'Check your connectivity and ensure the server is up',
|
||||||
},
|
},
|
||||||
503: { title: "Service Unavailable", detail: "Resource not available" },
|
503: { title: 'Service Unavailable', detail: 'Resource not available' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Interceptor = (
|
export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
|
||||||
req: HttpRequest<unknown>,
|
|
||||||
next: HttpHandlerFn,
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const utilsService = inject(UtilsService);
|
const utilsService = inject(UtilsService);
|
||||||
|
|
||||||
function showAndThrowError(title: string, details: string) {
|
function showAndThrowError(title: string, details: string) {
|
||||||
utilsService.toast("error", title, details);
|
utilsService.toast('error', title, details);
|
||||||
return throwError(() => details);
|
return throwError(() => details);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers.has("no_auth")) {
|
if (req.headers.has('no_auth')) {
|
||||||
// Shared Trip must be anonymous
|
// Shared Trip must be anonymous
|
||||||
return next(req);
|
return next(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.headers.has("enctype") && !req.headers.has("Content-Type")) {
|
if (!req.headers.has('enctype') && !req.headers.has('Content-Type')) {
|
||||||
req = req.clone({
|
req = req.clone({
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept-Language": "en-US;q=0.9,en-US,en;q=0.8",
|
'Accept-Language': 'en-US;q=0.9,en-US,en;q=0.8',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (authService.accessToken && !authService.isTokenExpired(authService.accessToken)) {
|
||||||
authService.accessToken &&
|
|
||||||
!authService.isTokenExpired(authService.accessToken)
|
|
||||||
) {
|
|
||||||
if (req.url.startsWith(authService.apiBaseUrl)) {
|
if (req.url.startsWith(authService.apiBaseUrl)) {
|
||||||
req = req.clone({
|
req = req.clone({
|
||||||
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
|
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
|
||||||
@ -70,17 +59,14 @@ export const Interceptor = (
|
|||||||
const errDetails = ERROR_CONFIG[err.status];
|
const errDetails = ERROR_CONFIG[err.status];
|
||||||
if (errDetails) {
|
if (errDetails) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return showAndThrowError(
|
return showAndThrowError(errDetails.title, `${err.error?.detail || err.message || errDetails.detail}`);
|
||||||
errDetails.title,
|
|
||||||
`${err.error?.detail || err.message || errDetails.detail}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.status == 401 && authService.accessToken) {
|
if (err.status == 401 && authService.accessToken) {
|
||||||
// Handle 401 on Refresh (RT expired)
|
// Handle 401 on Refresh (RT expired)
|
||||||
if (req.url.endsWith("/refresh")) {
|
if (req.url.endsWith('/refresh')) {
|
||||||
authService.logout("Your session has expired", true);
|
authService.logout('Your session has expired', true);
|
||||||
return throwError(() => "Your session has expired");
|
return throwError(() => 'Your session has expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unauthenticated, AT exists but is expired (authServices.accessToken truethy), we refresh it
|
// Unauthenticated, AT exists but is expired (authServices.accessToken truethy), we refresh it
|
||||||
@ -95,18 +81,15 @@ export const Interceptor = (
|
|||||||
return next(refreshedReq);
|
return next(refreshedReq);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (err.status == 401 && !req.url.endsWith("/refresh")) {
|
} else if (err.status == 401 && !req.url.endsWith('/refresh')) {
|
||||||
// If any API route 401 -> redirect to login. We skip /refresh/ to prevent toast on login errors.
|
// If any API route 401 -> redirect to login. We skip /refresh/ to prevent toast on login errors.
|
||||||
authService.logout(
|
authService.logout(`${err.error?.detail || err.message || 'You must be authenticated'}`, true);
|
||||||
`${err.error?.detail || err.message || "You must be authenticated"}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return showAndThrowError(
|
return showAndThrowError(
|
||||||
"Request Error",
|
'Request Error',
|
||||||
`${err.error?.detail || err.message || "Unknown error, check console for details"}`,
|
`${err.error?.detail || err.message || 'Unknown error, check console for details'}`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,47 +1,42 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { MessageService } from "primeng/api";
|
import { MessageService } from 'primeng/api';
|
||||||
import { TripStatus } from "../types/trip";
|
import { TripStatus } from '../types/trip';
|
||||||
import { ApiService } from "./api.service";
|
import { ApiService } from './api.service';
|
||||||
import { map } from "rxjs";
|
import { map } from 'rxjs';
|
||||||
|
|
||||||
type ToastSeverity = "info" | "warn" | "error" | "success";
|
type ToastSeverity = 'info' | 'warn' | 'error' | 'success';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UtilsService {
|
export class UtilsService {
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? "€"));
|
currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? '€'));
|
||||||
|
|
||||||
readonly statuses: TripStatus[] = [
|
readonly statuses: TripStatus[] = [
|
||||||
{ label: "pending", color: "#3258A8" },
|
{ label: 'pending', color: '#3258A8' },
|
||||||
{ label: "booked", color: "#00A341" },
|
{ label: 'booked', color: '#00A341' },
|
||||||
{ label: "constraint", color: "#FFB900" },
|
{ label: 'constraint', color: '#FFB900' },
|
||||||
{ label: "optional", color: "#625A84" },
|
{ label: 'optional', color: '#625A84' },
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(private ngMessageService: MessageService) {}
|
constructor(private ngMessageService: MessageService) {}
|
||||||
|
|
||||||
toGithubTRIP() {
|
toGithubTRIP() {
|
||||||
window.open("https://github.com/itskovacs/trip", "_blank");
|
window.open('https://github.com/itskovacs/trip', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode() {
|
toggleDarkMode() {
|
||||||
const element = document.querySelector("html");
|
const element = document.querySelector('html');
|
||||||
element?.classList.toggle("dark");
|
element?.classList.toggle('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
enableDarkMode() {
|
enableDarkMode() {
|
||||||
const element = document.querySelector("html");
|
const element = document.querySelector('html');
|
||||||
element?.classList.toggle("dark", true);
|
element?.classList.toggle('dark', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast(
|
toast(severity: ToastSeverity = 'info', summary = 'Info', detail = '', life = 3000): void {
|
||||||
severity: ToastSeverity = "info",
|
|
||||||
summary = "Info",
|
|
||||||
detail = "",
|
|
||||||
life = 3000,
|
|
||||||
): void {
|
|
||||||
this.ngMessageService.add({
|
this.ngMessageService.add({
|
||||||
severity,
|
severity,
|
||||||
summary,
|
summary,
|
||||||
@ -57,12 +52,12 @@ export class UtilsService {
|
|||||||
const lngMatch = url.match(/!4d([\d\-.]+)/);
|
const lngMatch = url.match(/!4d([\d\-.]+)/);
|
||||||
|
|
||||||
if (!placeMatch || !latMatch || !lngMatch) {
|
if (!placeMatch || !latMatch || !lngMatch) {
|
||||||
this.toast("error", "Error", "Unrecognized Google Maps URL format");
|
this.toast('error', 'Error', 'Unrecognized Google Maps URL format');
|
||||||
console.error("Unrecognized Google Maps URL format");
|
console.error('Unrecognized Google Maps URL format');
|
||||||
return ["", ""];
|
return ['', ''];
|
||||||
}
|
}
|
||||||
|
|
||||||
const place = decodeURIComponent(placeMatch[1].replace(/\+/g, " ").trim());
|
const place = decodeURIComponent(placeMatch[1].replace(/\+/g, ' ').trim());
|
||||||
const latlng = `${latMatch[1]},${lngMatch[1]}`;
|
const latlng = `${latMatch[1]},${lngMatch[1]}`;
|
||||||
return [place, latlng];
|
return [place, latlng];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
export function calculateDistanceBetween(
|
export function calculateDistanceBetween(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||||
lat1: number,
|
|
||||||
lon1: number,
|
|
||||||
lat2: number,
|
|
||||||
lon2: number,
|
|
||||||
) {
|
|
||||||
// returns d in meter
|
// returns d in meter
|
||||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||||
const dLat = toRad(lat2 - lat1);
|
const dLat = toRad(lat2 - lat1);
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
import OpenLocationCode from "open-location-code-typescript";
|
import OpenLocationCode from 'open-location-code-typescript';
|
||||||
|
|
||||||
const patternDEC = /^\s*(-?\d{1,3}(?:\.\d+)?)\s*,\s*(-?\d{1,3}(?:\.\d+)?)\s*$/;
|
const patternDEC = /^\s*(-?\d{1,3}(?:\.\d+)?)\s*,\s*(-?\d{1,3}(?:\.\d+)?)\s*$/;
|
||||||
const patternDD =
|
const patternDD = /^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i;
|
||||||
/^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i;
|
|
||||||
const patternDMS =
|
const patternDMS =
|
||||||
/^\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([EW])\s*$/i;
|
/^\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([EW])\s*$/i;
|
||||||
const patternDMM =
|
const patternDMM =
|
||||||
/^\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([EW])\s*$/i;
|
/^\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([EW])\s*$/i;
|
||||||
|
|
||||||
function _dmsToDecimal(
|
function _dmsToDecimal(deg: number, min: number, sec: number, dir: string): number {
|
||||||
deg: number,
|
|
||||||
min: number,
|
|
||||||
sec: number,
|
|
||||||
dir: string,
|
|
||||||
): number {
|
|
||||||
const dec = deg + min / 60 + sec / 3600;
|
const dec = deg + min / 60 + sec / 3600;
|
||||||
return /[SW]/i.test(dir) ? -dec : dec;
|
return /[SW]/i.test(dir) ? -dec : dec;
|
||||||
}
|
}
|
||||||
@ -24,14 +18,12 @@ function _dmmToDecimal(deg: number, min: number, dir: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatLatLng(num: number): string {
|
export function formatLatLng(num: number): string {
|
||||||
const decimals = num.toString().split(".")[1]?.length || 0;
|
const decimals = num.toString().split('.')[1]?.length || 0;
|
||||||
return num.toFixed(Math.min(decimals, 5));
|
return num.toFixed(Math.min(decimals, 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkAndParseLatLng(
|
export function checkAndParseLatLng(value: string | number): [number, number] | undefined {
|
||||||
value: string | number,
|
if (typeof value !== 'string') return undefined;
|
||||||
): [number, number] | undefined {
|
|
||||||
if (typeof value !== "string") return undefined;
|
|
||||||
|
|
||||||
// Parse PlusCode
|
// Parse PlusCode
|
||||||
if (OpenLocationCode.isValid(value)) {
|
if (OpenLocationCode.isValid(value)) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Pipe, PipeTransform } from "@angular/core";
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Pipe({ name: "linkify", standalone: true })
|
@Pipe({ name: 'linkify', standalone: true })
|
||||||
export class LinkifyPipe implements PipeTransform {
|
export class LinkifyPipe implements PipeTransform {
|
||||||
constructor(private sanitizer: DomSanitizer) {}
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
@ -10,11 +10,11 @@ export class LinkifyPipe implements PipeTransform {
|
|||||||
/[&<>"']/g,
|
/[&<>"']/g,
|
||||||
(char) =>
|
(char) =>
|
||||||
({
|
({
|
||||||
"&": "&",
|
'&': '&',
|
||||||
"<": "<",
|
'<': '<',
|
||||||
">": ">",
|
'>': '>',
|
||||||
'"': """,
|
'"': '"',
|
||||||
"'": "'",
|
"'": ''',
|
||||||
})[char]!,
|
})[char]!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ export class LinkifyPipe implements PipeTransform {
|
|||||||
const safeText = this.basicEscape(text);
|
const safeText = this.basicEscape(text);
|
||||||
|
|
||||||
const html = safeText.replace(urlRegex, (url) => {
|
const html = safeText.replace(urlRegex, (url) => {
|
||||||
const href = url.startsWith("http") ? url : `https://${url}`;
|
const href = url.startsWith('http') ? url : `https://${url}`;
|
||||||
return `<a href="${href}" target="_blank">${url}</a>`;
|
return `<a href="${href}" target="_blank">${url}</a>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import * as L from "leaflet";
|
import * as L from 'leaflet';
|
||||||
import "leaflet.markercluster";
|
import 'leaflet.markercluster';
|
||||||
import "leaflet-contextmenu";
|
import 'leaflet-contextmenu';
|
||||||
import { Place } from "../types/poi";
|
import { Place } from '../types/poi';
|
||||||
|
|
||||||
export const DEFAULT_TILE_URL =
|
export const DEFAULT_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
|
||||||
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png";
|
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
text: string;
|
text: string;
|
||||||
index?: number;
|
index?: number;
|
||||||
@ -20,15 +19,12 @@ export interface MarkerOptions extends L.MarkerOptions {
|
|||||||
contextmenuItems: ContextMenuItem[];
|
contextmenuItems: ContextMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMap(
|
export function createMap(contextMenuItems: ContextMenuItem[] = [], tilelayer: string = DEFAULT_TILE_URL): L.Map {
|
||||||
contextMenuItems: ContextMenuItem[] = [],
|
|
||||||
tilelayer: string = DEFAULT_TILE_URL,
|
|
||||||
): L.Map {
|
|
||||||
const southWest = L.latLng(-89.99, -180);
|
const southWest = L.latLng(-89.99, -180);
|
||||||
const northEast = L.latLng(89.99, 180);
|
const northEast = L.latLng(89.99, 180);
|
||||||
const bounds = L.latLngBounds(southWest, northEast);
|
const bounds = L.latLngBounds(southWest, northEast);
|
||||||
|
|
||||||
const map = L.map("map", {
|
const map = L.map('map', {
|
||||||
maxBoundsViscosity: 1.0,
|
maxBoundsViscosity: 1.0,
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
contextmenu: true,
|
contextmenu: true,
|
||||||
@ -61,34 +57,29 @@ export function createClusterGroup(): L.MarkerClusterGroup {
|
|||||||
const count = cluster.getChildCount();
|
const count = cluster.getChildCount();
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
html: `<div class="custom-cluster">${count}</div>`,
|
html: `<div class="custom-cluster">${count}</div>`,
|
||||||
className: "",
|
className: '',
|
||||||
iconSize: [40, 40],
|
iconSize: [40, 40],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tripDayMarker(item: {
|
export function tripDayMarker(item: { text: string; lat: number; lng: number; time?: string }): L.Marker {
|
||||||
text: string;
|
|
||||||
lat: number;
|
|
||||||
lng: number;
|
|
||||||
time?: string;
|
|
||||||
}): L.Marker {
|
|
||||||
const marker = new L.Marker([item.lat!, item.lng], {
|
const marker = new L.Marker([item.lat!, item.lng], {
|
||||||
icon: L.divIcon({
|
icon: L.divIcon({
|
||||||
className: "bg-black rounded-full",
|
className: 'bg-black rounded-full',
|
||||||
iconSize: [14, 14],
|
iconSize: [14, 14],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const touchDevice = "ontouchstart" in window;
|
const touchDevice = 'ontouchstart' in window;
|
||||||
if (!touchDevice) {
|
if (!touchDevice) {
|
||||||
marker.bindTooltip(
|
marker.bindTooltip(
|
||||||
`<div class="text-xs text-gray-500">${item.time}</div><div class="font-semibold mb-1 truncate text-base">${item.text}</div>`,
|
`<div class="text-xs text-gray-500">${item.time}</div><div class="font-semibold mb-1 truncate text-base">${item.text}</div>`,
|
||||||
{
|
{
|
||||||
direction: "right",
|
direction: 'right',
|
||||||
offset: [10, 0],
|
offset: [10, 0],
|
||||||
className: "class-tooltip",
|
className: 'class-tooltip',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -104,28 +95,25 @@ export function placeToMarker(
|
|||||||
const options: Partial<L.MarkerOptions> = {
|
const options: Partial<L.MarkerOptions> = {
|
||||||
riseOnHover: true,
|
riseOnHover: true,
|
||||||
title: place.name,
|
title: place.name,
|
||||||
alt: "",
|
alt: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const markerImage = isLowNet
|
const markerImage = isLowNet ? place.category.image : (place.image ?? place.category.image);
|
||||||
? place.category.image
|
|
||||||
: (place.image ?? place.category.image);
|
|
||||||
|
|
||||||
let markerClasses =
|
let markerClasses = 'w-full h-full rounded-full bg-center bg-cover bg-white dark:bg-surface-900';
|
||||||
"w-full h-full rounded-full bg-center bg-cover bg-white dark:bg-surface-900";
|
if (grayscale) markerClasses += ' grayscale';
|
||||||
if (grayscale) markerClasses += " grayscale";
|
|
||||||
|
|
||||||
const iconHtml = `
|
const iconHtml = `
|
||||||
<div class="flex items-center justify-center relative rounded-full marker-anchor size-14 box-border" style="border: 2px solid ${place.category.color};">
|
<div class="flex items-center justify-center relative rounded-full marker-anchor size-14 box-border" style="border: 2px solid ${place.category.color};">
|
||||||
<div class="${markerClasses}" style="background-image: url('${markerImage}');"></div>
|
<div class="${markerClasses}" style="background-image: url('${markerImage}');"></div>
|
||||||
${gpxInBubble && place.gpx ? '<div class="absolute -top-1 -left-1 size-6 flex justify-center items-center bg-white dark:bg-surface-900 border-2 border-black rounded-full"><i class="pi pi-compass"></i></div>' : ""}
|
${gpxInBubble && place.gpx ? '<div class="absolute -top-1 -left-1 size-6 flex justify-center items-center bg-white dark:bg-surface-900 border-2 border-black rounded-full"><i class="pi pi-compass"></i></div>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const icon = L.divIcon({
|
const icon = L.divIcon({
|
||||||
html: iconHtml.trim(),
|
html: iconHtml.trim(),
|
||||||
iconSize: [56, 56],
|
iconSize: [56, 56],
|
||||||
className: "",
|
className: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = new L.Marker([+place.lat, +place.lng], {
|
const marker = new L.Marker([+place.lat, +place.lng], {
|
||||||
@ -133,12 +121,12 @@ export function placeToMarker(
|
|||||||
icon,
|
icon,
|
||||||
});
|
});
|
||||||
|
|
||||||
const touchDevice = "ontouchstart" in window;
|
const touchDevice = 'ontouchstart' in window;
|
||||||
if (!touchDevice) {
|
if (!touchDevice) {
|
||||||
marker.bindTooltip(placeHoverTooltip(place), {
|
marker.bindTooltip(placeHoverTooltip(place), {
|
||||||
direction: "right",
|
direction: 'right',
|
||||||
offset: [28, 0],
|
offset: [28, 0],
|
||||||
className: "class-tooltip",
|
className: 'class-tooltip',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return marker;
|
return marker;
|
||||||
@ -146,16 +134,12 @@ export function placeToMarker(
|
|||||||
|
|
||||||
export function gpxToPolyline(gpx: string): L.Polyline {
|
export function gpxToPolyline(gpx: string): L.Polyline {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const gpxDoc = parser.parseFromString(gpx, "application/xml");
|
const gpxDoc = parser.parseFromString(gpx, 'application/xml');
|
||||||
|
|
||||||
const trkpts = Array.from(gpxDoc.querySelectorAll("trkpt"));
|
const trkpts = Array.from(gpxDoc.querySelectorAll('trkpt'));
|
||||||
const latlngs = trkpts.map(
|
const latlngs = trkpts.map(
|
||||||
(pt) =>
|
(pt) => [parseFloat(pt.getAttribute('lat')!), parseFloat(pt.getAttribute('lon')!)] as [number, number],
|
||||||
[
|
|
||||||
parseFloat(pt.getAttribute("lat")!),
|
|
||||||
parseFloat(pt.getAttribute("lon")!),
|
|
||||||
] as [number, number],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return L.polyline(latlngs, { color: "blue" });
|
return L.polyline(latlngs, { color: 'blue' });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Pipe, PipeTransform } from "@angular/core";
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: "orderBy",
|
name: 'orderBy',
|
||||||
pure: true,
|
pure: true,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
@ -9,8 +9,6 @@ export class orderByPipe implements PipeTransform {
|
|||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return items
|
return items.slice().sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0));
|
||||||
.slice()
|
|
||||||
.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,19 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
import { ButtonModule } from 'primeng/button';
|
||||||
Component,
|
import { MenuModule } from 'primeng/menu';
|
||||||
EventEmitter,
|
import { Place } from '../../types/poi';
|
||||||
Input,
|
import { MenuItem } from 'primeng/api';
|
||||||
OnInit,
|
import { UtilsService } from '../../services/utils.service';
|
||||||
Output,
|
import { Observable } from 'rxjs';
|
||||||
} from "@angular/core";
|
import { AsyncPipe } from '@angular/common';
|
||||||
import { ButtonModule } from "primeng/button";
|
import { LinkifyPipe } from '../linkify.pipe';
|
||||||
import { MenuModule } from "primeng/menu";
|
|
||||||
import { Place } from "../../types/poi";
|
|
||||||
import { MenuItem } from "primeng/api";
|
|
||||||
import { UtilsService } from "../../services/utils.service";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { AsyncPipe } from "@angular/common";
|
|
||||||
import { LinkifyPipe } from "../linkify.pipe";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-place-box",
|
selector: 'app-place-box',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ButtonModule, MenuModule, AsyncPipe, LinkifyPipe],
|
imports: [ButtonModule, MenuModule, AsyncPipe, LinkifyPipe],
|
||||||
templateUrl: "./place-box.component.html",
|
templateUrl: './place-box.component.html',
|
||||||
styleUrls: ["./place-box.component.scss"],
|
styleUrls: ['./place-box.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PlaceBoxComponent implements OnInit {
|
export class PlaceBoxComponent implements OnInit {
|
||||||
@ -43,33 +36,33 @@ export class PlaceBoxComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: 'Edit',
|
||||||
icon: "pi pi-pencil",
|
icon: 'pi pi-pencil',
|
||||||
iconClass: "text-blue-500!",
|
iconClass: 'text-blue-500!',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.editPlace();
|
this.editPlace();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Favorite",
|
label: 'Favorite',
|
||||||
icon: "pi pi-star",
|
icon: 'pi pi-star',
|
||||||
iconClass: "text-yellow-500!",
|
iconClass: 'text-yellow-500!',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.favoritePlace();
|
this.favoritePlace();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Mark",
|
label: 'Mark',
|
||||||
icon: "pi pi-check",
|
icon: 'pi pi-check',
|
||||||
iconClass: "text-green-500!",
|
iconClass: 'text-green-500!',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.visitPlace();
|
this.visitPlace();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Delete",
|
label: 'Delete',
|
||||||
icon: "pi pi-trash",
|
icon: 'pi pi-trash',
|
||||||
iconClass: "text-red-500!",
|
iconClass: 'text-red-500!',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.deletePlace();
|
this.deletePlace();
|
||||||
},
|
},
|
||||||
@ -78,9 +71,9 @@ export class PlaceBoxComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.selectedPlace?.gpx) {
|
if (this.selectedPlace?.gpx) {
|
||||||
items.unshift({
|
items.unshift({
|
||||||
label: "Display GPX",
|
label: 'Display GPX',
|
||||||
icon: "pi pi-compass",
|
icon: 'pi pi-compass',
|
||||||
iconClass: "text-gray-500!",
|
iconClass: 'text-gray-500!',
|
||||||
command: () => {
|
command: () => {
|
||||||
this.displayGPX();
|
this.displayGPX();
|
||||||
},
|
},
|
||||||
@ -89,7 +82,7 @@ export class PlaceBoxComponent implements OnInit {
|
|||||||
|
|
||||||
this.menuItems = [
|
this.menuItems = [
|
||||||
{
|
{
|
||||||
label: "Place",
|
label: 'Place',
|
||||||
items: items,
|
items: items,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
import { ButtonModule } from 'primeng/button';
|
||||||
Component,
|
import { Place } from '../../types/poi';
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
Output,
|
|
||||||
} from "@angular/core";
|
|
||||||
import { ButtonModule } from "primeng/button";
|
|
||||||
import { Place } from "../../types/poi";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-place-gpx",
|
selector: 'app-place-gpx',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ButtonModule],
|
imports: [ButtonModule],
|
||||||
templateUrl: "./place-gpx.component.html",
|
templateUrl: './place-gpx.component.html',
|
||||||
styleUrls: ["./place-gpx.component.scss"],
|
styleUrls: ['./place-gpx.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PlaceGPXComponent {
|
export class PlaceGPXComponent {
|
||||||
@ -33,6 +27,6 @@ export class PlaceGPXComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadTrace() {
|
downloadTrace() {
|
||||||
this.downloadEmitter.emit()
|
this.downloadEmitter.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Category, Place } from "./poi";
|
import { Category, Place } from './poi';
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Place } from "./poi";
|
import { Place } from './poi';
|
||||||
|
|
||||||
export interface TripBase {
|
export interface TripBase {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@ -2,6 +2,4 @@ import { bootstrapApplication } from '@angular/platform-browser';
|
|||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { AppComponent } from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
|
||||||
console.error(err),
|
|
||||||
);
|
|
||||||
|
|||||||
@ -388,24 +388,20 @@ export const TripThemePreset = definePreset(Aura, {
|
|||||||
overlay: {
|
overlay: {
|
||||||
select: {
|
select: {
|
||||||
borderRadius: '{border.radius.md}',
|
borderRadius: '{border.radius.md}',
|
||||||
shadow:
|
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
borderRadius: '{border.radius.md}',
|
borderRadius: '{border.radius.md}',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
shadow:
|
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
borderRadius: '{border.radius.xl}',
|
borderRadius: '{border.radius.xl}',
|
||||||
padding: '1.25rem',
|
padding: '1.25rem',
|
||||||
shadow:
|
shadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
||||||
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
shadow:
|
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
colorScheme: {
|
colorScheme: {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
@use "primeicons/primeicons.css";
|
@use 'primeicons/primeicons.css';
|
||||||
@plugin 'tailwindcss-primeui';
|
@plugin 'tailwindcss-primeui';
|
||||||
@variant dark (&:where(.dark, .dark *));
|
@variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@layer tailwind {
|
@layer tailwind {
|
||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@ -30,17 +30,17 @@
|
|||||||
html {
|
html {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family:
|
font-family:
|
||||||
"Inter",
|
'Inter',
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
"Segoe UI",
|
'Segoe UI',
|
||||||
Roboto,
|
Roboto,
|
||||||
Helvetica,
|
Helvetica,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif,
|
sans-serif,
|
||||||
"Apple Color Emoji",
|
'Apple Color Emoji',
|
||||||
"Segoe UI Emoji",
|
'Segoe UI Emoji',
|
||||||
"Segoe UI Symbol";
|
'Segoe UI Symbol';
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ html {
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-family: "Inter", sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user