🎨 Prettier setup

This commit is contained in:
itskovacs 2025-10-10 18:02:26 +02:00
parent aa17b78a40
commit 42654fb9a1
41 changed files with 1551 additions and 2103 deletions

View File

@ -1 +1,9 @@
{}
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"bracketSameLine": true,
"arrowParens": "always",
"semi": true,
"endOfLine": "lf"
}

View File

@ -1,18 +1,14 @@
import {
ApplicationConfig,
provideZoneChangeDetection,
isDevMode,
} from "@angular/core";
import { provideAnimationsAsync } from "@angular/platform-browser/animations/async";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";
import { providePrimeNG } from "primeng/config";
import { TripThemePreset } from "../mytheme";
import { MessageService } from "primeng/api";
import { provideHttpClient, withInterceptors } from "@angular/common/http";
import { Interceptor } from "./services/interceptor.service";
import { DialogService } from "primeng/dynamicdialog";
import { provideServiceWorker } from "@angular/service-worker";
import { ApplicationConfig, provideZoneChangeDetection, isDevMode } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { providePrimeNG } from 'primeng/config';
import { TripThemePreset } from '../mytheme';
import { MessageService } from 'primeng/api';
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 = {
providers: [
@ -27,19 +23,19 @@ export const appConfig: ApplicationConfig = {
theme: {
preset: TripThemePreset,
options: {
darkModeSelector: ".dark",
darkModeSelector: '.dark',
cssLayer: {
name: "primeng",
order: "tailwind, primeng",
name: 'primeng',
order: 'tailwind, primeng',
},
},
},
}),
MessageService,
DialogService,
provideServiceWorker("ngsw-worker.js", {
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: "registerWhenStable:30000",
registrationStrategy: 'registerWhenStable:30000',
}),
],
};

View File

@ -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 { AuthGuard } from "./services/auth.guard";
import { TripComponent } from "./components/trip/trip.component";
import { TripsComponent } from "./components/trips/trips.component";
import { SharedTripComponent } from "./components/shared-trip/shared-trip.component";
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { AuthGuard } from './services/auth.guard';
import { TripComponent } from './components/trip/trip.component';
import { TripsComponent } from './components/trips/trips.component';
import { SharedTripComponent } from './components/shared-trip/shared-trip.component';
export const routes: Routes = [
{
path: "auth",
pathMatch: "full",
path: 'auth',
pathMatch: 'full',
component: AuthComponent,
title: "TRIP - Authentication",
title: 'TRIP - Authentication',
},
{
path: "s",
path: 's',
children: [
{
path: "t/:token",
path: 't/:token',
component: SharedTripComponent,
title: "TRIP - Shared Trip",
title: 'TRIP - Shared Trip',
},
{ path: "**", redirectTo: "/home", pathMatch: "full" },
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
],
},
{
path: "",
path: '',
canActivate: [AuthGuard],
children: [
{
path: "home",
path: 'home',
component: DashboardComponent,
title: "TRIP - Map",
title: 'TRIP - Map',
},
{
path: "trips",
path: 'trips',
children: [
{
path: "",
path: '',
component: TripsComponent,
title: "TRIP - Trips",
title: 'TRIP - Trips',
},
{
path: ":id",
path: ':id',
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' },
];

View File

@ -1,4 +1,4 @@
.cover-auth {
background: url("/cover.webp");
background: url('/cover.webp');
background-size: cover;
}

View File

@ -1,24 +1,19 @@
import { Component, OnInit } from "@angular/core";
import { Component, OnInit } from '@angular/core';
import { FloatLabelModule } from "primeng/floatlabel";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { InputTextModule } from "primeng/inputtext";
import { ButtonModule } from "primeng/button";
import { FocusTrapModule } from "primeng/focustrap";
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";
import { FloatLabelModule } from 'primeng/floatlabel';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonModule } from 'primeng/button';
import { FocusTrapModule } from 'primeng/focustrap';
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({
selector: "app-auth",
selector: 'app-auth',
standalone: true,
imports: [
FloatLabelModule,
@ -29,8 +24,8 @@ import { take } from "rxjs";
FocusTrapModule,
MessageModule,
],
templateUrl: "./auth.component.html",
styleUrl: "./auth.component.scss",
templateUrl: './auth.component.html',
styleUrl: './auth.component.scss',
})
export class AuthComponent implements OnInit {
readonly redirectURL: string;
@ -45,31 +40,29 @@ export class AuthComponent implements OnInit {
private route: ActivatedRoute,
private fb: FormBuilder,
) {
this.redirectURL =
this.route.snapshot.queryParams["redirectURL"] || "/home";
this.redirectURL = this.route.snapshot.queryParams['redirectURL'] || '/home';
this.authForm = this.fb.group({
username: ["", { validators: Validators.required }],
password: ["", { validators: Validators.required }],
username: ['', { validators: Validators.required }],
password: ['', { validators: Validators.required }],
});
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const code = params["code"];
const state = params["state"];
const code = params['code'];
const state = params['state'];
if (code && state) {
this.authService.oidcLogin(code, state).subscribe({
next: (data) => {
if (!data.access_token) {
this.error = "Authentication failed";
this.error = 'Authentication failed';
return;
}
this.router.navigateByUrl(this.redirectURL);
},
error: (err: HttpErrorResponse) => {
this.error =
err.error.detail || "Login failed, check console for details";
this.error = err.error.detail || 'Login failed, check console for details';
},
});
} else {
@ -92,9 +85,7 @@ export class AuthComponent implements OnInit {
},
error: (err: HttpErrorResponse) => {
this.authForm.reset();
this.error =
err.error.detail ||
"Registration failed, check console for details";
this.error = 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({
next: (data) => {
if (!data.access_token) {
this.error = "Authentication failed";
this.error = 'Authentication failed';
return;
}
this.router.navigateByUrl(this.redirectURL);

View File

@ -1,48 +1,36 @@
import { AfterViewInit, Component, OnInit } from "@angular/core";
import { combineLatest, debounceTime, take, tap } from "rxjs";
import { Place, Category } from "../../types/poi";
import { ApiService } from "../../services/api.service";
import { PlaceBoxComponent } from "../../shared/place-box/place-box.component";
import * as L from "leaflet";
import "leaflet.markercluster";
import "leaflet-contextmenu";
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
import { PlaceCreateModalComponent } from "../../modals/place-create-modal/place-create-modal.component";
import { InputTextModule } from "primeng/inputtext";
import { SkeletonModule } from "primeng/skeleton";
import { TabsModule } from "primeng/tabs";
import { ToggleSwitchModule } from "primeng/toggleswitch";
import { FloatLabelModule } from "primeng/floatlabel";
import { BatchCreateModalComponent } from "../../modals/batch-create-modal/batch-create-modal.component";
import { UtilsService } from "../../services/utils.service";
import { Info } from "../../types/info";
import {
createMap,
placeToMarker,
createClusterGroup,
gpxToPolyline,
} from "../../shared/map";
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";
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { combineLatest, debounceTime, take, tap } from 'rxjs';
import { Place, Category } from '../../types/poi';
import { ApiService } from '../../services/api.service';
import { PlaceBoxComponent } from '../../shared/place-box/place-box.component';
import * as L from 'leaflet';
import 'leaflet.markercluster';
import 'leaflet-contextmenu';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
import { PlaceCreateModalComponent } from '../../modals/place-create-modal/place-create-modal.component';
import { InputTextModule } from 'primeng/inputtext';
import { SkeletonModule } from 'primeng/skeleton';
import { TabsModule } from 'primeng/tabs';
import { ToggleSwitchModule } from 'primeng/toggleswitch';
import { FloatLabelModule } from 'primeng/floatlabel';
import { BatchCreateModalComponent } from '../../modals/batch-create-modal/batch-create-modal.component';
import { UtilsService } from '../../services/utils.service';
import { Info } from '../../types/info';
import { createMap, placeToMarker, createClusterGroup, gpxToPolyline } from '../../shared/map';
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 {
text: string;
@ -60,7 +48,7 @@ export interface MarkerOptions extends L.MarkerOptions {
}
@Component({
selector: "app-dashboard",
selector: 'app-dashboard',
standalone: true,
imports: [
PlaceBoxComponent,
@ -78,11 +66,11 @@ export interface MarkerOptions extends L.MarkerOptions {
ButtonModule,
CommonModule,
],
templateUrl: "./dashboard.component.html",
styleUrls: ["./dashboard.component.scss"],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements OnInit, AfterViewInit {
searchInput = new FormControl("");
searchInput = new FormControl('');
info?: Info;
isLowNet = false;
isDarkMode = false;
@ -123,34 +111,27 @@ export class DashboardComponent implements OnInit, AfterViewInit {
) {
this.settingsForm = this.fb.group({
map_lat: [
"",
'',
{
validators: [
Validators.required,
Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
],
validators: [Validators.required, Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)')],
},
],
map_lng: [
"",
'',
{
validators: [
Validators.required,
Validators.pattern(
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
),
Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
],
},
],
currency: ["", Validators.required],
currency: ['', Validators.required],
do_not_display: [],
tile_layer: ["", Validators.required],
tile_layer: ['', Validators.required],
});
// HACK: Subscribe in constructor for takeUntilDestroyed
this.searchInput.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe({
this.searchInput.valueChanges.pipe(debounceTime(200), takeUntilDestroyed()).subscribe({
next: () => this.setVisibleMarkers(),
});
}
@ -195,27 +176,24 @@ export class DashboardComponent implements OnInit, AfterViewInit {
initMap(): void {
if (!this.settings) return;
const isTouch = "ontouchstart" in window;
const isTouch = 'ontouchstart' in window;
const contentMenuItems = [
{
text: "Add Point of Interest",
icon: "add-location.png",
text: 'Add Point of Interest',
icon: 'add-location.png',
callback: (e: any) => {
this.addPlaceModal(e);
},
},
];
this.map = createMap(
isTouch ? [] : contentMenuItems,
this.settings?.tile_layer,
);
this.map = createMap(isTouch ? [] : contentMenuItems, this.settings?.tile_layer);
if (isTouch) {
this.map.on("contextmenu", (e: any) => {
this.map.on('contextmenu', (e: any) => {
this.addPlaceModal(e);
});
}
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);
}
@ -223,30 +201,22 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (!this.viewMarkersList || !this.map) return;
const bounds = this.map.getBounds();
this.visiblePlaces = this.filteredPlaces.filter((p) =>
bounds.contains([p.lat, p.lng]),
);
this.visiblePlaces = this.filteredPlaces.filter((p) => bounds.contains([p.lat, p.lng]));
const searchValue = this.searchInput.value?.toLowerCase() ?? "";
const searchValue = this.searchInput.value?.toLowerCase() ?? '';
if (searchValue)
this.visiblePlaces = this.visiblePlaces.filter(
(p) =>
p.name.toLowerCase().includes(searchValue) ||
p.description?.toLowerCase().includes(searchValue),
(p) => p.name.toLowerCase().includes(searchValue) || p.description?.toLowerCase().includes(searchValue),
);
this.visiblePlaces.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.visiblePlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
resetFilters() {
this.filter_display_visited = false;
this.filter_display_favorite_only = false;
this.activeCategories = new Set(this.categories.map((c) => c.name));
this.settings?.do_not_display.forEach((c) =>
this.activeCategories.delete(c),
);
this.settings?.do_not_display.forEach((c) => this.activeCategories.delete(c));
this.updateMarkersAndClusters();
if (this.viewMarkersList) this.setVisibleMarkers();
}
@ -278,47 +248,38 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}
_placeToMarker(place: Place): L.Marker {
const marker = placeToMarker(
place,
this.isLowNet,
place.visited,
this.isGpxInPlaceMode,
);
const marker = placeToMarker(place, this.isLowNet, place.visited, this.isGpxInPlaceMode);
marker
.on("click", (e) => {
.on('click', (e) => {
this.selectedPlace = { ...place };
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();
this.map?.setView(toView);
})
.on("contextmenu", () => {
if (this.map && (this.map as any).contextmenu)
(this.map as any).contextmenu.hide();
.on('contextmenu', () => {
if (this.map && (this.map as any).contextmenu) (this.map as any).contextmenu.hide();
});
return marker;
}
addPlaceModal(e?: any): void {
const opts = e ? { data: { place: e.latlng } } : {};
const modal: DynamicDialogRef = this.dialogService.open(
PlaceCreateModalComponent,
{
header: "Create Place",
const modal: DynamicDialogRef = this.dialogService.open(PlaceCreateModalComponent, {
header: 'Create Place',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
width: "55vw",
width: '55vw',
breakpoints: {
"1920px": "70vw",
"1260px": "90vw",
'1920px': '70vw',
'1260px': '90vw',
},
...opts,
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (place: Place | null) => {
@ -329,9 +290,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe({
next: (place: Place) => {
this.places = [...this.places, place].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.places = [...this.places, place].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
setTimeout(() => {
this.updateMarkersAndClusters();
}, 10);
@ -342,21 +301,18 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}
batchAddModal() {
const modal: DynamicDialogRef = this.dialogService.open(
BatchCreateModalComponent,
{
header: "Create Places",
const modal: DynamicDialogRef = this.dialogService.open(BatchCreateModalComponent, {
header: 'Create Places',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
width: "55vw",
width: '55vw',
breakpoints: {
"1920px": "70vw",
"1260px": "90vw",
'1920px': '70vw',
'1260px': '90vw',
},
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (places: string | null) => {
@ -367,7 +323,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
parsedPlaces = JSON.parse(places);
if (!Array.isArray(parsedPlaces)) throw new Error();
} catch (err) {
this.utilsService.toast("error", "Error", "Content looks invalid");
this.utilsService.toast('error', 'Error', 'Content looks invalid');
return;
}
@ -375,9 +331,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.postPlaces(parsedPlaces)
.pipe(take(1))
.subscribe((places) => {
this.places = [...this.places, ...places].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.places = [...this.places, ...places].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
setTimeout(() => {
this.updateMarkersAndClusters();
}, 10);
@ -388,7 +342,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
resetHoverPlace() {
if (!this.hoveredElement) return;
this.hoveredElement.classList.remove("list-hover");
this.hoveredElement.classList.remove('list-hover');
this.hoveredElement = undefined;
}
@ -405,17 +359,15 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (markerElement) {
// marker, not clustered
markerElement.classList.add("list-hover");
markerElement.classList.add('list-hover');
this.hoveredElement = markerElement;
} else {
// marker is clustered
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
marker,
);
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(marker);
if (parentCluster) {
const clusterEl = parentCluster.getElement();
if (clusterEl) {
clusterEl.classList.add("list-hover");
clusterEl.classList.add('list-hover');
this.hoveredElement = clusterEl;
}
}
@ -431,11 +383,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe({
next: () => {
const idx = this.places.findIndex(
(p) => p.id === this.selectedPlace!.id,
);
if (idx !== -1)
this.places[idx] = { ...this.places[idx], favorite: favoriteBool };
const idx = this.places.findIndex((p) => p.id === this.selectedPlace!.id);
if (idx !== -1) this.places[idx] = { ...this.places[idx], favorite: favoriteBool };
this.selectedPlace = { ...this.places[idx] };
this.updateMarkersAndClusters();
},
@ -451,11 +400,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe({
next: () => {
const idx = this.places.findIndex(
(p) => p.id === this.selectedPlace!.id,
);
if (idx !== -1)
this.places[idx] = { ...this.places[idx], visited: visitedBool };
const idx = this.places.findIndex((p) => p.id === this.selectedPlace!.id);
if (idx !== -1) this.places[idx] = { ...this.places[idx], visited: visitedBool };
this.selectedPlace = { ...this.places[idx] };
this.updateMarkersAndClusters();
},
@ -466,12 +412,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (!this.selectedPlace) return;
const modal = this.dialogService.open(YesNoModalComponent, {
header: "Confirm deletion",
header: 'Confirm deletion',
modal: true,
closable: true,
dismissableMask: true,
breakpoints: {
"640px": "90vw",
'640px': '90vw',
},
data: `Delete ${this.selectedPlace.name} ?`,
})!;
@ -484,9 +430,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe({
next: () => {
this.places = this.places.filter(
(p) => p.id !== this.selectedPlace!.id,
);
this.places = this.places.filter((p) => p.id !== this.selectedPlace!.id);
this.closePlaceBox();
this.updateMarkersAndClusters();
if (this.viewMarkersList) this.setVisibleMarkers();
@ -500,18 +444,16 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (!this.selectedPlace && !p) return;
const _placeToEdit: Place = { ...(this.selectedPlace ?? p)! };
const modal: DynamicDialogRef = this.dialogService.open(
PlaceCreateModalComponent,
{
header: "Edit Place",
const modal: DynamicDialogRef = this.dialogService.open(PlaceCreateModalComponent, {
header: 'Edit Place',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
width: "55vw",
width: '55vw',
breakpoints: {
"1920px": "70vw",
"1260px": "90vw",
'1920px': '70vw',
'1260px': '90vw',
},
data: {
place: {
@ -519,8 +461,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
category: _placeToEdit.category.id,
},
},
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (place: Place | null) => {
@ -534,9 +475,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
const places = [...this.places];
const idx = places.findIndex((p) => p.id == place.id);
if (idx > -1) places.splice(idx, 1, place);
places.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
places.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
this.places = places;
if (this.selectedPlace) this.selectedPlace = { ...place };
setTimeout(() => {
@ -551,21 +490,20 @@ export class DashboardComponent implements OnInit, AfterViewInit {
displayGPXOnMap(gpx: string) {
if (!this.map || !this.selectedPlace) return;
if (!this.gpxLayerGroup)
this.gpxLayerGroup = L.layerGroup().addTo(this.map);
if (!this.gpxLayerGroup) this.gpxLayerGroup = L.layerGroup().addTo(this.map);
this.gpxLayerGroup.clearLayers();
try {
const gpxPolyline = gpxToPolyline(gpx);
const selectedPlaceWithGPX = { ...this.selectedPlace, gpx };
gpxPolyline.on("click", () => {
gpxPolyline.on('click', () => {
this.selectedGPX = selectedPlaceWithGPX;
});
this.gpxLayerGroup?.addLayer(gpxPolyline);
this.map.fitBounds(gpxPolyline.getBounds(), { padding: [20, 20] });
} catch {
this.utilsService.toast("error", "Error", "Couldn't parse GPX data");
this.utilsService.toast('error', 'Error', "Couldn't parse GPX data");
}
this.closePlaceBox();
}
@ -578,11 +516,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.subscribe({
next: (p) => {
if (!p.gpx) {
this.utilsService.toast(
"error",
"Error",
"Couldn't retrieve GPX data",
);
this.utilsService.toast('error', 'Error', "Couldn't retrieve GPX data");
return;
}
this.displayGPXOnMap(p.gpx);
@ -597,7 +531,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
this.settingsForm.reset(this.settings);
this.doNotDisplayOptions = [
{
label: "Categories",
label: 'Categories',
items: this.categories.map((c) => ({ label: c.name, value: c.name })),
},
];
@ -610,13 +544,13 @@ export class DashboardComponent implements OnInit, AfterViewInit {
toggleMarkersList() {
this.viewMarkersList = !this.viewMarkersList;
this.viewMarkersListSearch = false;
this.searchInput.setValue("");
this.searchInput.setValue('');
if (this.viewMarkersList) this.setVisibleMarkers();
}
toggleMarkersListSearch() {
this.viewMarkersListSearch = !this.viewMarkersListSearch;
if (this.viewMarkersListSearch) this.searchInput.setValue("");
if (this.viewMarkersListSearch) this.searchInput.setValue('');
}
setMapCenterToCurrent() {
@ -631,7 +565,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (!input.files?.length) return;
const formdata = new FormData();
formdata.append("file", input.files[0]);
formdata.append('file', input.files[0]);
this.apiService
.settingsUserImport(formdata)
@ -667,12 +601,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe((resp: Object) => {
const dataBlob = new Blob([JSON.stringify(resp, null, 2)], {
type: "application/json",
type: 'application/json',
});
const downloadURL = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
const link = document.createElement('a');
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.remove();
URL.revokeObjectURL(downloadURL);
@ -699,22 +633,19 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}
editCategory(c: Category) {
const modal: DynamicDialogRef = this.dialogService.open(
CategoryCreateModalComponent,
{
header: "Update Category",
const modal: DynamicDialogRef = this.dialogService.open(CategoryCreateModalComponent, {
header: 'Update Category',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
data: { category: c },
width: "40vw",
width: '40vw',
breakpoints: {
"960px": "70vw",
"640px": "90vw",
'960px': '70vw',
'640px': '90vw',
},
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (category: Category | null) => {
@ -725,17 +656,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.pipe(take(1))
.subscribe({
next: (updated) => {
this.categories = this.categories.map((cat) =>
cat.id === updated.id ? updated : cat,
);
this.categories = this.categories.map((cat) => (cat.id === updated.id ? updated : cat));
this.sortCategories();
this.activeCategories = new Set(
this.categories.map((c) => c.name),
);
this.activeCategories = new Set(this.categories.map((c) => c.name));
this.places = this.places.map((p) => {
if (p.category.id == updated.id)
return { ...p, category: updated };
if (p.category.id == updated.id) return { ...p, category: updated };
return p;
});
setTimeout(() => {
@ -748,21 +674,18 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}
addCategory() {
const modal: DynamicDialogRef = this.dialogService.open(
CategoryCreateModalComponent,
{
header: "Create Category",
const modal: DynamicDialogRef = this.dialogService.open(CategoryCreateModalComponent, {
header: 'Create Category',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
width: "40vw",
width: '40vw',
breakpoints: {
"960px": "70vw",
"640px": "90vw",
'960px': '70vw',
'640px': '90vw',
},
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (category: Category | null) => {
@ -774,9 +697,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.subscribe({
next: (category: Category) => {
this.categories.push(category);
this.categories.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.categories.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
this.activeCategories.add(category.name);
},
});
@ -786,14 +707,14 @@ export class DashboardComponent implements OnInit, AfterViewInit {
deleteCategory(c_id: number) {
const modal = this.dialogService.open(YesNoModalComponent, {
header: "Confirm deletion",
header: 'Confirm deletion',
modal: true,
closable: true,
dismissableMask: true,
breakpoints: {
"640px": "90vw",
'640px': '90vw',
},
data: "Delete this category ?",
data: 'Delete this category ?',
})!;
modal.onClose.pipe(take(1)).subscribe({
@ -806,9 +727,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
next: () => {
this.categories = this.categories.filter((c) => c.id !== c_id);
this.activeCategories = new Set(
this.categories.map((c) => c.name),
);
this.activeCategories = new Set(this.categories.map((c) => c.name));
},
});
},
@ -824,13 +743,11 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}
sortCategories() {
this.categories = [...this.categories].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.categories = [...this.categories].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
navigateToTrips() {
this.router.navigateByUrl("/trips");
this.router.navigateByUrl('/trips');
}
logout() {
@ -849,7 +766,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
if (!this.selectedGPX?.gpx) return;
const dataBlob = new Blob([this.selectedGPX.gpx]);
const downloadURL = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
const link = document.createElement('a');
link.href = downloadURL;
link.download = `TRIP_${this.selectedGPX.name}.gpx`;
link.click();
@ -874,13 +791,8 @@ export class DashboardComponent implements OnInit, AfterViewInit {
.subscribe({
next: (remote_version) => {
if (!remote_version)
this.utilsService.toast(
"success",
"Latest version",
"You're running the latest version of TRIP",
);
if (this.info && remote_version != this.info?.version)
this.info.update = remote_version;
this.utilsService.toast('success', 'Latest version', "You're running the latest version of TRIP");
if (this.info && remote_version != this.info?.version) this.info.update = remote_version;
},
});
}

View File

@ -8,16 +8,16 @@
<div class="mt-1 flex items-center">
<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">{{
trip.days.length }} {{ trip.days!.length > 1 ? 'days' : 'day'}}</span>
trip.days.length }} {{ trip.days!.length > 1 ? 'days' : 'day' }}</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">{{
(totalPrice | number:'1.0-2') || '-' }} {{ trip.currency }}</span>
(totalPrice | number: '1.0-2') || '-' }} {{ trip.currency }}</span>
</div>
</div>
</div>
<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>
<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"
(click)="isExpanded = !isExpanded" text />
<p-button [label]="tableExpandableMode ? 'Ungroup' : 'Group'"
[pTooltip]="tableExpandableMode ? 'Switch table mode' : 'Switch table mode, allow column resizing'"
[icon]="tableExpandableMode ? '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 />
[pTooltip]="tableExpandableMode ? 'Switch table mode' : 'Switch table mode, allow column resizing'" [icon]="
tableExpandableMode
? '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()"
text />
<p-button label="Highlight" pTooltip="Show itinerary on map" icon="pi pi-directions"
@ -73,20 +75,37 @@
@defer {
@if (flattenedTripItems.length) {
<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">
<ng-template #header>
<tr>
@if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {<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('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>}
@if (!tableExpandableMode && tripTableSelectedColumns.includes('day')) {
<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('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>
</ng-template>
@if (tableExpandableMode) {
@ -95,9 +114,10 @@
<td colspan="8">
<div class="flex items-center gap-2 w-full">
<button type="button" pButton pRipple [pRowToggler]="tripitem" text rounded plain
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'">
</button>
<span class="font-bold w-xs max-w-xs min-w-0 inline-block truncate">{{ tripitem.td_label }}</span>
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
<span class="font-bold w-xs max-w-xs min-w-0 inline-block truncate">{{
tripitem.td_label
}}</span>
<p-button class="ml-2" label="Highlight" pTooltip="Show itinerary on map" text icon="pi pi-directions"
[severity]="tripMapAntLayerDayID == tripitem.day_id ? 'help' : 'primary'"
@ -112,57 +132,87 @@
<ng-template #expandedrow let-tripitem>
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
(click)="onRowClick(tripitem)">
@if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
@if (tripTableSelectedColumns.includes('text')) {<td class="relative">
@if (tripTableSelectedColumns.includes('time')) {
<td class="font-mono text-sm">{{ tripitem.time }}</td>
}
@if (tripTableSelectedColumns.includes('text')) {
<td class="relative">
<div class="truncate">
@if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
[style.background]="tripitem.status.color"></div>}
@if (tripitem.status) {
<div class="block absolute top-3 left-1.5 size-2 rounded-full" [style.background]="tripitem.status.color">
</div>
}
{{ tripitem.text }}
</div>
</td>}
@if (tripTableSelectedColumns.includes('place')) {<td class="relative">
</td>
}
@if (tripTableSelectedColumns.includes('place')) {
<td class="relative">
@if (tripitem.place) {
<div class="ml-7 print:ml-0 truncate print:whitespace-normal">
<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" /> {{
tripitem.place.name }}
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" />
{{ tripitem.place.name }}
</div>
} @else {-}
</td>}
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
} @else {
-
}
</td>
}
@if (tripTableSelectedColumns.includes('comment')) {
<td class="relative">
@if (tripitem.image) {
<div
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
<img [src]="tripitem.image"
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover" /> {{
tripitem.comment }}
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover" />
{{ tripitem.comment }}
</div>
} @else {
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
{{ tripitem.comment || '-' }}
</div>
}
</td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
</td>
}
@if (tripTableSelectedColumns.includes('LatLng')) {
<td class="font-mono text-sm">
<div class="print:max-w-full truncate">
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
@else {-}
@if (tripitem.lat) {
{{ tripitem.lat }}, {{ tripitem.lng }}
} @else {
-
}
</div>
</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">{{
tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</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">
</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">{{ tripitem.price }}
@if (tripitem.price) {
{{ trip.currency }}
}
</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>
</td>}
</td>
}
</tr>
</ng-template>
}
@else {
} @else {
<ng-template #body let-tripitem let-rowgroup="rowgroup" let-rowspan="rowspan">
<tr class="h-12 cursor-pointer" [class.font-bold]="selectedItem?.id === tripitem.id"
(click)="onRowClick(tripitem)">
@ -170,18 +220,25 @@
<td [attr.rowspan]="rowspan" class="font-normal! max-w-20 truncate cursor-pointer"
[class.text-blue-500]="tripMapAntLayerDayID == tripitem.day_id"
(click)="toggleTripDayHighlight(tripitem.day_id); $event.stopPropagation()">
<div class="truncate">{{tripitem.td_label }}</div>
<div class="truncate">{{ tripitem.td_label }}</div>
</td>
}
@if (tripTableSelectedColumns.includes('time')) {<td class="font-mono text-sm">{{ tripitem.time }}</td>}
@if (tripTableSelectedColumns.includes('text')) {<td class="relative max-w-60">
@if (tripTableSelectedColumns.includes('time')) {
<td class="font-mono text-sm">{{ tripitem.time }}</td>
}
@if (tripTableSelectedColumns.includes('text')) {
<td class="relative max-w-60">
<div class="truncate">
@if (tripitem.status) {<div class="block absolute top-3 left-1.5 size-2 rounded-full"
[style.background]="tripitem.status.color"></div>}
@if (tripitem.status) {
<div class="block absolute top-3 left-1.5 size-2 rounded-full" [style.background]="tripitem.status.color">
</div>
}
{{ tripitem.text }}
</div>
</td>}
@if (tripTableSelectedColumns.includes('place')) {<td>
</td>
}
@if (tripTableSelectedColumns.includes('place')) {
<td>
@if (tripitem.place) {
<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">
@ -190,38 +247,62 @@
</div>
<span class="text-sm truncate min-w-0">{{ tripitem.place.name }}</span>
</div>
} @else {-}
</td>}
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
} @else {
-
}
</td>
}
@if (tripTableSelectedColumns.includes('comment')) {
<td class="relative">
@if (tripitem.image) {
<div
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
<img [src]="tripitem.image"
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
tripitem.comment }}
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" />
{{ tripitem.comment }}
</div>
} @else {
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
{{ tripitem.comment || '-' }}
</div>
}
</td>}
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
</td>
}
@if (tripTableSelectedColumns.includes('LatLng')) {
<td class="font-mono text-sm">
<div class="max-w-20 print:max-w-full truncate">
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} }
@else {-}
@if (tripitem.lat) {
{{ tripitem.lat }}, {{ tripitem.lng }}
} @else {
-
}
</div>
</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">{{
tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</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">
</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">{{ tripitem.price }}
@if (tripitem.price) {
{{ trip.currency }}
}
</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>
</td>}
</td>
}
</tr>
</ng-template>
}
@ -234,9 +315,7 @@
</h2>
</div>
</div>
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">
No Trip
</div>
<div class="hidden print:block text-center text-sm text-gray-500 mt-4">No Trip</div>
}
} @placeholder (minimum 0.4s) {
<div class="h-[400px] w-full">
@ -257,8 +336,7 @@
}
<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>
<h1 class="text-start font-semibold mb-0 line-clamp-1 md:text-xl">{{ selectedItem.text }}</h1>
<div class="flex items-center gap-2 flex-none">
@if (selectedItem.gpx) {
@ -302,16 +380,23 @@
<div class="rounded-md shadow p-4">
<p class="font-bold mb-1 truncate">Latitude, Longitude</p>
<p class="text-sm text-gray-500 truncate cursor-copy"
[cdkCopyToClipboard]="selectedItem.lat + ',' + selectedItem.lng">{{
selectedItem.lat }}, {{ selectedItem.lng }}</p>
[cdkCopyToClipboard]="selectedItem.lat + ',' + selectedItem.lng">
{{ selectedItem.lat }}, {{ selectedItem.lng }}
</p>
</div>
}
@if (selectedItem.price) {
<div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Price</p>
<p class="text-sm text-gray-500">{{ selectedItem.price }} @if (selectedItem.price) { {{ trip.currency }} }
@if (selectedItem.paid_by) {<span class="text-xs text-gray-500">(by {{ selectedItem.paid_by }})</span>}
<p class="text-sm text-gray-500">
{{ selectedItem.price }}
@if (selectedItem.price) {
{{ trip.currency }}
}
@if (selectedItem.paid_by) {
<span class="text-xs text-gray-500">(by {{ selectedItem.paid_by }})</span>
}
</p>
</div>
}
@ -319,9 +404,8 @@
@if (selectedItem.status) {
<div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Status</p>
<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">{{
selectedItem.status.label }}</span>
<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">{{ selectedItem.status.label }}</span>
</div>
}
@ -349,8 +433,7 @@
</div>
</div>
<div id="map" [class.fullscreen-map]="isMapFullscreen" class="w-full rounded-md min-h-96 h-1/3 max-h-full">
</div>
<div id="map" [class.fullscreen-map]="isMapFullscreen" class="w-full rounded-md min-h-96 h-1/3 max-h-full"></div>
</div>
@if (!selectedItem) {
@ -370,7 +453,8 @@
<div class="flex items-center">
@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">{{
places.length }}</span>
places.length
}}</span>
} @placeholder (minimum 0.4s) {
<p-skeleton height="1.75rem" width="2.5rem" class="mr-1" />
}
@ -383,7 +467,7 @@
@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"
(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">
<h1 class="tracking-tight truncate dark:text-surface-300">{{ p.name }}</h1>
@ -404,8 +488,11 @@
<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">{{
p.price || '-'
}} @if (p.price) { {{ trip.currency }} }</span>
p.price || '-' }}
@if (p.price) {
{{ trip.currency }}
}
</span>
@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
@ -419,7 +506,7 @@
}
} @placeholder (minimum 0.4s) {
<div class="flex flex-col gap-4">
@for (_ of [1,2,3]; track _) {
@for (_ of [1, 2, 3]; track _) {
<div class="h-16">
<p-skeleton height="100%" />
</div>
@ -454,9 +541,14 @@
</div>
<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">{{
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">{{
getDayStats(d).places }}</span>
getDayStats(d).places
}}</span>
</div>
</div>
} @empty {
@ -474,16 +566,19 @@
<div class="p-4 shadow rounded-md w-full min-h-20">
<div class="p-2 mb-2 flex justify-between items-center">
<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"
(click)="toGithub()" /></div>
<div class="flex items-center gap-1 text-gray-500">
<p-button text label="itskovacs/trip" icon="pi pi-github" (click)="toGithub()" />
</div>
</div>
<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
keeping it free, your support simply helps keep the project going. Thank you! ❤️</div>
<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 coffee</a>
<a href="https://ko-fi.com/itskovacs" target="_blank" class="custom-button flex items-center">Buy me a
coffee</a>
</div>
</div>
}
@ -505,12 +600,17 @@
[class.prettyprint]="isPrinting">
@if (isMapFullscreenDays) {
<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) {
<button (click)="toggleTripDayHighlight(day.id)"
[ngClass]="tripMapAntLayerDayID === day.id ? 'shadow-md bg-blue-500' : 'hover:bg-blue-50 dark:hover:bg-blue-950/20'"
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'">
@for (day of trip.days; track day.id) {
<button (click)="toggleTripDayHighlight(day.id)" [ngClass]="
tripMapAntLayerDayID === day.id
? 'shadow-md bg-blue-500'
: 'hover:bg-blue-50 dark:hover:bg-blue-950/20'
" 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 }}
</span>
<span class="flex-1 text-left text-sm font-medium truncate"
@ -537,7 +637,7 @@
</div>
<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>
<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">
<p-checkbox disabled [binary]="true" [inputId]="item.id.toString()" [(ngModel)]="item.packed" />
<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>
</div>
</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">
<label [for]="item.id" class="flex items-center gap-2 w-full">
<div class="relative">
@if (item.status) {<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full"
[style.background]="item.status.color"></div>}
@if (item.status) {
<div class="z-50 block absolute top-0 left-3 size-2.5 rounded-full" [style.background]="item.status.color">
</div>
}
<p-checkbox disabled />
</div>
<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 flex-col items-center max-w-[55vw] md:max-w-full">
<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>
@ -620,13 +724,11 @@
<div class="flex items-center">
<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="pi pi-calendar text-xs"></i> {{
trip?.days?.length }} {{ trip?.days!.length > 1 ? 'days' :
'day'}}</span>
class="pi pi-calendar text-xs"></i> {{ trip?.days?.length }}
{{ (trip?.days)!.length > 1 ? 'days' : 'day' }}</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="pi pi-wallet text-xs"></i> {{
(totalPrice | number:'1.0-2') || '-' }} {{ trip?.currency }}</span>
class="pi pi-wallet text-xs"></i> {{ (totalPrice | number: '1.0-2') || '-' }} {{ trip?.currency }}</span>
</div>
</div>
</div>
@ -635,8 +737,7 @@
<div class="text-2xl font-semibold">Notes</div>
<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>
<p class="text-sm leading-relaxed text-gray-800 whitespace-pre-line">{{ trip?.notes || 'Nothing there.' }}</p>
</div>
</div>
@ -680,9 +781,7 @@
@if (item.status) {
<span class="text-xs font-medium px-2.5 py-1 rounded min-w-fit"
[style.background]="statusToTripStatus(item.status)?.color + '1A'"
[style.color]="statusToTripStatus(item.status)?.color">{{
statusToTripStatus(item.status)?.label
}}</span>
[style.color]="statusToTripStatus(item.status)?.color">{{ statusToTripStatus(item.status)?.label }}</span>
}
</div>
</div>
@ -700,7 +799,7 @@
<div class="text-2xl font-semibold">📍 Places</div>
<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="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" />

View File

@ -1,47 +1,33 @@
import { AfterViewInit, Component } from "@angular/core";
import { ApiService } from "../../services/api.service";
import { ButtonModule } from "primeng/button";
import { SkeletonModule } from "primeng/skeleton";
import * as L from "leaflet";
import { antPath } from "leaflet-ant-path";
import { TableModule } from "primeng/table";
import {
Trip,
FlattenedTripItem,
TripDay,
TripItem,
TripStatus,
PackingItem,
ChecklistItem,
} from "../../types/trip";
import { Place } from "../../types/poi";
import {
createMap,
placeToMarker,
createClusterGroup,
tripDayMarker,
gpxToPolyline,
} from "../../shared/map";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, take, tap } from "rxjs";
import { UtilsService } from "../../services/utils.service";
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";
import { AfterViewInit, Component } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { ButtonModule } from 'primeng/button';
import { SkeletonModule } from 'primeng/skeleton';
import * as L from 'leaflet';
import { antPath } from 'leaflet-ant-path';
import { TableModule } from 'primeng/table';
import { Trip, FlattenedTripItem, TripDay, TripItem, TripStatus, PackingItem, ChecklistItem } from '../../types/trip';
import { Place } from '../../types/poi';
import { createMap, placeToMarker, createClusterGroup, tripDayMarker, gpxToPolyline } from '../../shared/map';
import { ActivatedRoute } from '@angular/router';
import { debounceTime, take, tap } from 'rxjs';
import { UtilsService } from '../../services/utils.service';
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({
selector: "app-shared-trip",
selector: 'app-shared-trip',
standalone: true,
imports: [
CommonModule,
@ -61,8 +47,8 @@ import { orderByPipe } from "../../shared/order-by.pipe";
ClipboardModule,
orderByPipe,
],
templateUrl: "./shared-trip.component.html",
styleUrls: ["./shared-trip.component.scss"],
templateUrl: './shared-trip.component.html',
styleUrls: ['./shared-trip.component.scss'],
})
export class SharedTripComponent implements AfterViewInit {
token?: string;
@ -98,18 +84,18 @@ export class SharedTripComponent implements AfterViewInit {
readonly menuTripActionsItems: MenuItem[] = [
{
label: "Lists",
label: 'Lists',
items: [
{
label: "Checklist",
icon: "pi pi-check-square",
label: 'Checklist',
icon: 'pi pi-check-square',
command: () => {
this.openChecklist();
},
},
{
label: "Packing",
icon: "pi pi-briefcase",
label: 'Packing',
icon: 'pi pi-briefcase',
command: () => {
this.openPackingList();
},
@ -117,11 +103,11 @@ export class SharedTripComponent implements AfterViewInit {
],
},
{
label: "Trip",
label: 'Trip',
items: [
{
label: "Pretty Print",
icon: "pi pi-print",
label: 'Pretty Print',
icon: 'pi pi-print',
command: () => {
this.togglePrint();
},
@ -131,11 +117,11 @@ export class SharedTripComponent implements AfterViewInit {
];
readonly menuTripTableActionsItems: MenuItem[] = [
{
label: "Actions",
label: 'Actions',
items: [
{
label: "Pretty Print",
icon: "pi pi-print",
label: 'Pretty Print',
icon: 'pi pi-print',
command: () => {
this.togglePrint();
},
@ -143,18 +129,18 @@ export class SharedTripComponent implements AfterViewInit {
],
},
{
label: "Table",
label: 'Table',
items: [
{
label: "Filter",
icon: "pi pi-filter",
label: 'Filter',
icon: 'pi pi-filter',
command: () => {
this.toggleFiltering();
},
},
{
label: "Group",
icon: "pi pi-arrow-down-left-and-arrow-up-right-to-center",
label: 'Group',
icon: 'pi pi-arrow-down-left-and-arrow-up-right-to-center',
command: () => {
this.tableExpandableMode = !this.tableExpandableMode;
},
@ -162,18 +148,18 @@ export class SharedTripComponent implements AfterViewInit {
],
},
{
label: "Directions",
label: 'Directions',
items: [
{
label: "Highlight",
icon: "pi pi-directions",
label: 'Highlight',
icon: 'pi pi-directions',
command: () => {
this.toggleTripDaysHighlight();
},
},
{
label: "GMaps itinerary",
icon: "pi pi-car",
label: 'GMaps itinerary',
icon: 'pi pi-car',
command: () => {
this.tripToNavigation();
},
@ -182,24 +168,18 @@ export class SharedTripComponent implements AfterViewInit {
},
];
readonly tripTableColumns: string[] = [
"day",
"time",
"text",
"place",
"comment",
"LatLng",
"price",
"status",
"distance",
'day',
'time',
'text',
'place',
'comment',
'LatLng',
'price',
'status',
'distance',
];
tripTableSelectedColumns: string[] = [
"day",
"time",
"text",
"place",
"comment",
];
tripTableSearchInput = new FormControl("");
tripTableSelectedColumns: string[] = ['day', 'time', 'text', 'place', 'comment'];
tripTableSearchInput = new FormControl('');
dayStatsCache = new Map<number, { price: number; places: number }>();
placesUsedInTable = new Set<number>();
@ -210,9 +190,7 @@ export class SharedTripComponent implements AfterViewInit {
private route: ActivatedRoute,
) {
this.statuses = this.utilsService.statuses;
this.tripTableSearchInput.valueChanges
.pipe(debounceTime(300), takeUntilDestroyed())
.subscribe({
this.tripTableSearchInput.valueChanges.pipe(debounceTime(300), takeUntilDestroyed()).subscribe({
next: (value) => {
if (value) this.flattenTripDayItems(value.toLowerCase());
else this.flattenTripDayItems();
@ -225,7 +203,7 @@ export class SharedTripComponent implements AfterViewInit {
.pipe(
take(1),
tap((params) => {
const token = params.get("token");
const token = params.get('token');
if (token) {
this.token = token;
this.loadTripData(token);
@ -252,12 +230,10 @@ export class SharedTripComponent implements AfterViewInit {
initMap(): void {
const contentMenuItems = [
{
text: "Copy coordinates",
text: 'Copy coordinates',
callback: (e: any) => {
const latlng = e.latlng;
navigator.clipboard.writeText(
`${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`,
);
navigator.clipboard.writeText(`${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`);
},
},
];
@ -305,11 +281,7 @@ export class SharedTripComponent implements AfterViewInit {
if (!this.trip?.days) return [];
return this.trip.days
.flatMap((day) =>
day.items.filter((item) =>
["constraint", "pending"].includes(item.status as string),
),
)
.flatMap((day) => day.items.filter((item) => ['constraint', 'pending'].includes(item.status as string)))
.map((item) => ({
...item,
status: this.statusToTripStatus(item.status as string),
@ -383,9 +355,7 @@ export class SharedTripComponent implements AfterViewInit {
setPlacesAndMarkers() {
this.computePlacesUsedInTable();
this.places = [...(this.trip?.places ?? [])].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.places = [...(this.trip?.places ?? [])].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
this.markerClusterGroup?.clearLayers();
this.places.forEach((p) => {
const marker = this._placeToMarker(p);
@ -394,12 +364,8 @@ export class SharedTripComponent implements AfterViewInit {
}
_placeToMarker(place: Place): L.Marker {
const marker = placeToMarker(
place,
false,
!this.placesUsedInTable.has(place.id),
);
marker.on("click", () => {
const marker = placeToMarker(place, false, !this.placesUsedInTable.has(place.id));
marker.on('click', () => {
this.onMapMarkerClick(place.id);
marker.closeTooltip();
});
@ -409,9 +375,7 @@ export class SharedTripComponent implements AfterViewInit {
resetMapBounds() {
if (!this.places.length) {
this.map?.fitBounds(
this.flattenedTripItems
.filter((i) => i.lat != null && i.lng != null)
.map((i) => [i.lat!, i.lng!]),
this.flattenedTripItems.filter((i) => i.lat != null && i.lng != null).map((i) => [i.lat!, i.lng!]),
{ padding: [30, 30] },
);
return;
@ -425,7 +389,7 @@ export class SharedTripComponent implements AfterViewInit {
toggleMapFullscreen() {
this.isMapFullscreen = !this.isMapFullscreen;
document.body.classList.toggle("overflow-hidden");
document.body.classList.toggle('overflow-hidden');
setTimeout(() => {
this.map?.invalidateSize();
@ -444,14 +408,12 @@ export class SharedTripComponent implements AfterViewInit {
return;
}
this.totalPrice =
this.trip?.days
.flatMap((d) => d.items)
.reduce((price, item) => price + (item.price ?? 0), 0) ?? 0;
this.trip?.days.flatMap((d) => d.items).reduce((price, item) => price + (item.price ?? 0), 0) ?? 0;
}
resetPlaceHighlightMarker() {
if (this.tripMapHoveredElement) {
this.tripMapHoveredElement.classList.remove("list-hover");
this.tripMapHoveredElement.classList.remove('list-hover');
this.tripMapHoveredElement = undefined;
}
@ -468,8 +430,7 @@ export class SharedTripComponent implements AfterViewInit {
}
placeHighlightMarker(item: any) {
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker)
this.resetPlaceHighlightMarker();
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker) this.resetPlaceHighlightMarker();
let marker: L.Marker | undefined;
this.markerClusterGroup?.eachLayer((layer: any) => {
@ -487,10 +448,7 @@ export class SharedTripComponent implements AfterViewInit {
// TripItem without place, but latlng
this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!);
if (this.tripMapGpxLayer) {
this.map?.fitBounds(
[[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()],
{ padding: [30, 30] },
);
this.map?.fitBounds([[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()], { padding: [30, 30] });
} else this.map?.fitBounds([[item.lat, item.lng]], { padding: [60, 60] });
return;
}
@ -499,18 +457,16 @@ export class SharedTripComponent implements AfterViewInit {
const markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster
if (markerElement) {
// marker, not clustered
markerElement.classList.add("list-hover");
markerElement.classList.add('list-hover');
this.tripMapHoveredElement = markerElement;
targetLatLng = marker.getLatLng();
} else {
// marker is clustered
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
marker,
);
const parentCluster = (this.markerClusterGroup as any).getVisibleParent(marker);
if (parentCluster) {
const clusterEl = parentCluster.getElement();
if (clusterEl) {
clusterEl.classList.add("list-hover");
clusterEl.classList.add('list-hover');
this.tripMapHoveredElement = clusterEl;
}
targetLatLng = parentCluster.getLatLng();
@ -574,11 +530,7 @@ export class SharedTripComponent implements AfterViewInit {
.filter((n) => n !== undefined);
if (items.length < 2) {
this.utilsService.toast(
"info",
"Info",
"Not enough values to map an itinerary",
);
this.utilsService.toast('info', 'Info', 'Not enough values to map an itinerary');
return;
}
@ -590,20 +542,20 @@ export class SharedTripComponent implements AfterViewInit {
const layGroup = L.featureGroup();
const COLORS: string[] = [
"#e6194b",
"#3cb44b",
"#ffe119",
"#4363d8",
"#9a6324",
"#f58231",
"#911eb4",
"#46f0f0",
"#f032e6",
"#bcf60c",
"#fabebe",
"#008080",
"#e6beff",
"#808000",
'#e6194b',
'#3cb44b',
'#ffe119',
'#4363d8',
'#9a6324',
'#f58231',
'#911eb4',
'#46f0f0',
'#f032e6',
'#bcf60c',
'#fabebe',
'#008080',
'#e6beff',
'#808000',
];
let prevPoint: [number, number] | null = null;
@ -614,7 +566,7 @@ export class SharedTripComponent implements AfterViewInit {
dashArray: [10, 20],
weight: 5,
color: COLORS[idx % COLORS.length],
pulseColor: "#FFFFFF",
pulseColor: '#FFFFFF',
paused: false,
reverse: false,
hardwareAccelerated: true,
@ -665,9 +617,7 @@ export class SharedTripComponent implements AfterViewInit {
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
if (!this.trip || idx === undefined || idx == -1) return;
const data = this.trip.days[idx].items.sort((a, b) =>
a.time < b.time ? -1 : a.time > b.time ? 1 : 0,
);
const data = this.trip.days[idx].items.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
const items = data
.map((item) => {
if (item.lat && item.lng)
@ -693,11 +643,7 @@ export class SharedTripComponent implements AfterViewInit {
.filter((n) => n !== undefined);
if (items.length < 2) {
this.utilsService.toast(
"info",
"Info",
"Not enough values to map an itinerary",
);
this.utilsService.toast('info', 'Info', 'Not enough values to map an itinerary');
return;
}
@ -712,8 +658,8 @@ export class SharedTripComponent implements AfterViewInit {
delay: 400,
dashArray: [10, 20],
weight: 5,
color: "#0000FF",
pulseColor: "#FFFFFF",
color: '#0000FF',
pulseColor: '#FFFFFF',
paused: false,
reverse: false,
hardwareAccelerated: true,
@ -751,15 +697,9 @@ export class SharedTripComponent implements AfterViewInit {
}
onMapMarkerClick(place_id: number) {
const item = this.flattenedTripItems.find(
(i) => i.place && i.place.id == place_id,
);
const item = this.flattenedTripItems.find((i) => i.place && i.place.id == place_id);
if (!item) {
this.utilsService.toast(
"info",
"Place not used",
"The place is not used in the table",
);
this.utilsService.toast('info', 'Place not used', 'The place is not used in the table');
return;
}
@ -773,14 +713,14 @@ export class SharedTripComponent implements AfterViewInit {
// TODO: More services
// 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}`;
window.open(url, "_blank");
window.open(url, '_blank');
}
downloadItemGPX() {
if (!this.selectedItem?.gpx) return;
const dataBlob = new Blob([this.selectedItem.gpx]);
const downloadURL = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
const link = document.createElement('a');
link.href = downloadURL;
link.download = `TRIP_${this.trip?.name}_${this.selectedItem.text}.gpx`;
link.click();
@ -791,27 +731,23 @@ export class SharedTripComponent implements AfterViewInit {
tripDayToNavigation(day_id: number) {
const idx = this.trip?.days.findIndex((d) => d.id === day_id);
if (!this.trip || idx === undefined || idx == -1) return;
const data = this.trip.days[idx].items.sort((a, b) =>
a.time < b.time ? -1 : a.time > b.time ? 1 : 0,
);
const data = this.trip.days[idx].items.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
const items = data.filter((item) => item.lat && item.lng);
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}`;
window.open(url, "_blank");
window.open(url, '_blank');
}
tripToNavigation() {
// TODO: More services
const items = this.flattenedTripItems.filter(
(item) => item.lat && item.lng,
);
const items = this.flattenedTripItems.filter((item) => item.lat && item.lng);
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}`;
window.open(url, "_blank");
window.open(url, '_blank');
}
openPackingList() {
@ -832,24 +768,13 @@ export class SharedTripComponent implements AfterViewInit {
computeDispPackingList() {
const sorted: PackingItem[] = [...this.packingList].sort((a, b) =>
a.packed !== b.packed
? a.packed
? 1
: -1
: a.text < b.text
? -1
: a.text > b.text
? 1
: 0,
a.packed !== b.packed ? (a.packed ? 1 : -1) : a.text < b.text ? -1 : a.text > b.text ? 1 : 0,
);
this.dispPackingList = sorted.reduce<Record<string, PackingItem[]>>(
(acc, item) => {
this.dispPackingList = sorted.reduce<Record<string, PackingItem[]>>((acc, item) => {
(acc[item.category] ??= []).push(item);
return acc;
},
{},
);
}, {});
}
openChecklist() {

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,14 @@
import { Component, OnInit } from "@angular/core";
import { ApiService } from "../../services/api.service";
import { ButtonModule } from "primeng/button";
import { SkeletonModule } from "primeng/skeleton";
import { TripBase, TripInvitation } from "../../types/trip";
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
import { TripCreateModalComponent } from "../../modals/trip-create-modal/trip-create-modal.component";
import { Router } from "@angular/router";
import { forkJoin, take } from "rxjs";
import { DatePipe } from "@angular/common";
import { DialogModule } from "primeng/dialog";
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { ButtonModule } from 'primeng/button';
import { SkeletonModule } from 'primeng/skeleton';
import { TripBase, TripInvitation } from '../../types/trip';
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
import { TripCreateModalComponent } from '../../modals/trip-create-modal/trip-create-modal.component';
import { Router } from '@angular/router';
import { forkJoin, take } from 'rxjs';
import { DatePipe } from '@angular/common';
import { DialogModule } from 'primeng/dialog';
interface TripBaseWithDates extends TripBase {
from?: Date;
@ -16,11 +16,11 @@ interface TripBaseWithDates extends TripBase {
}
@Component({
selector: "app-trips",
selector: 'app-trips',
standalone: true,
imports: [SkeletonModule, ButtonModule, DialogModule, DatePipe],
templateUrl: "./trips.component.html",
styleUrls: ["./trips.component.scss"],
templateUrl: './trips.component.html',
styleUrls: ['./trips.component.scss'],
})
export class TripsComponent implements OnInit {
trips: TripBase[] = [];
@ -53,7 +53,7 @@ export class TripsComponent implements OnInit {
}
gotoMap() {
this.router.navigateByUrl("/");
this.router.navigateByUrl('/');
}
viewTrip(id: number) {
@ -61,20 +61,17 @@ export class TripsComponent implements OnInit {
}
addTrip() {
const modal: DynamicDialogRef = this.dialogService.open(
TripCreateModalComponent,
{
header: "Create Trip",
const modal: DynamicDialogRef = this.dialogService.open(TripCreateModalComponent, {
header: 'Create Trip',
modal: true,
appendTo: "body",
appendTo: 'body',
closable: true,
dismissableMask: true,
width: "50vw",
width: '50vw',
breakpoints: {
"960px": "80vw",
'960px': '80vw',
},
},
)!;
})!;
modal.onClose.pipe(take(1)).subscribe({
next: (trip: TripBaseWithDates | null) => {
@ -85,12 +82,8 @@ export class TripsComponent implements OnInit {
let dayCount = 0;
if (trip.from && trip.to) {
const obs$ = this.generateDaysLabel(trip.from!, trip.to!).map(
(label) =>
this.apiService.postTripDay(
{ id: -1, label: label, items: [] },
new_trip.id,
),
const obs$ = this.generateDaysLabel(trip.from!, trip.to!).map((label) =>
this.apiService.postTripDay({ id: -1, label: label, items: [] }, new_trip.id),
);
dayCount = obs$.length;
forkJoin(obs$).pipe(take(1)).subscribe();
@ -116,32 +109,17 @@ export class TripsComponent implements OnInit {
generateDaysLabel(from: Date, to: Date): string[] {
const labels: string[] = [];
const sameMonth =
from.getFullYear() === to.getFullYear() &&
from.getMonth() === to.getMonth();
const sameMonth = from.getFullYear() === to.getFullYear() && from.getMonth() === to.getMonth();
const months = [
"Jan.",
"Feb.",
"Mar.",
"Apr.",
"May.",
"Jun.",
"Jul.",
"Aug.",
"Sep.",
"Oct.",
"Nov.",
"Dec.",
];
const months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
const current = new Date(from);
while (current <= to) {
let label = "";
let label = '';
if (sameMonth) {
label = `${current.getDate().toString().padStart(2, "0")} ${months[current.getMonth()]}`;
label = `${current.getDate().toString().padStart(2, '0')} ${months[current.getMonth()]}`;
} 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);
current.setDate(current.getDate() + 1);

View File

@ -1 +1 @@
declare module "leaflet-ant-path";
declare module 'leaflet-ant-path';

View File

@ -1,19 +1,19 @@
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { TextareaModule } from "primeng/textarea";
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { TextareaModule } from 'primeng/textarea';
@Component({
selector: "app-batch-create-modal",
selector: 'app-batch-create-modal',
imports: [FloatLabelModule, ButtonModule, ReactiveFormsModule, TextareaModule],
standalone: true,
templateUrl: "./batch-create-modal.component.html",
styleUrl: "./batch-create-modal.component.scss",
templateUrl: './batch-create-modal.component.html',
styleUrl: './batch-create-modal.component.scss',
})
export class BatchCreateModalComponent {
batchInput = new FormControl("");
batchInput = new FormControl('');
constructor(private ref: DynamicDialogRef) {}

View File

@ -1,31 +1,19 @@
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
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";
import { ColorPickerModule } from "primeng/colorpicker";
import { Category } from "../../types/poi";
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
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';
import { ColorPickerModule } from 'primeng/colorpicker';
import { Category } from '../../types/poi';
@Component({
selector: "app-category-create-modal",
imports: [
FloatLabelModule,
InputTextModule,
ButtonModule,
ColorPickerModule,
ReactiveFormsModule,
FocusTrapModule,
],
selector: 'app-category-create-modal',
imports: [FloatLabelModule, InputTextModule, ButtonModule, ColorPickerModule, ReactiveFormsModule, FocusTrapModule],
standalone: true,
templateUrl: "./category-create-modal.component.html",
styleUrl: "./category-create-modal.component.scss",
templateUrl: './category-create-modal.component.html',
styleUrl: './category-create-modal.component.scss',
})
export class CategoryCreateModalComponent {
categoryForm: FormGroup;
@ -38,14 +26,11 @@ export class CategoryCreateModalComponent {
) {
this.categoryForm = this.fb.group({
id: -1,
name: ["", Validators.required],
name: ['', Validators.required],
color: [
"#000000",
'#000000',
{
validators: [
Validators.required,
Validators.pattern("\#[abcdefABCDEF0-9]{6}"),
],
validators: [Validators.required, Validators.pattern('\#[abcdefABCDEF0-9]{6}')],
},
],
image: null,
@ -58,8 +43,8 @@ export class CategoryCreateModalComponent {
closeDialog() {
// Normalize data for API POST
let ret = this.categoryForm.value;
if (!ret["name"]) return;
if (!this.updatedImage) delete ret["image"];
if (!ret['name']) return;
if (!this.updatedImage) delete ret['image'];
this.ref.close(ret);
}
@ -70,8 +55,8 @@ export class CategoryCreateModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
this.categoryForm.get("image")?.setValue(e.target?.result as string);
this.categoryForm.get("image")?.markAsDirty();
this.categoryForm.get('image')?.setValue(e.target?.result as string);
this.categoryForm.get('image')?.markAsDirty();
this.updatedImage = true;
};
@ -80,7 +65,7 @@ export class CategoryCreateModalComponent {
}
clearImage() {
this.categoryForm.get("image")?.setValue(null);
this.categoryForm.get('image')?.setValue(null);
this.updatedImage = false;
}
}

View File

@ -1,32 +1,27 @@
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { InputTextModule } from "primeng/inputtext";
import { SelectModule } from "primeng/select";
import { TextareaModule } from "primeng/textarea";
import { Observable } from "rxjs";
import { AsyncPipe } from "@angular/common";
import { InputGroupModule } from "primeng/inputgroup";
import { InputGroupAddonModule } from "primeng/inputgroupaddon";
import { ApiService } from "../../services/api.service";
import { UtilsService } from "../../services/utils.service";
import { FocusTrapModule } from "primeng/focustrap";
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";
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { InputTextModule } from 'primeng/inputtext';
import { SelectModule } from 'primeng/select';
import { TextareaModule } from 'primeng/textarea';
import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
import { ApiService } from '../../services/api.service';
import { UtilsService } from '../../services/utils.service';
import { FocusTrapModule } from 'primeng/focustrap';
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({
selector: "app-place-create-modal",
selector: 'app-place-create-modal',
imports: [
FloatLabelModule,
InputTextModule,
@ -43,8 +38,8 @@ import { InputNumberModule } from "primeng/inputnumber";
FocusTrapModule,
],
standalone: true,
templateUrl: "./place-create-modal.component.html",
styleUrl: "./place-create-modal.component.scss",
templateUrl: './place-create-modal.component.html',
styleUrl: './place-create-modal.component.scss',
})
export class PlaceCreateModalComponent {
placeForm: FormGroup;
@ -66,32 +61,27 @@ export class PlaceCreateModalComponent {
this.placeForm = this.fb.group({
id: -1,
name: ["", Validators.required],
place: ["", { validators: Validators.required, updateOn: "blur" }],
name: ['', Validators.required],
place: ['', { validators: Validators.required, updateOn: 'blur' }],
lat: [
"",
'',
{
validators: [
Validators.required,
Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
],
updateOn: "blur",
validators: [Validators.required, Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)')],
updateOn: 'blur',
},
],
lng: [
"",
'',
{
validators: [
Validators.required,
Validators.pattern(
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
),
Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
],
},
],
category: [null, Validators.required],
description: null,
duration: [null, Validators.pattern("\\d+")],
duration: [null, Validators.pattern('\\d+')],
price: null,
allowdog: false,
visited: false,
@ -104,12 +94,11 @@ export class PlaceCreateModalComponent {
if (patchValue) this.placeForm.patchValue(patchValue);
this.placeForm
.get("place")
.get('place')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe({
next: (value: string) => {
const isGoogleMapsURL =
/^(https?:\/\/)?(www\.)?google\.[a-z.]+\/maps/.test(value);
const isGoogleMapsURL = /^(https?:\/\/)?(www\.)?google\.[a-z.]+\/maps/.test(value);
if (isGoogleMapsURL) {
this.parseGoogleMapsUrl(value);
}
@ -117,7 +106,7 @@ export class PlaceCreateModalComponent {
});
this.placeForm
.get("lat")
.get('lat')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe({
next: (value: string) => {
@ -125,8 +114,8 @@ export class PlaceCreateModalComponent {
if (!result) return;
const [lat, lng] = result;
const latControl = this.placeForm.get("lat");
const lngControl = this.placeForm.get("lng");
const latControl = this.placeForm.get('lat');
const lngControl = this.placeForm.get('lng');
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
@ -140,15 +129,15 @@ export class PlaceCreateModalComponent {
closeDialog() {
// Normalize data for API POST
let ret = this.placeForm.value;
ret["category_id"] = ret["category"];
delete ret["category"];
if (ret["image_id"]) {
delete ret["image"];
delete ret["image_id"];
ret['category_id'] = ret['category'];
delete ret['category'];
if (ret['image_id']) {
delete ret['image'];
delete ret['image_id'];
}
if (ret["gpx"] == "1") delete ret["gpx"];
ret["lat"] = +ret["lat"];
ret["lng"] = +ret["lng"];
if (ret['gpx'] == '1') delete ret['gpx'];
ret['lat'] = +ret['lat'];
ret['lng'] = +ret['lng'];
this.ref.close(ret);
}
@ -156,13 +145,12 @@ export class PlaceCreateModalComponent {
const [place, latlng] = this.utilsService.parseGoogleMapsUrl(url);
if (!place || !latlng) return;
const [lat, lng] = latlng.split(",");
this.placeForm.get("place")?.setValue(place);
this.placeForm.get("lat")?.setValue(lat);
this.placeForm.get("lng")?.setValue(lng);
const [lat, lng] = latlng.split(',');
this.placeForm.get('place')?.setValue(place);
this.placeForm.get('lat')?.setValue(lat);
this.placeForm.get('lng')?.setValue(lng);
if (!this.placeForm.get("name")?.value)
this.placeForm.get("name")?.setValue(place);
if (!this.placeForm.get('name')?.value) this.placeForm.get('name')?.setValue(place);
}
onImageSelected(event: Event) {
@ -172,14 +160,14 @@ export class PlaceCreateModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
if (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.placeForm.get("image_id")?.setValue(null);
if (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.placeForm.get('image_id')?.setValue(null);
}
this.placeForm.get("image")?.setValue(e.target?.result as string);
this.placeForm.get("image")?.markAsDirty();
this.placeForm.get('image')?.setValue(e.target?.result as string);
this.placeForm.get('image')?.markAsDirty();
};
reader.readAsDataURL(file);
@ -187,12 +175,12 @@ export class PlaceCreateModalComponent {
}
clearImage() {
this.placeForm.get("image")?.setValue(null);
this.placeForm.get("image_id")?.setValue(null);
this.placeForm.get('image')?.setValue(null);
this.placeForm.get('image_id')?.setValue(null);
if (this.previous_image && 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_id')?.setValue(this.previous_image_id);
this.placeForm.get('image')?.setValue(this.previous_image);
}
}
@ -203,8 +191,8 @@ export class PlaceCreateModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
this.placeForm.get("gpx")?.setValue(e.target?.result as string);
this.placeForm.get("gpx")?.markAsDirty();
this.placeForm.get('gpx')?.setValue(e.target?.result as string);
this.placeForm.get('gpx')?.markAsDirty();
};
reader.readAsText(file);
@ -212,7 +200,7 @@ export class PlaceCreateModalComponent {
}
clearGPX() {
this.placeForm.get("gpx")?.setValue(null);
this.placeForm.get("gpx")?.markAsDirty();
this.placeForm.get('gpx')?.setValue(null);
this.placeForm.get('gpx')?.markAsDirty();
}
}

View File

@ -1,25 +1,20 @@
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { TextareaModule } from "primeng/textarea";
import { Trip } from "../../types/trip";
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { TextareaModule } from 'primeng/textarea';
import { Trip } from '../../types/trip';
@Component({
selector: "app-trip-archive-modal",
imports: [
FloatLabelModule,
TextareaModule,
ButtonModule,
ReactiveFormsModule,
],
selector: 'app-trip-archive-modal',
imports: [FloatLabelModule, TextareaModule, ButtonModule, ReactiveFormsModule],
standalone: true,
templateUrl: "./trip-archive-modal.component.html",
styleUrl: "./trip-archive-modal.component.scss",
templateUrl: './trip-archive-modal.component.html',
styleUrl: './trip-archive-modal.component.scss',
})
export class TripArchiveModalComponent {
review = new FormControl("");
review = new FormControl('');
constructor(
private ref: DynamicDialogRef,
@ -34,16 +29,13 @@ export class TripArchiveModalComponent {
return;
}
if (!trip.days.length) return;
let placeholder = "General feedback:\n\n";
let placeholder = 'General feedback:\n\n';
trip.days.forEach((day, index) => {
placeholder += `\nDay ${index + 1} (${day.label})\n`;
if (!day.items.length) placeholder += " No activities.\n";
else
day.items.forEach(
(item) => (placeholder += ` - ${item.time} | ${item.text}\n`),
);
if (!day.items.length) placeholder += ' No activities.\n';
else day.items.forEach((item) => (placeholder += ` - ${item.time} | ${item.text}\n`));
});
placeholder += "\nAnything else?";
placeholder += '\nAnything else?';
this.review.setValue(placeholder);
}

View File

@ -1,28 +1,17 @@
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
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";
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
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({
selector: "app-trip-create-checklist-modal",
imports: [
FloatLabelModule,
InputTextModule,
ButtonModule,
ReactiveFormsModule,
FocusTrapModule,
],
selector: 'app-trip-create-checklist-modal',
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
standalone: true,
templateUrl: "./trip-create-checklist-modal.component.html",
styleUrl: "./trip-create-checklist-modal.component.scss",
templateUrl: './trip-create-checklist-modal.component.html',
styleUrl: './trip-create-checklist-modal.component.scss',
})
export class TripCreateChecklistModalComponent {
checklistForm: FormGroup;
@ -33,7 +22,7 @@ export class TripCreateChecklistModalComponent {
) {
this.checklistForm = this.fb.group({
id: -1,
text: ["", { validators: Validators.required }],
text: ['', { validators: Validators.required }],
});
const patchValue = this.config.data?.packing;

View File

@ -1,30 +1,25 @@
import { Component, ViewChild } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { InputTextModule } from "primeng/inputtext";
import { TripDay, TripMember, TripStatus } from "../../types/trip";
import { Place } from "../../types/poi";
import { SelectModule } from "primeng/select";
import { TextareaModule } from "primeng/textarea";
import { InputMaskModule } from "primeng/inputmask";
import { UtilsService } from "../../services/utils.service";
import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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";
import { Component, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { InputTextModule } from 'primeng/inputtext';
import { TripDay, TripMember, TripStatus } from '../../types/trip';
import { Place } from '../../types/poi';
import { SelectModule } from 'primeng/select';
import { TextareaModule } from 'primeng/textarea';
import { InputMaskModule } from 'primeng/inputmask';
import { UtilsService } from '../../services/utils.service';
import { checkAndParseLatLng, formatLatLng } from '../../shared/latlng-parser';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
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({
selector: "app-trip-create-day-item-modal",
selector: 'app-trip-create-day-item-modal',
imports: [
FloatLabelModule,
InputTextModule,
@ -44,11 +39,11 @@ import { Popover, PopoverModule } from "primeng/popover";
PopoverModule,
],
standalone: true,
templateUrl: "./trip-create-day-item-modal.component.html",
styleUrl: "./trip-create-day-item-modal.component.scss",
templateUrl: './trip-create-day-item-modal.component.html',
styleUrl: './trip-create-day-item-modal.component.scss',
})
export class TripCreateDayItemModalComponent {
@ViewChild("op") op!: Popover;
@ViewChild('op') op!: Popover;
members: TripMember[] = [];
itemForm: FormGroup;
days: TripDay[] = [];
@ -68,16 +63,13 @@ export class TripCreateDayItemModalComponent {
this.itemForm = this.fb.group({
id: -1,
time: [
"",
'',
{
validators: [
Validators.required,
Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/),
],
validators: [Validators.required, Validators.pattern(/^([01]\d|2[0-3])(:[0-5]\d)?$/)],
},
],
text: ["", Validators.required],
comment: "",
text: ['', Validators.required],
comment: '',
day_id: [null, Validators.required],
place: null,
status: null,
@ -86,18 +78,16 @@ export class TripCreateDayItemModalComponent {
image_id: null,
gpx: null,
lat: [
"",
'',
{
validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"),
updateOn: "blur",
validators: Validators.pattern('-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)'),
updateOn: 'blur',
},
],
lng: [
"",
'',
{
validators: Validators.pattern(
"-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)",
),
validators: Validators.pattern('-?(180(\\.0+)?|1[0-7]\\d(\\.\\d+)?|[1-9]?\\d(\\.\\d+)?)'),
},
],
paid_by: null,
@ -115,36 +105,34 @@ export class TripCreateDayItemModalComponent {
place: data.item.place?.id ?? null,
});
if (data.selectedDay)
this.itemForm.get("day_id")?.setValue([data.selectedDay]);
if (data.selectedDay) this.itemForm.get('day_id')?.setValue([data.selectedDay]);
}
this.itemForm
.get("place")
.get('place')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe({
next: (value?: number) => {
if (!value) {
this.itemForm.get("lat")?.setValue("");
this.itemForm.get("lng")?.setValue("");
this.itemForm.get('lat')?.setValue('');
this.itemForm.get('lng')?.setValue('');
return;
}
const p: Place = this.places.find((p) => p.id === value) as Place;
if (p) {
this.itemForm.get("lat")?.setValue(p.lat);
this.itemForm.get("lng")?.setValue(p.lng);
this.itemForm.get("price")?.setValue(p.price || 0);
if (!this.itemForm.get("text")?.value)
this.itemForm.get("text")?.setValue(p.name);
if (p.description && !this.itemForm.get("comment")?.value)
this.itemForm.get("comment")?.setValue(p.description);
this.itemForm.get('lat')?.setValue(p.lat);
this.itemForm.get('lng')?.setValue(p.lng);
this.itemForm.get('price')?.setValue(p.price || 0);
if (!this.itemForm.get('text')?.value) this.itemForm.get('text')?.setValue(p.name);
if (p.description && !this.itemForm.get('comment')?.value)
this.itemForm.get('comment')?.setValue(p.description);
}
},
});
this.itemForm
.get("lat")
.get('lat')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe({
next: (value: string) => {
@ -152,8 +140,8 @@ export class TripCreateDayItemModalComponent {
if (!result) return;
const [lat, lng] = result;
const latControl = this.itemForm.get("lat");
const lngControl = this.itemForm.get("lng");
const latControl = this.itemForm.get('lat');
const lngControl = this.itemForm.get('lng');
latControl?.setValue(formatLatLng(lat).trim(), { emitEvent: false });
lngControl?.setValue(formatLatLng(lng).trim(), { emitEvent: false });
@ -167,16 +155,16 @@ export class TripCreateDayItemModalComponent {
closeDialog() {
// Normalize data for API POST
let ret = this.itemForm.value;
if (!ret["lat"]) {
ret["lat"] = null;
ret["lng"] = null;
if (!ret['lat']) {
ret['lat'] = null;
ret['lng'] = null;
}
if (ret["image_id"]) {
delete ret["image"];
delete ret["image_id"];
if (ret['image_id']) {
delete ret['image'];
delete ret['image_id'];
}
if (ret["gpx"] == "1") delete ret["gpx"];
if (!ret["place"]) delete ret["place"];
if (ret['gpx'] == '1') delete ret['gpx'];
if (!ret['place']) delete ret['place'];
this.ref.close(ret);
}
@ -185,7 +173,7 @@ export class TripCreateDayItemModalComponent {
}
get paidByControl(): any {
return this.itemForm.get("paid_by");
return this.itemForm.get('paid_by');
}
selectPriceMember(member: any) {
@ -206,14 +194,14 @@ export class TripCreateDayItemModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
if (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.itemForm.get("image_id")?.setValue(null);
if (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.itemForm.get('image_id')?.setValue(null);
}
this.itemForm.get("image")?.setValue(e.target?.result as string);
this.itemForm.get("image")?.markAsDirty();
this.itemForm.get('image')?.setValue(e.target?.result as string);
this.itemForm.get('image')?.markAsDirty();
};
reader.readAsDataURL(file);
@ -221,13 +209,13 @@ export class TripCreateDayItemModalComponent {
}
clearImage() {
this.itemForm.get("image")?.setValue(null);
this.itemForm.get("image_id")?.setValue(null);
this.itemForm.get('image')?.setValue(null);
this.itemForm.get('image_id')?.setValue(null);
this.itemForm.markAsDirty();
if (this.previous_image && 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_id')?.setValue(this.previous_image_id);
this.itemForm.get('image')?.setValue(this.previous_image);
}
}
@ -238,8 +226,8 @@ export class TripCreateDayItemModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
this.itemForm.get("gpx")?.setValue(e.target?.result as string);
this.itemForm.get("gpx")?.markAsDirty();
this.itemForm.get('gpx')?.setValue(e.target?.result as string);
this.itemForm.get('gpx')?.markAsDirty();
};
reader.readAsText(file);
@ -247,7 +235,7 @@ export class TripCreateDayItemModalComponent {
}
clearGPX() {
this.itemForm.get("gpx")?.setValue(null);
this.itemForm.get("gpx")?.markAsDirty();
this.itemForm.get('gpx')?.setValue(null);
this.itemForm.get('gpx')?.markAsDirty();
}
}

View File

@ -1,27 +1,17 @@
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
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";
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
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({
selector: "app-trip-create-day-modal",
imports: [
FloatLabelModule,
InputTextModule,
ButtonModule,
ReactiveFormsModule,
],
selector: 'app-trip-create-day-modal',
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule],
standalone: true,
templateUrl: "./trip-create-day-modal.component.html",
styleUrl: "./trip-create-day-modal.component.scss",
templateUrl: './trip-create-day-modal.component.html',
styleUrl: './trip-create-day-modal.component.scss',
})
export class TripCreateDayModalComponent {
dayForm: FormGroup;
@ -34,7 +24,7 @@ export class TripCreateDayModalComponent {
) {
this.dayForm = this.fb.group({
id: -1,
label: ["", Validators.required],
label: ['', Validators.required],
});
if (this.config.data) {
@ -46,7 +36,7 @@ export class TripCreateDayModalComponent {
closeDialog() {
// Normalize data for API POST
let ret = this.dayForm.value;
if (!ret["label"]) return;
if (!ret['label']) return;
this.ref.close(ret);
}
}

View File

@ -1,31 +1,31 @@
import { Component } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { TextareaModule } from "primeng/textarea";
import { TripDay, TripItem } from "../../types/trip";
import { SelectModule } from "primeng/select";
import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { TextareaModule } from 'primeng/textarea';
import { TripDay, TripItem } from '../../types/trip';
import { SelectModule } from 'primeng/select';
@Component({
selector: "app-trip-create-items-modal",
selector: 'app-trip-create-items-modal',
imports: [FloatLabelModule, ButtonModule, SelectModule, ReactiveFormsModule, TextareaModule],
standalone: true,
templateUrl: "./trip-create-items-modal.component.html",
styleUrl: "./trip-create-items-modal.component.scss",
templateUrl: './trip-create-items-modal.component.html',
styleUrl: './trip-create-items-modal.component.scss',
})
export class TripCreateItemsModalComponent {
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[] = [];
constructor(
private ref: DynamicDialogRef,
private fb: FormBuilder,
private config: DynamicDialogConfig
private config: DynamicDialogConfig,
) {
this.itemBatchForm = this.fb.group({
batch: ["", Validators.required],
batch: ['', Validators.required],
day_id: [null, Validators.required],
});
@ -37,15 +37,15 @@ export class TripCreateItemsModalComponent {
closeDialog() {
const ret = this.itemBatchForm.value;
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>[] = [];
lines.forEach((l) => {
const match = l.match(/^(\d{1,2})(?:h|:)?(\d{0,2})?\s+(.+)$/);
if (match) {
const [_, hoursStr, minutesStr = "", text] = match;
const hours = hoursStr.padStart(2, "0");
const minutes = minutesStr.padStart(2, "0") || "00";
const [_, hoursStr, minutesStr = '', text] = match;
const hours = hoursStr.padStart(2, '0');
const minutes = minutesStr.padStart(2, '0') || '00';
const time = `${hours}:${minutes}`;
tripItems.push({ time: time, text: text, day_id: day_id });
}

View File

@ -1,30 +1,18 @@
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
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";
import { DatePickerModule } from "primeng/datepicker";
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
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';
import { DatePickerModule } from 'primeng/datepicker';
@Component({
selector: "app-trip-create-modal",
imports: [
FloatLabelModule,
InputTextModule,
DatePickerModule,
ButtonModule,
ReactiveFormsModule,
FocusTrapModule,
],
selector: 'app-trip-create-modal',
imports: [FloatLabelModule, InputTextModule, DatePickerModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
standalone: true,
templateUrl: "./trip-create-modal.component.html",
styleUrl: "./trip-create-modal.component.scss",
templateUrl: './trip-create-modal.component.html',
styleUrl: './trip-create-modal.component.scss',
})
export class TripCreateModalComponent {
tripForm: FormGroup;
@ -38,8 +26,8 @@ export class TripCreateModalComponent {
) {
this.tripForm = this.fb.group({
id: -1,
name: ["", Validators.required],
image: "",
name: ['', Validators.required],
image: '',
currency: null,
image_id: null,
from: null,
@ -48,7 +36,7 @@ export class TripCreateModalComponent {
const patchValue = this.config.data?.trip;
if (patchValue) {
if (!patchValue.image_id) delete patchValue["image"];
if (!patchValue.image_id) delete patchValue['image'];
this.tripForm.patchValue(patchValue);
}
}
@ -56,10 +44,10 @@ export class TripCreateModalComponent {
closeDialog() {
// Normalize data for API POST
let ret = this.tripForm.value;
if (!ret["name"]) return;
if (ret["image_id"]) {
delete ret["image"];
delete ret["image_id"];
if (!ret['name']) return;
if (ret['image_id']) {
delete ret['image'];
delete ret['image_id'];
}
this.ref.close(ret);
}
@ -71,14 +59,14 @@ export class TripCreateModalComponent {
const reader = new FileReader();
reader.onload = (e) => {
if (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.tripForm.get("image_id")?.setValue(null);
if (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.tripForm.get('image_id')?.setValue(null);
}
this.tripForm.get("image")?.setValue(e.target?.result as string);
this.tripForm.get("image")?.markAsDirty();
this.tripForm.get('image')?.setValue(e.target?.result as string);
this.tripForm.get('image')?.markAsDirty();
};
reader.readAsDataURL(file);
@ -86,11 +74,11 @@ export class TripCreateModalComponent {
}
clearImage() {
this.tripForm.get("image")?.setValue(null);
this.tripForm.get('image')?.setValue(null);
if (this.previous_image && 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_id')?.setValue(this.previous_image_id);
this.tripForm.get('image')?.setValue(this.previous_image);
}
}
}

View File

@ -1,20 +1,15 @@
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
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";
import { SelectModule } from "primeng/select";
import { InputNumberModule } from "primeng/inputnumber";
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
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';
import { SelectModule } from 'primeng/select';
import { InputNumberModule } from 'primeng/inputnumber';
@Component({
selector: "app-trip-create-packing-modal",
selector: 'app-trip-create-packing-modal',
imports: [
FloatLabelModule,
InputTextModule,
@ -25,17 +20,17 @@ import { InputNumberModule } from "primeng/inputnumber";
InputNumberModule,
],
standalone: true,
templateUrl: "./trip-create-packing-modal.component.html",
styleUrl: "./trip-create-packing-modal.component.scss",
templateUrl: './trip-create-packing-modal.component.html',
styleUrl: './trip-create-packing-modal.component.scss',
})
export class TripCreatePackingModalComponent {
packingForm: FormGroup;
readonly packingCategories = [
{ value: "clothes", dispValue: "Clothes" },
{ value: "toiletries", dispValue: "Toiletries" },
{ value: "tech", dispValue: "Tech" },
{ value: "documents", dispValue: "Documents" },
{ value: "other", dispValue: "Other" },
{ value: 'clothes', dispValue: 'Clothes' },
{ value: 'toiletries', dispValue: 'Toiletries' },
{ value: 'tech', dispValue: 'Tech' },
{ value: 'documents', dispValue: 'Documents' },
{ value: 'other', dispValue: 'Other' },
];
constructor(
@ -46,8 +41,8 @@ export class TripCreatePackingModalComponent {
this.packingForm = this.fb.group({
id: -1,
qt: null,
text: ["", { validators: Validators.required }],
category: ["", { validators: Validators.required }],
text: ['', { validators: Validators.required }],
category: ['', { validators: Validators.required }],
});
const patchValue = this.config.data?.packing;

View File

@ -1,26 +1,20 @@
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { InputTextModule } from "primeng/inputtext";
import { FocusTrapModule } from "primeng/focustrap";
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { InputTextModule } from 'primeng/inputtext';
import { FocusTrapModule } from 'primeng/focustrap';
@Component({
selector: "app-trip-invite-member-modal",
imports: [
FloatLabelModule,
InputTextModule,
ButtonModule,
ReactiveFormsModule,
FocusTrapModule,
],
selector: 'app-trip-invite-member-modal',
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, FocusTrapModule],
standalone: true,
templateUrl: "./trip-invite-member-modal.component.html",
styleUrl: "./trip-invite-member-modal.component.scss",
templateUrl: './trip-invite-member-modal.component.html',
styleUrl: './trip-invite-member-modal.component.scss',
})
export class TripInviteMemberModalComponent {
memberForm = new FormControl("");
memberForm = new FormControl('');
constructor(private ref: DynamicDialogRef) {}
closeDialog() {

View File

@ -1,24 +1,19 @@
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { TextareaModule } from "primeng/textarea";
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { TextareaModule } from 'primeng/textarea';
@Component({
selector: "app-trip-notes-modal",
imports: [
FloatLabelModule,
TextareaModule,
ButtonModule,
ReactiveFormsModule,
],
selector: 'app-trip-notes-modal',
imports: [FloatLabelModule, TextareaModule, ButtonModule, ReactiveFormsModule],
standalone: true,
templateUrl: "./trip-notes-modal.component.html",
styleUrl: "./trip-notes-modal.component.scss",
templateUrl: './trip-notes-modal.component.html',
styleUrl: './trip-notes-modal.component.scss',
})
export class TripNotesModalComponent {
notes = new FormControl("");
notes = new FormControl('');
isEditing: boolean = false;
constructor(

View File

@ -1,28 +1,22 @@
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ButtonModule } from "primeng/button";
import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
import { FloatLabelModule } from "primeng/floatlabel";
import { InputTextModule } from "primeng/inputtext";
import { Place } from "../../types/poi";
import { ApiService } from "../../services/api.service";
import { SkeletonModule } from "primeng/skeleton";
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FloatLabelModule } from 'primeng/floatlabel';
import { InputTextModule } from 'primeng/inputtext';
import { Place } from '../../types/poi';
import { ApiService } from '../../services/api.service';
import { SkeletonModule } from 'primeng/skeleton';
@Component({
selector: "app-trip-place-select-modal",
imports: [
FloatLabelModule,
InputTextModule,
ButtonModule,
ReactiveFormsModule,
SkeletonModule,
],
selector: 'app-trip-place-select-modal',
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule],
standalone: true,
templateUrl: "./trip-place-select-modal.component.html",
styleUrl: "./trip-place-select-modal.component.scss",
templateUrl: './trip-place-select-modal.component.html',
styleUrl: './trip-place-select-modal.component.scss',
})
export class TripPlaceSelectModalComponent {
searchInput = new FormControl("");
searchInput = new FormControl('');
selectedPlaces: Place[] = [];
showSelectedPlaces: boolean = false;
@ -38,9 +32,7 @@ export class TripPlaceSelectModalComponent {
) {
this.apiService.getPlaces().subscribe({
next: (places) => {
this.places = places.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.places = places.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
this.displayedPlaces = places;
},
});
@ -60,9 +52,7 @@ export class TripPlaceSelectModalComponent {
const v = value.toLowerCase();
this.displayedPlaces = this.places.filter(
(p) =>
p.name.toLowerCase().includes(v) ||
p.description?.toLowerCase().includes(v),
(p) => p.name.toLowerCase().includes(v) || p.description?.toLowerCase().includes(v),
);
},
});
@ -80,9 +70,7 @@ export class TripPlaceSelectModalComponent {
this.selectedPlacesID.push(p.id);
this.selectedPlaces.push(p);
this.selectedPlaces.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.selectedPlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
closeDialog() {

View File

@ -1,9 +1,9 @@
import { inject, Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Category, Place } from "../types/poi";
import { BehaviorSubject, map, Observable, shareReplay, tap } from "rxjs";
import { Info } from "../types/info";
import { ImportResponse, Settings } from "../types/settings";
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Category, Place } from '../types/poi';
import { BehaviorSubject, map, Observable, shareReplay, tap } from 'rxjs';
import { Info } from '../types/info';
import { ImportResponse, Settings } from '../types/settings';
import {
ChecklistItem,
PackingItem,
@ -14,37 +14,31 @@ import {
TripInvitation,
TripItem,
TripMember,
} from "../types/trip";
} from '../types/trip';
const NO_AUTH_HEADER = {
no_auth: "1",
no_auth: '1',
};
@Injectable({
providedIn: "root",
providedIn: 'root',
})
export class ApiService {
public readonly apiBaseUrl: string = "/api";
public readonly apiBaseUrl: string = '/api';
private categoriesSubject = new BehaviorSubject<Category[] | null>(null);
public categories$: Observable<Category[] | null> =
this.categoriesSubject.asObservable();
public categories$: Observable<Category[] | null> = this.categoriesSubject.asObservable();
private settingsSubject = new BehaviorSubject<Settings | null>(null);
public settings$: Observable<Settings | null> =
this.settingsSubject.asObservable();
public settings$: Observable<Settings | null> = this.settingsSubject.asObservable();
private httpClient = inject(HttpClient);
getInfo(): Observable<Info> {
return this.httpClient.get<Info>(this.apiBaseUrl + "/info");
return this.httpClient.get<Info>(this.apiBaseUrl + '/info');
}
_categoriesSubjectNext(categories: Category[]) {
this.categoriesSubject.next(
[...categories].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
),
);
this.categoriesSubject.next([...categories].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)));
}
getCategories(): Observable<Category[]> {
@ -58,21 +52,12 @@ export class ApiService {
postCategory(c: Category): Observable<Category> {
return this.httpClient
.post<Category>(this.apiBaseUrl + "/categories", c)
.pipe(
tap((category) =>
this._categoriesSubjectNext([
...(this.categoriesSubject.value || []),
category,
]),
),
);
.post<Category>(this.apiBaseUrl + '/categories', c)
.pipe(tap((category) => this._categoriesSubjectNext([...(this.categoriesSubject.value || []), category])));
}
putCategory(c_id: number, c: Partial<Category>): Observable<Category> {
return this.httpClient
.put<Category>(this.apiBaseUrl + `/categories/${c_id}`, c)
.pipe(
return this.httpClient.put<Category>(this.apiBaseUrl + `/categories/${c_id}`, c).pipe(
tap((category) => {
const categories = this.categoriesSubject.value || [];
const idx = categories?.findIndex((c) => c.id == c_id) || -1;
@ -86,9 +71,7 @@ export class ApiService {
}
deleteCategory(category_id: number): Observable<{}> {
return this.httpClient
.delete<{}>(this.apiBaseUrl + `/categories/${category_id}`)
.pipe(
return this.httpClient.delete<{}>(this.apiBaseUrl + `/categories/${category_id}`).pipe(
tap(() => {
const categories = this.categoriesSubject.value || [];
const idx = categories?.findIndex((c) => c.id == category_id) || -1;
@ -109,23 +92,15 @@ export class ApiService {
}
postPlaces(places: Partial<Place[]>): Observable<Place[]> {
return this.httpClient.post<Place[]>(
`${this.apiBaseUrl}/places/batch`,
places,
);
return this.httpClient.post<Place[]>(`${this.apiBaseUrl}/places/batch`, places);
}
putPlace(place_id: number, place: Partial<Place>): Observable<Place> {
return this.httpClient.put<Place>(
`${this.apiBaseUrl}/places/${place_id}`,
place,
);
return this.httpClient.put<Place>(`${this.apiBaseUrl}/places/${place_id}`, place);
}
deletePlace(place_id: number): Observable<null> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/places/${place_id}`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/places/${place_id}`);
}
getPlaceGPX(place_id: number): Observable<Place> {
@ -141,9 +116,7 @@ export class ApiService {
}
getTripBalance(id: number): Observable<{ [user: string]: number }> {
return this.httpClient.get<{ [user: string]: number }>(
`${this.apiBaseUrl}/trips/${id}/balance`,
);
return this.httpClient.get<{ [user: string]: number }>(`${this.apiBaseUrl}/trips/${id}/balance`);
}
postTrip(trip: TripBase): Observable<TripBase> {
@ -155,76 +128,39 @@ export class ApiService {
}
putTrip(trip: Partial<Trip>, trip_id: number): Observable<Trip> {
return this.httpClient.put<Trip>(
`${this.apiBaseUrl}/trips/${trip_id}`,
trip,
);
return this.httpClient.put<Trip>(`${this.apiBaseUrl}/trips/${trip_id}`, trip);
}
postTripDay(tripDay: TripDay, trip_id: number): Observable<TripDay> {
return this.httpClient.post<TripDay>(
`${this.apiBaseUrl}/trips/${trip_id}/days`,
tripDay,
);
return this.httpClient.post<TripDay>(`${this.apiBaseUrl}/trips/${trip_id}/days`, tripDay);
}
putTripDay(tripDay: Partial<TripDay>, trip_id: number): Observable<TripDay> {
return this.httpClient.put<TripDay>(
`${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`,
tripDay,
);
return this.httpClient.put<TripDay>(`${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`, tripDay);
}
deleteTripDay(trip_id: number, day_id: number): Observable<null> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}`);
}
postTripDayItem(
item: TripItem,
trip_id: number,
day_id: number,
): Observable<TripItem> {
return this.httpClient.post<TripItem>(
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`,
item,
);
postTripDayItem(item: TripItem, trip_id: number, day_id: number): Observable<TripItem> {
return this.httpClient.post<TripItem>(`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`, item);
}
putTripDayItem(
item: Partial<TripItem>,
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,
);
putTripDayItem(item: Partial<TripItem>, 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(
trip_id: number,
day_id: number,
item_id: number,
): Observable<null> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`,
);
deleteTripDayItem(trip_id: number, 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> {
return this.httpClient.get<Trip>(
`${this.apiBaseUrl}/trips/shared/${token}`,
{ headers: NO_AUTH_HEADER },
);
return this.httpClient.get<Trip>(`${this.apiBaseUrl}/trips/shared/${token}`, { headers: NO_AUTH_HEADER });
}
getSharedTripURL(trip_id: number): Observable<string> {
return this.httpClient
.get<SharedTripURL>(`${this.apiBaseUrl}/trips/${trip_id}/share`)
.pipe(
return this.httpClient.get<SharedTripURL>(`${this.apiBaseUrl}/trips/${trip_id}/share`).pipe(
map((t) => window.location.origin + t.url),
shareReplay(),
);
@ -237,138 +173,79 @@ export class ApiService {
}
deleteSharedTrip(trip_id: number): Observable<null> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/share`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/share`);
}
getPackingList(trip_id: number): Observable<PackingItem[]> {
return this.httpClient.get<PackingItem[]>(
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
);
return this.httpClient.get<PackingItem[]>(`${this.apiBaseUrl}/trips/${trip_id}/packing`);
}
getSharedTripPackingList(token: string): Observable<PackingItem[]> {
return this.httpClient.get<PackingItem[]>(
`${this.apiBaseUrl}/trips/shared/${token}/packing`,
);
return this.httpClient.get<PackingItem[]>(`${this.apiBaseUrl}/trips/shared/${token}/packing`);
}
postPackingItem(
trip_id: number,
p_item: PackingItem,
): Observable<PackingItem> {
return this.httpClient.post<PackingItem>(
`${this.apiBaseUrl}/trips/${trip_id}/packing`,
p_item,
);
postPackingItem(trip_id: number, p_item: PackingItem): Observable<PackingItem> {
return this.httpClient.post<PackingItem>(`${this.apiBaseUrl}/trips/${trip_id}/packing`, p_item);
}
putPackingItem(
trip_id: number,
p_id: number,
p_item: Partial<PackingItem>,
): Observable<PackingItem> {
return this.httpClient.put<PackingItem>(
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
p_item,
);
putPackingItem(trip_id: number, 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> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/packing/${p_id}`);
}
getChecklist(trip_id: number): Observable<ChecklistItem[]> {
return this.httpClient.get<ChecklistItem[]>(
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
);
return this.httpClient.get<ChecklistItem[]>(`${this.apiBaseUrl}/trips/${trip_id}/checklist`);
}
getSharedTripChecklist(token: string): Observable<ChecklistItem[]> {
return this.httpClient.get<ChecklistItem[]>(
`${this.apiBaseUrl}/trips/shared/${token}/checklist`,
);
return this.httpClient.get<ChecklistItem[]>(`${this.apiBaseUrl}/trips/shared/${token}/checklist`);
}
postChecklistItem(
trip_id: number,
item: ChecklistItem,
): Observable<ChecklistItem> {
return this.httpClient.post<ChecklistItem>(
`${this.apiBaseUrl}/trips/${trip_id}/checklist`,
item,
);
postChecklistItem(trip_id: number, item: ChecklistItem): Observable<ChecklistItem> {
return this.httpClient.post<ChecklistItem>(`${this.apiBaseUrl}/trips/${trip_id}/checklist`, item);
}
putChecklistItem(
trip_id: number,
id: number,
item: Partial<ChecklistItem>,
): Observable<ChecklistItem> {
return this.httpClient.put<ChecklistItem>(
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`,
item,
);
putChecklistItem(trip_id: number, 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> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/checklist/${id}`);
}
getHasTripsInvitations(): Observable<boolean> {
return this.httpClient.get<boolean>(
`${this.apiBaseUrl}/trips/invitations/pending`,
);
return this.httpClient.get<boolean>(`${this.apiBaseUrl}/trips/invitations/pending`);
}
getTripsInvitations(): Observable<TripInvitation[]> {
return this.httpClient.get<TripInvitation[]>(
`${this.apiBaseUrl}/trips/invitations`,
);
return this.httpClient.get<TripInvitation[]>(`${this.apiBaseUrl}/trips/invitations`);
}
getTripMembers(trip_id: number): Observable<TripMember[]> {
return this.httpClient.get<TripMember[]>(
`${this.apiBaseUrl}/trips/${trip_id}/members`,
);
return this.httpClient.get<TripMember[]>(`${this.apiBaseUrl}/trips/${trip_id}/members`);
}
deleteTripMember(trip_id: number, username: string): Observable<null> {
return this.httpClient.delete<null>(
`${this.apiBaseUrl}/trips/${trip_id}/members/${username}`,
);
return this.httpClient.delete<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/${username}`);
}
inviteTripMember(trip_id: number, user: string): Observable<TripMember> {
return this.httpClient.post<TripMember>(
`${this.apiBaseUrl}/trips/${trip_id}/members`,
{ user },
);
return this.httpClient.post<TripMember>(`${this.apiBaseUrl}/trips/${trip_id}/members`, { user });
}
acceptTripMemberInvite(trip_id: number): Observable<null> {
return this.httpClient.post<null>(
`${this.apiBaseUrl}/trips/${trip_id}/members/accept`,
{},
);
return this.httpClient.post<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/accept`, {});
}
declineTripMemberInvite(trip_id: number): Observable<null> {
return this.httpClient.post<null>(
`${this.apiBaseUrl}/trips/${trip_id}/members/decline`,
{},
);
return this.httpClient.post<null>(`${this.apiBaseUrl}/trips/${trip_id}/members/decline`, {});
}
checkVersion(): Observable<string> {
return this.httpClient.get<string>(
`${this.apiBaseUrl}/settings/checkversion`,
);
return this.httpClient.get<string>(`${this.apiBaseUrl}/settings/checkversion`);
}
getSettings(): Observable<Settings> {
@ -392,7 +269,7 @@ export class ApiService {
}
settingsUserImport(formdata: FormData): Observable<ImportResponse> {
const headers = { enctype: "multipart/form-data" };
const headers = { enctype: 'multipart/form-data' };
return this.httpClient
.post<ImportResponse>(`${this.apiBaseUrl}/settings/import`, formdata, {
headers: headers,

View File

@ -1,8 +1,8 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { UtilsService } from "./utils.service";
import { AuthService } from "./auth.service";
import { of, switchMap, take } from "rxjs";
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { UtilsService } from './utils.service';
import { AuthService } from './auth.service';
import { of, switchMap, take } from 'rxjs';
export const AuthGuard: CanActivateFn = (_, state) => {
const router: Router = inject(Router);
@ -14,14 +14,9 @@ export const AuthGuard: CanActivateFn = (_, state) => {
take(1),
switchMap((authenticated) => {
if (!authenticated) {
const redirectURL =
state.url === "/auth" ? "" : `redirectURL=${state.url}`;
const redirectURL = state.url === '/auth' ? '' : `redirectURL=${state.url}`;
const urlTree = router.parseUrl(`auth?${redirectURL}`);
utilsService.toast(
"warn",
"Authentication required",
"You must be authenticated",
);
utilsService.toast('warn', 'Authentication required', 'You must be authenticated');
return of(urlTree);
}

View File

@ -1,10 +1,10 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { Observable, of, ReplaySubject } from "rxjs";
import { tap } from "rxjs/operators";
import { ApiService } from "./api.service";
import { UtilsService } from "./utils.service";
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of, ReplaySubject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ApiService } from './api.service';
import { UtilsService } from './utils.service';
export interface Token {
refresh_token: string;
@ -16,11 +16,11 @@ export interface AuthParams {
oidc?: string;
}
const JWT_TOKEN = "TRIP_AT";
const REFRESH_TOKEN = "TRIP_RT";
const JWT_USER = "TRIP_USER";
const JWT_TOKEN = 'TRIP_AT';
const REFRESH_TOKEN = 'TRIP_RT';
const JWT_USER = 'TRIP_USER';
@Injectable({ providedIn: "root" })
@Injectable({ providedIn: 'root' })
export class AuthService {
public readonly apiBaseUrl: string;
private refreshInProgressLock$: ReplaySubject<Token> | null = null;
@ -39,7 +39,7 @@ export class AuthService {
}
get loggedUser(): string {
return localStorage.getItem(JWT_USER) ?? "";
return localStorage.getItem(JWT_USER) ?? '';
}
set accessToken(token: string) {
@ -47,7 +47,7 @@ export class AuthService {
}
get accessToken(): string {
return localStorage.getItem(JWT_TOKEN) ?? "";
return localStorage.getItem(JWT_TOKEN) ?? '';
}
set refreshToken(token: string) {
@ -55,11 +55,11 @@ export class AuthService {
}
get refreshToken(): string {
return localStorage.getItem(REFRESH_TOKEN) ?? "";
return localStorage.getItem(REFRESH_TOKEN) ?? '';
}
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 {
@ -81,7 +81,7 @@ export class AuthService {
this.refreshInProgressLock$ = new ReplaySubject(1);
this.httpClient
.post<Token>(this.apiBaseUrl + "/auth/refresh", {
.post<Token>(this.apiBaseUrl + '/auth/refresh', {
refresh_token: this.refreshToken,
})
.pipe(
@ -103,9 +103,7 @@ export class AuthService {
}
login(authForm: { username: string; password: string }): Observable<Token> {
return this.httpClient
.post<Token>(this.apiBaseUrl + "/auth/login", authForm)
.pipe(
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/login', authForm).pipe(
tap((tokens: Token) => {
this.loggedUser = authForm.username;
this.storeTokens(tokens);
@ -113,13 +111,8 @@ export class AuthService {
);
}
register(authForm: {
username: string;
password: string;
}): Observable<Token> {
return this.httpClient
.post<Token>(this.apiBaseUrl + "/auth/register", authForm)
.pipe(
register(authForm: { username: string; password: string }): Observable<Token> {
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/register', authForm).pipe(
tap((tokens: Token) => {
this.loggedUser = authForm.username;
this.storeTokens(tokens);
@ -128,9 +121,7 @@ export class AuthService {
}
oidcLogin(code: string, state: string): Observable<Token> {
return this.httpClient
.post<Token>(this.apiBaseUrl + "/auth/oidc/login", { code, state })
.pipe(
return this.httpClient.post<Token>(this.apiBaseUrl + '/auth/oidc/login', { code, state }).pipe(
tap((data: any) => {
if (data.access_token && data.refresh_token) {
this.loggedUser = this._getTokenUsername(data.access_token);
@ -140,23 +131,19 @@ export class AuthService {
);
}
logout(custom_msg: string = "", is_error = false): void {
this.loggedUser = "";
logout(custom_msg: string = '', is_error = false): void {
this.loggedUser = '';
this.removeTokens();
if (custom_msg) {
if (is_error) {
this.utilsService.toast(
"error",
"You must be authenticated",
custom_msg,
);
this.utilsService.toast('error', 'You must be authenticated', custom_msg);
} 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 {
@ -167,7 +154,7 @@ export class AuthService {
isTokenExpired(token: string, offsetSeconds?: number): boolean {
// Return if there is no token
if (!token || token === "") {
if (!token || token === '') {
return true;
}
@ -186,25 +173,19 @@ export class AuthService {
private _b64DecodeUnicode(str: any): string {
return decodeURIComponent(
Array.prototype.map
.call(
this._b64decode(str),
(c: any) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2),
)
.join(""),
.call(this._b64decode(str), (c: any) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join(''),
);
}
private _b64decode(str: string): string {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
let output = "";
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let output = '';
str = String(str).replace(/=+$/, "");
str = String(str).replace(/=+$/, '');
if (str.length % 4 === 1) {
throw new Error(
"'atob' failed: The string to be decoded is not correctly encoded.",
);
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
}
/* eslint-disable */
@ -223,21 +204,21 @@ export class AuthService {
}
private _urlBase64Decode(str: string): string {
let output = str.replace(/-/g, "+").replace(/_/g, "/");
let output = str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) {
case 0: {
break;
}
case 2: {
output += "==";
output += '==';
break;
}
case 3: {
output += "=";
output += '=';
break;
}
default: {
throw Error("Illegal base64url string!");
throw Error('Illegal base64url string!');
}
}
return this._b64DecodeUnicode(output);
@ -247,11 +228,11 @@ export class AuthService {
const decodedToken = this._decodeToken(token);
if (decodedToken === null) {
return "";
return '';
}
if (!decodedToken.hasOwnProperty("sub")) {
return "";
if (!decodedToken.hasOwnProperty('sub')) {
return '';
}
return decodedToken.sub;
@ -262,7 +243,7 @@ export class AuthService {
return null;
}
const parts = token.split(".");
const parts = token.split('.');
if (parts.length !== 3) {
return null;
@ -284,7 +265,7 @@ export class AuthService {
return null;
}
if (!decodedToken.hasOwnProperty("exp")) {
if (!decodedToken.hasOwnProperty('exp')) {
return null;
}

View File

@ -1,63 +1,52 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandlerFn,
HttpRequest,
} 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";
import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpRequest } 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 }> = {
400: {
title: "Bad Request",
detail: "Unknown error, check console for details",
title: 'Bad Request',
detail: 'Unknown error, check console for details',
},
403: { title: "Forbidden", detail: "You are not allowed to do this" },
409: { title: "Conflict", detail: "Conflict on resource" },
413: { title: "Request Entity Too Large", detail: "The resource is too big" },
403: { title: 'Forbidden', detail: 'You are not allowed to do this' },
409: { title: 'Conflict', detail: 'Conflict on resource' },
413: { title: 'Request Entity Too Large', detail: 'The resource is too big' },
422: {
title: "Unprocessable Entity",
detail: "The resource you sent was unprocessable",
title: 'Unprocessable Entity',
detail: 'The resource you sent was unprocessable',
},
502: {
title: "Bad Gateway",
detail: "Check your connectivity and ensure the server is up",
title: 'Bad Gateway',
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 = (
req: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> => {
export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
const authService = inject(AuthService);
const utilsService = inject(UtilsService);
function showAndThrowError(title: string, details: string) {
utilsService.toast("error", title, details);
utilsService.toast('error', title, details);
return throwError(() => details);
}
if (req.headers.has("no_auth")) {
if (req.headers.has('no_auth')) {
// Shared Trip must be anonymous
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({
setHeaders: {
"Content-Type": "application/json",
"Accept-Language": "en-US;q=0.9,en-US,en;q=0.8",
'Content-Type': 'application/json',
'Accept-Language': 'en-US;q=0.9,en-US,en;q=0.8',
},
});
}
if (
authService.accessToken &&
!authService.isTokenExpired(authService.accessToken)
) {
if (authService.accessToken && !authService.isTokenExpired(authService.accessToken)) {
if (req.url.startsWith(authService.apiBaseUrl)) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
@ -70,17 +59,14 @@ export const Interceptor = (
const errDetails = ERROR_CONFIG[err.status];
if (errDetails) {
console.error(err);
return showAndThrowError(
errDetails.title,
`${err.error?.detail || err.message || errDetails.detail}`,
);
return showAndThrowError(errDetails.title, `${err.error?.detail || err.message || errDetails.detail}`);
}
if (err.status == 401 && authService.accessToken) {
// Handle 401 on Refresh (RT expired)
if (req.url.endsWith("/refresh")) {
authService.logout("Your session has expired", true);
return throwError(() => "Your session has expired");
if (req.url.endsWith('/refresh')) {
authService.logout('Your session has expired', true);
return throwError(() => 'Your session has expired');
}
// Unauthenticated, AT exists but is expired (authServices.accessToken truethy), we refresh it
@ -95,18 +81,15 @@ export const Interceptor = (
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.
authService.logout(
`${err.error?.detail || err.message || "You must be authenticated"}`,
true,
);
authService.logout(`${err.error?.detail || err.message || 'You must be authenticated'}`, true);
}
console.error(err);
return showAndThrowError(
"Request Error",
`${err.error?.detail || err.message || "Unknown error, check console for details"}`,
'Request Error',
`${err.error?.detail || err.message || 'Unknown error, check console for details'}`,
);
}),
);

View File

@ -1,47 +1,42 @@
import { inject, Injectable } from "@angular/core";
import { MessageService } from "primeng/api";
import { TripStatus } from "../types/trip";
import { ApiService } from "./api.service";
import { map } from "rxjs";
import { inject, Injectable } from '@angular/core';
import { MessageService } from 'primeng/api';
import { TripStatus } from '../types/trip';
import { ApiService } from './api.service';
import { map } from 'rxjs';
type ToastSeverity = "info" | "warn" | "error" | "success";
type ToastSeverity = 'info' | 'warn' | 'error' | 'success';
@Injectable({
providedIn: "root",
providedIn: 'root',
})
export class UtilsService {
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[] = [
{ label: "pending", color: "#3258A8" },
{ label: "booked", color: "#00A341" },
{ label: "constraint", color: "#FFB900" },
{ label: "optional", color: "#625A84" },
{ label: 'pending', color: '#3258A8' },
{ label: 'booked', color: '#00A341' },
{ label: 'constraint', color: '#FFB900' },
{ label: 'optional', color: '#625A84' },
];
constructor(private ngMessageService: MessageService) {}
toGithubTRIP() {
window.open("https://github.com/itskovacs/trip", "_blank");
window.open('https://github.com/itskovacs/trip', '_blank');
}
toggleDarkMode() {
const element = document.querySelector("html");
element?.classList.toggle("dark");
const element = document.querySelector('html');
element?.classList.toggle('dark');
}
enableDarkMode() {
const element = document.querySelector("html");
element?.classList.toggle("dark", true);
const element = document.querySelector('html');
element?.classList.toggle('dark', true);
}
toast(
severity: ToastSeverity = "info",
summary = "Info",
detail = "",
life = 3000,
): void {
toast(severity: ToastSeverity = 'info', summary = 'Info', detail = '', life = 3000): void {
this.ngMessageService.add({
severity,
summary,
@ -57,12 +52,12 @@ export class UtilsService {
const lngMatch = url.match(/!4d([\d\-.]+)/);
if (!placeMatch || !latMatch || !lngMatch) {
this.toast("error", "Error", "Unrecognized Google Maps URL format");
console.error("Unrecognized Google Maps URL format");
return ["", ""];
this.toast('error', 'Error', 'Unrecognized Google Maps URL format');
console.error('Unrecognized Google Maps URL format');
return ['', ''];
}
const place = decodeURIComponent(placeMatch[1].replace(/\+/g, " ").trim());
const place = decodeURIComponent(placeMatch[1].replace(/\+/g, ' ').trim());
const latlng = `${latMatch[1]},${lngMatch[1]}`;
return [place, latlng];
}

View File

@ -1,9 +1,4 @@
export function calculateDistanceBetween(
lat1: number,
lon1: number,
lat2: number,
lon2: number,
) {
export function calculateDistanceBetween(lat1: number, lon1: number, lat2: number, lon2: number) {
// returns d in meter
const toRad = (deg: number) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);

View File

@ -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 patternDD =
/^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i;
const patternDD = /^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i;
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;
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;
function _dmsToDecimal(
deg: number,
min: number,
sec: number,
dir: string,
): number {
function _dmsToDecimal(deg: number, min: number, sec: number, dir: string): number {
const dec = deg + min / 60 + sec / 3600;
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 {
const decimals = num.toString().split(".")[1]?.length || 0;
const decimals = num.toString().split('.')[1]?.length || 0;
return num.toFixed(Math.min(decimals, 5));
}
export function checkAndParseLatLng(
value: string | number,
): [number, number] | undefined {
if (typeof value !== "string") return undefined;
export function checkAndParseLatLng(value: string | number): [number, number] | undefined {
if (typeof value !== 'string') return undefined;
// Parse PlusCode
if (OpenLocationCode.isValid(value)) {

View File

@ -1,7 +1,7 @@
import { Pipe, PipeTransform } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({ name: "linkify", standalone: true })
@Pipe({ name: 'linkify', standalone: true })
export class LinkifyPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
@ -10,11 +10,11 @@ export class LinkifyPipe implements PipeTransform {
/[&<>"']/g,
(char) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[char]!,
);
}
@ -26,7 +26,7 @@ export class LinkifyPipe implements PipeTransform {
const safeText = this.basicEscape(text);
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>`;
});

View File

@ -1,10 +1,9 @@
import * as L from "leaflet";
import "leaflet.markercluster";
import "leaflet-contextmenu";
import { Place } from "../types/poi";
import * as L from 'leaflet';
import 'leaflet.markercluster';
import 'leaflet-contextmenu';
import { Place } from '../types/poi';
export const DEFAULT_TILE_URL =
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png";
export const DEFAULT_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
export interface ContextMenuItem {
text: string;
index?: number;
@ -20,15 +19,12 @@ export interface MarkerOptions extends L.MarkerOptions {
contextmenuItems: ContextMenuItem[];
}
export function createMap(
contextMenuItems: ContextMenuItem[] = [],
tilelayer: string = DEFAULT_TILE_URL,
): L.Map {
export function createMap(contextMenuItems: ContextMenuItem[] = [], tilelayer: string = DEFAULT_TILE_URL): L.Map {
const southWest = L.latLng(-89.99, -180);
const northEast = L.latLng(89.99, 180);
const bounds = L.latLngBounds(southWest, northEast);
const map = L.map("map", {
const map = L.map('map', {
maxBoundsViscosity: 1.0,
zoomControl: false,
contextmenu: true,
@ -61,34 +57,29 @@ export function createClusterGroup(): L.MarkerClusterGroup {
const count = cluster.getChildCount();
return L.divIcon({
html: `<div class="custom-cluster">${count}</div>`,
className: "",
className: '',
iconSize: [40, 40],
});
},
});
}
export function tripDayMarker(item: {
text: string;
lat: number;
lng: number;
time?: string;
}): L.Marker {
export function tripDayMarker(item: { text: string; lat: number; lng: number; time?: string }): L.Marker {
const marker = new L.Marker([item.lat!, item.lng], {
icon: L.divIcon({
className: "bg-black rounded-full",
className: 'bg-black rounded-full',
iconSize: [14, 14],
}),
});
const touchDevice = "ontouchstart" in window;
const touchDevice = 'ontouchstart' in window;
if (!touchDevice) {
marker.bindTooltip(
`<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],
className: "class-tooltip",
className: 'class-tooltip',
},
);
}
@ -104,28 +95,25 @@ export function placeToMarker(
const options: Partial<L.MarkerOptions> = {
riseOnHover: true,
title: place.name,
alt: "",
alt: '',
};
const markerImage = isLowNet
? place.category.image
: (place.image ?? place.category.image);
const markerImage = isLowNet ? place.category.image : (place.image ?? place.category.image);
let markerClasses =
"w-full h-full rounded-full bg-center bg-cover bg-white dark:bg-surface-900";
if (grayscale) markerClasses += " grayscale";
let markerClasses = 'w-full h-full rounded-full bg-center bg-cover bg-white dark:bg-surface-900';
if (grayscale) markerClasses += ' grayscale';
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="${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>
`;
const icon = L.divIcon({
html: iconHtml.trim(),
iconSize: [56, 56],
className: "",
className: '',
});
const marker = new L.Marker([+place.lat, +place.lng], {
@ -133,12 +121,12 @@ export function placeToMarker(
icon,
});
const touchDevice = "ontouchstart" in window;
const touchDevice = 'ontouchstart' in window;
if (!touchDevice) {
marker.bindTooltip(placeHoverTooltip(place), {
direction: "right",
direction: 'right',
offset: [28, 0],
className: "class-tooltip",
className: 'class-tooltip',
});
}
return marker;
@ -146,16 +134,12 @@ export function placeToMarker(
export function gpxToPolyline(gpx: string): L.Polyline {
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(
(pt) =>
[
parseFloat(pt.getAttribute("lat")!),
parseFloat(pt.getAttribute("lon")!),
] as [number, number],
(pt) => [parseFloat(pt.getAttribute('lat')!), parseFloat(pt.getAttribute('lon')!)] as [number, number],
);
return L.polyline(latlngs, { color: "blue" });
return L.polyline(latlngs, { color: 'blue' });
}

View File

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from "@angular/core";
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: "orderBy",
name: 'orderBy',
pure: true,
standalone: true,
})
@ -9,8 +9,6 @@ export class orderByPipe implements PipeTransform {
if (!items || items.length === 0) {
return items;
}
return items
.slice()
.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0));
return items.slice().sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0));
}
}

View File

@ -1,26 +1,19 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from "@angular/core";
import { ButtonModule } from "primeng/button";
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";
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ButtonModule } from 'primeng/button';
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({
selector: "app-place-box",
selector: 'app-place-box',
standalone: true,
imports: [ButtonModule, MenuModule, AsyncPipe, LinkifyPipe],
templateUrl: "./place-box.component.html",
styleUrls: ["./place-box.component.scss"],
templateUrl: './place-box.component.html',
styleUrls: ['./place-box.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlaceBoxComponent implements OnInit {
@ -43,33 +36,33 @@ export class PlaceBoxComponent implements OnInit {
ngOnInit() {
const items = [
{
label: "Edit",
icon: "pi pi-pencil",
iconClass: "text-blue-500!",
label: 'Edit',
icon: 'pi pi-pencil',
iconClass: 'text-blue-500!',
command: () => {
this.editPlace();
},
},
{
label: "Favorite",
icon: "pi pi-star",
iconClass: "text-yellow-500!",
label: 'Favorite',
icon: 'pi pi-star',
iconClass: 'text-yellow-500!',
command: () => {
this.favoritePlace();
},
},
{
label: "Mark",
icon: "pi pi-check",
iconClass: "text-green-500!",
label: 'Mark',
icon: 'pi pi-check',
iconClass: 'text-green-500!',
command: () => {
this.visitPlace();
},
},
{
label: "Delete",
icon: "pi pi-trash",
iconClass: "text-red-500!",
label: 'Delete',
icon: 'pi pi-trash',
iconClass: 'text-red-500!',
command: () => {
this.deletePlace();
},
@ -78,9 +71,9 @@ export class PlaceBoxComponent implements OnInit {
if (this.selectedPlace?.gpx) {
items.unshift({
label: "Display GPX",
icon: "pi pi-compass",
iconClass: "text-gray-500!",
label: 'Display GPX',
icon: 'pi pi-compass',
iconClass: 'text-gray-500!',
command: () => {
this.displayGPX();
},
@ -89,7 +82,7 @@ export class PlaceBoxComponent implements OnInit {
this.menuItems = [
{
label: "Place",
label: 'Place',
items: items,
},
];

View File

@ -1,19 +1,13 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from "@angular/core";
import { ButtonModule } from "primeng/button";
import { Place } from "../../types/poi";
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { Place } from '../../types/poi';
@Component({
selector: "app-place-gpx",
selector: 'app-place-gpx',
standalone: true,
imports: [ButtonModule],
templateUrl: "./place-gpx.component.html",
styleUrls: ["./place-gpx.component.scss"],
templateUrl: './place-gpx.component.html',
styleUrls: ['./place-gpx.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlaceGPXComponent {
@ -33,6 +27,6 @@ export class PlaceGPXComponent {
}
downloadTrace() {
this.downloadEmitter.emit()
this.downloadEmitter.emit();
}
}

View File

@ -1,4 +1,4 @@
import { Category, Place } from "./poi";
import { Category, Place } from './poi';
export interface Settings {
username: string;

View File

@ -1,4 +1,4 @@
import { Place } from "./poi";
import { Place } from './poi';
export interface TripBase {
id: number;

View File

@ -2,6 +2,4 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));

View File

@ -388,24 +388,20 @@ export const TripThemePreset = definePreset(Aura, {
overlay: {
select: {
borderRadius: '{border.radius.md}',
shadow:
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
popover: {
borderRadius: '{border.radius.md}',
padding: '0.75rem',
shadow:
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
modal: {
borderRadius: '{border.radius.xl}',
padding: '1.25rem',
shadow:
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
shadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
},
navigation: {
shadow:
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
},
colorScheme: {

View File

@ -1,9 +1,9 @@
@use "primeicons/primeicons.css";
@use 'primeicons/primeicons.css';
@plugin 'tailwindcss-primeui';
@variant dark (&:where(.dark, .dark *));
@layer tailwind {
@import "tailwindcss";
@import 'tailwindcss';
}
* {
@ -30,17 +30,17 @@
html {
font-size: 15px;
font-family:
"Inter",
'Inter',
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol';
line-height: normal;
}
@ -82,7 +82,7 @@ html {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
font-family: "Inter", sans-serif;
font-family: 'Inter', sans-serif;
font-size: 15px;
}