diff --git a/backend/storage/assets/accommodation.png b/backend/storage/assets/accommodation.png deleted file mode 100644 index 1e10866..0000000 Binary files a/backend/storage/assets/accommodation.png and /dev/null differ diff --git a/backend/storage/assets/adventure.png b/backend/storage/assets/adventure.png deleted file mode 100644 index 2530d32..0000000 Binary files a/backend/storage/assets/adventure.png and /dev/null differ diff --git a/backend/storage/assets/culture.png b/backend/storage/assets/culture.png deleted file mode 100644 index 93d2b80..0000000 Binary files a/backend/storage/assets/culture.png and /dev/null differ diff --git a/backend/storage/assets/entertainment.png b/backend/storage/assets/entertainment.png deleted file mode 100644 index 51b74b1..0000000 Binary files a/backend/storage/assets/entertainment.png and /dev/null differ diff --git a/backend/storage/assets/event.png b/backend/storage/assets/event.png deleted file mode 100644 index 329b80c..0000000 Binary files a/backend/storage/assets/event.png and /dev/null differ diff --git a/backend/storage/assets/food.png b/backend/storage/assets/food.png deleted file mode 100644 index e5d1642..0000000 Binary files a/backend/storage/assets/food.png and /dev/null differ diff --git a/backend/storage/assets/nature.png b/backend/storage/assets/nature.png deleted file mode 100644 index acc8bbf..0000000 Binary files a/backend/storage/assets/nature.png and /dev/null differ diff --git a/backend/storage/assets/wellness.png b/backend/storage/assets/wellness.png deleted file mode 100644 index 52457d8..0000000 Binary files a/backend/storage/assets/wellness.png and /dev/null differ diff --git a/backend/storage/config.yml b/backend/storage/config.yml deleted file mode 100644 index e69de29..0000000 diff --git a/backend/trip/__init__.py b/backend/trip/__init__.py index 3e8d9f9..5b60188 100644 --- a/backend/trip/__init__.py +++ b/backend/trip/__init__.py @@ -1 +1 @@ -__version__ = "1.4.0" +__version__ = "1.5.0" diff --git a/backend/trip/config.py b/backend/trip/config.py index 1638aa7..6a1d4da 100644 --- a/backend/trip/config.py +++ b/backend/trip/config.py @@ -4,17 +4,19 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): - ASSETS_FOLDER: str = "storage/assets" FRONTEND_FOLDER: str = "frontend" SQLITE_FILE: str = "storage/trip.sqlite" + + ASSETS_FOLDER: str = "storage/assets" + ASSETS_URL: str = "/api/assets" + PLACE_IMAGE_SIZE: int = 500 + TRIP_IMAGE_SIZE: int = 600 + SECRET_KEY: str = secrets.token_hex(32) ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 REFRESH_TOKEN_EXPIRE_MINUTES: int = 1440 - PLACE_IMAGE_SIZE: int = 500 - TRIP_IMAGE_SIZE: int = 600 - class Config: env_file = "storage/config.yml" diff --git a/backend/trip/db/core.py b/backend/trip/db/core.py index e1f5349..d680ea6 100644 --- a/backend/trip/db/core.py +++ b/backend/trip/db/core.py @@ -3,7 +3,7 @@ from sqlalchemy.engine import Engine from sqlmodel import Session, SQLModel, create_engine from ..config import settings -from ..models.models import Category, Image +from ..models.models import Category _engine = None @@ -32,45 +32,17 @@ def init_db(): def init_user_data(session: Session, username: str): data = [ - { - "image": {"filename": "nature.png", "user": username}, - "category": {"user": username, "name": "Nature & Outdoor"}, - }, - { - "image": {"filename": "entertainment.png", "user": username}, - "category": {"user": username, "name": "Entertainment & Leisure"}, - }, - { - "image": {"filename": "culture.png", "user": username}, - "category": {"user": username, "name": "Culture"}, - }, - { - "image": {"filename": "food.png", "user": username}, - "category": {"user": username, "name": "Food & Drink"}, - }, - { - "image": {"filename": "adventure.png", "user": username}, - "category": {"user": username, "name": "Adventure & Sports"}, - }, - { - "image": {"filename": "event.png", "user": username}, - "category": {"user": username, "name": "Festival & Event"}, - }, - { - "image": {"filename": "wellness.png", "user": username}, - "category": {"user": username, "name": "Wellness"}, - }, - { - "image": {"filename": "accommodation.png", "user": username}, - "category": {"user": username, "name": "Accommodation"}, - }, + {"category": {"user": username, "name": "Nature & Outdoor"}}, + {"category": {"user": username, "name": "Entertainment & Leisure"}}, + {"category": {"user": username, "name": "Culture"}}, + {"category": {"user": username, "name": "Food & Drink"}}, + {"category": {"user": username, "name": "Adventure & Sports"}}, + {"category": {"user": username, "name": "Festival & Event"}}, + {"category": {"user": username, "name": "Wellness"}}, + {"category": {"user": username, "name": "Accommodation"}}, ] for element in data: - img = Image(**element["image"]) - session.add(img) - session.flush() - - category = Category(**element["category"], image_id=img.id) + category = Category(**element["category"]) session.add(category) session.commit() diff --git a/backend/trip/models/models.py b/backend/trip/models/models.py index ffca778..81eac07 100644 --- a/backend/trip/models/models.py +++ b/backend/trip/models/models.py @@ -7,6 +7,8 @@ from pydantic import BaseModel, StringConstraints, field_validator from sqlalchemy import MetaData from sqlmodel import Field, Relationship, SQLModel +from ..config import settings + convention = { "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", @@ -18,6 +20,13 @@ convention = { SQLModel.metadata = MetaData(naming_convention=convention) +def _prefix_assets_url(filename: str) -> str: + base = settings.ASSETS_URL + if not base.endswith("/"): + base += "/" + return base + filename + + class TripItemStatusEnum(str, Enum): PENDING = "pending" CONFIRMED = "booked" @@ -99,7 +108,7 @@ class Category(CategoryBase, table=True): class CategoryCreate(CategoryBase): name: str - image: str + image: str | None = None class CategoryUpdate(CategoryBase): @@ -109,13 +118,16 @@ class CategoryUpdate(CategoryBase): class CategoryRead(CategoryBase): id: int - image: str - image_id: int + image: str | None + image_id: int | None @classmethod def serialize(cls, obj: Category) -> "CategoryRead": return cls( - id=obj.id, name=obj.name, image_id=obj.image_id, image=obj.image.filename if obj.image else None + id=obj.id, + name=obj.name, + image_id=obj.image_id, + image=_prefix_assets_url(obj.image.filename) if obj.image else "/favicon.png", ) @@ -194,7 +206,7 @@ class PlaceRead(PlaceBase): price=obj.price, duration=obj.duration, visited=obj.visited, - image=obj.image.filename if obj.image else None, + image=_prefix_assets_url(obj.image.filename) if obj.image else None, image_id=obj.image_id, favorite=obj.favorite, gpx=("1" if obj.gpx else None) @@ -241,7 +253,7 @@ class TripReadBase(TripBase): id=obj.id, name=obj.name, archived=obj.archived, - image=obj.image.filename if obj.image else None, + image=_prefix_assets_url(obj.image.filename) if obj.image else None, image_id=obj.image_id, days=len(obj.days), ) @@ -260,7 +272,7 @@ class TripRead(TripBase): id=obj.id, name=obj.name, archived=obj.archived, - image=obj.image.filename if obj.image else None, + image=_prefix_assets_url(obj.image.filename) if obj.image else None, image_id=obj.image_id, days=[TripDayRead.serialize(day) for day in obj.days], places=[PlaceRead.serialize(place) for place in obj.places], diff --git a/backend/trip/routers/categories.py b/backend/trip/routers/categories.py index c1db7b8..578cdf8 100644 --- a/backend/trip/routers/categories.py +++ b/backend/trip/routers/categories.py @@ -29,16 +29,17 @@ def post_category( ) -> CategoryRead: new_category = Category(name=category.name, user=current_user) - image_bytes = b64img_decode(category.image) - filename = save_image_to_file(image_bytes, settings.PLACE_IMAGE_SIZE) - if not filename: - raise HTTPException(status_code=400, detail="Bad request") + if category.image: + image_bytes = b64img_decode(category.image) + filename = save_image_to_file(image_bytes, settings.PLACE_IMAGE_SIZE) + if not filename: + raise HTTPException(status_code=400, detail="Bad request") - image = Image(filename=filename, user=current_user) - session.add(image) - session.commit() - session.refresh(image) - new_category.image_id = image.id + image = Image(filename=filename, user=current_user) + session.add(image) + session.commit() + session.refresh(image) + new_category.image_id = image.id session.add(new_category) session.commit() @@ -57,9 +58,10 @@ def put_category( verify_exists_and_owns(current_user, db_category) category_data = category.model_dump(exclude_unset=True) - if category_data.get("image"): + category_image = category_data.pop("image", None) + if category_image: try: - image_bytes = b64img_decode(category_data.pop("image")) + image_bytes = b64img_decode(category_image) except Exception: raise HTTPException(status_code=400, detail="Bad request") @@ -105,6 +107,16 @@ def delete_category( if get_category_placess_cnt(session, category_id, current_user) > 0: raise HTTPException(status_code=409, detail="The resource is not orphan") + if db_category.image: + try: + remove_image(db_category.image.filename) + session.delete(db_category.image) + except Exception: + raise HTTPException( + status_code=500, + detail="Roses are red, violets are blue, if you're reading this, I'm sorry for you", + ) + session.delete(db_category) session.commit() return {} diff --git a/src/src/app/components/dashboard/dashboard.component.html b/src/src/app/components/dashboard/dashboard.component.html index 9b95ada..5df3ec9 100644 --- a/src/src/app/components/dashboard/dashboard.component.html +++ b/src/src/app/components/dashboard/dashboard.component.html @@ -7,7 +7,7 @@ }
- +
@@ -65,7 +65,7 @@ @for (p of visiblePlaces; track p.id) {
- +

{{ p.name }}

@@ -166,59 +166,68 @@ - -
-
-

Map parameters

- You can customize the default view on map loading + +
+

Low Network Mode

+ You can disable Low Network Mode. Default is true. Display Category + image instead of Place image. +
+
+
Low Network Mode
+ +
+ +
+
+
+

Map parameters

+ You can customize the default view on map loading +
+ +
- -
+
+ + + + -
- - - - + + + + +
- - - - -
+
+

Currency

+
+
+ + + + +
-
-

Currency

-
-
- - - - -
- -
-

Filters

- You can customize the categories and attributes to hide by - default -
-
- - - - -
- - -
- -
+
+

Filters

+ You can customize the categories to hide by default +
+
+ + + + +
+
+ +
+ diff --git a/src/src/app/components/dashboard/dashboard.component.ts b/src/src/app/components/dashboard/dashboard.component.ts index 1e485bb..d969226 100644 --- a/src/src/app/components/dashboard/dashboard.component.ts +++ b/src/src/app/components/dashboard/dashboard.component.ts @@ -78,6 +78,7 @@ export interface MarkerOptions extends L.MarkerOptions { export class DashboardComponent implements AfterViewInit { searchInput = new FormControl(""); info: Info | undefined; + isLowNet: boolean = false; viewSettings = false; viewFilters = false; @@ -111,6 +112,7 @@ export class DashboardComponent implements AfterViewInit { private fb: FormBuilder, ) { this.currencySigns = this.utilsService.currencySigns(); + this.isLowNet = this.utilsService.isLowNet; this.settingsForm = this.fb.group({ mapLat: [ @@ -244,6 +246,13 @@ export class DashboardComponent implements AfterViewInit { if (this.viewMarkersList) this.setVisibleMarkers(); } + toggleLowNet() { + this.utilsService.toggleLowNet(); + setTimeout(() => { + this.updateMarkersAndClusters(); + }, 200); + } + get filteredPlaces(): Place[] { return this.places.filter((p) => { if (!this.filter_display_visited && p.visited) return false; @@ -264,7 +273,7 @@ export class DashboardComponent implements AfterViewInit { } placeToMarker(place: Place): L.Marker { - let marker = placeToMarker(place); + let marker = placeToMarker(place, this.isLowNet); marker .on("click", (e) => { this.selectedPlace = place; @@ -456,6 +465,7 @@ export class DashboardComponent implements AfterViewInit { if (index > -1) this.places.splice(index, 1); this.closePlaceBox(); this.updateMarkersAndClusters(); + if (this.viewMarkersList) this.setVisibleMarkers(); }, }); }, @@ -499,6 +509,7 @@ export class DashboardComponent implements AfterViewInit { setTimeout(() => { this.updateMarkersAndClusters(); }, 10); + if (this.viewMarkersList) this.setVisibleMarkers(); }, }); }, @@ -561,6 +572,7 @@ export class DashboardComponent implements AfterViewInit { toggleMarkersList() { this.viewMarkersList = !this.viewMarkersList; + if (this.viewMarkersList) this.setVisibleMarkers(); } toggleMarkersListSearch() { @@ -649,7 +661,13 @@ export class DashboardComponent implements AfterViewInit { this.activeCategories = new Set( this.categories.map((c) => c.name), ); - this.updateMarkersAndClusters(); + this.places = this.places.map((p) => { + if (p.category.id == category.id) return { ...p, category }; + return p; + }); + setTimeout(() => { + this.updateMarkersAndClusters(); + }, 100); } }, }); diff --git a/src/src/app/components/trip/trip.component.html b/src/src/app/components/trip/trip.component.html index cbe4d8c..748f987 100644 --- a/src/src/app/components/trip/trip.component.html +++ b/src/src/app/components/trip/trip.component.html @@ -93,7 +93,7 @@ @if (tripitem.place) {
- {{ tripitem.place.name }}
@@ -146,7 +146,8 @@
@@ -224,10 +225,14 @@ {{ trip?.name }} places
- +
+ + +
-
+
+
@if (!selectedItem) { @@ -259,7 +264,7 @@ @for (p of places; track p.id) {
- +

{{ p.name }}

@@ -270,7 +275,7 @@ class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded flex gap-2 items-center truncate">{{ p.category.name }} - @if (p.placeUsage) { + @if (isPlaceUsed(p.id)) { } @else { @@ -395,4 +400,15 @@ }
- \ No newline at end of file + + +@if (isMapFullscreen) { +
+ +
+ +
+ +
+} \ No newline at end of file diff --git a/src/src/app/components/trip/trip.component.scss b/src/src/app/components/trip/trip.component.scss index 7c6b484..f22b193 100644 --- a/src/src/app/components/trip/trip.component.scss +++ b/src/src/app/components/trip/trip.component.scss @@ -8,3 +8,13 @@ background-color: white !important; } } + +.fullscreen-map { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + border-radius: 0 !important; + box-shadow: none !important; +} diff --git a/src/src/app/components/trip/trip.component.ts b/src/src/app/components/trip/trip.component.ts index 38cc16a..b2555f1 100644 --- a/src/src/app/components/trip/trip.component.ts +++ b/src/src/app/components/trip/trip.component.ts @@ -31,10 +31,6 @@ import { AsyncPipe } from "@angular/common"; import { MenuItem } from "primeng/api"; import { MenuModule } from "primeng/menu"; -interface PlaceWithUsage extends Place { - placeUsage?: boolean; -} - @Component({ selector: "app-trip", standalone: true, @@ -59,15 +55,17 @@ export class TripComponent implements AfterViewInit { statuses: TripStatus[] = []; hoveredElement: HTMLElement | undefined; currency$: Observable; + placesUsedInTable = new Set(); trip: Trip | undefined; tripMapAntLayer: undefined; tripMapAntLayerDayID: number | undefined; + isMapFullscreen: boolean = false; totalPrice: number = 0; dayStatsCache = new Map(); - places: PlaceWithUsage[] = []; + places: Place[] = []; flattenedTripItems: FlattenedTripItem[] = []; menuTripActionsItems: MenuItem[] = []; @@ -176,7 +174,18 @@ export class TripComponent implements AfterViewInit { this.updateTotalPrice(); - this.map = createMap(); + let contentMenuItems = [ + { + text: "Copy coordinates", + callback: (e: any) => { + const latlng = e.latlng; + navigator.clipboard.writeText( + `${parseFloat(latlng.lat).toFixed(5)}, ${parseFloat(latlng.lng).toFixed(5)}`, + ); + }, + }, + ]; + this.map = createMap(contentMenuItems); this.markerClusterGroup = createClusterGroup().addTo(this.map); this.setPlacesAndMarkers(); @@ -224,6 +233,10 @@ export class TripComponent implements AfterViewInit { })) as (TripItem & { status: TripStatus })[]; } + isPlaceUsed(id: number): boolean { + return this.placesUsedInTable.has(id); + } + statusToTripStatus(status?: string): TripStatus | undefined { if (!status) return undefined; return this.statuses.find((s) => s.label == status) as TripStatus; @@ -250,18 +263,21 @@ export class TripComponent implements AfterViewInit { ); } - setPlacesAndMarkers() { - let usedPlaces = this.flattenedTripItems.map((i) => i.place?.id); - this.places = (this.trip?.places || []).map((p) => { - let ret: PlaceWithUsage = { ...p }; - if (usedPlaces.includes(p.id)) ret.placeUsage = true; - return ret; + makePlacesUsedInTable() { + this.placesUsedInTable.clear(); + this.flattenedTripItems.forEach((i) => { + if (i.place?.id) this.placesUsedInTable.add(i.place.id); }); + } + + setPlacesAndMarkers() { + this.makePlacesUsedInTable(); + this.places = this.trip?.places || []; this.places.sort((a, b) => a.name.localeCompare(b.name)); this.markerClusterGroup?.clearLayers(); this.places.forEach((p) => { - const marker = placeToMarker(p); + const marker = placeToMarker(p, false); this.markerClusterGroup?.addLayer(marker); }); } @@ -274,6 +290,15 @@ export class TripComponent implements AfterViewInit { ); } + toggleMapFullscreen() { + this.isMapFullscreen = !this.isMapFullscreen; + + setTimeout(() => { + this.map.invalidateSize(); + this.resetMapBounds(); + }, 50); + } + updateTotalPrice(n?: number) { if (n) this.totalPrice += n; else @@ -352,7 +377,7 @@ export class TripComponent implements AfterViewInit { this.map.fitBounds(coords, { padding: [30, 30] }); const path = antPath(coords, { - delay: 400, + delay: 600, dashArray: [10, 20], weight: 5, color: "#0000FF", @@ -607,6 +632,7 @@ export class TripComponent implements AfterViewInit { this.trip?.days!, ); this.dayStatsCache.delete(day.id); + this.makePlacesUsedInTable(); } }, }); @@ -685,6 +711,7 @@ export class TripComponent implements AfterViewInit { ); } if (item.price) this.updateTotalPrice(item.price); + if (item.place?.id) this.placesUsedInTable.add(item.place.id); }, }); }, @@ -718,6 +745,7 @@ export class TripComponent implements AfterViewInit { modal.onClose.subscribe({ next: (it: TripItem | null) => { if (!it) return; + if (item.place?.id) this.placesUsedInTable.delete(item.place.id); this.apiService .putTripDayItem(it, this.trip?.id!, item.day_id, item.id) @@ -744,6 +772,7 @@ export class TripComponent implements AfterViewInit { this.dayStatsCache.delete(item.day_id); } + if (item.place?.id) this.placesUsedInTable.add(item.place.id); const updatedPrice = -(item.price || 0) + (it.price || 0); this.updateTotalPrice(updatedPrice); @@ -786,6 +815,8 @@ export class TripComponent implements AfterViewInit { this.flattenedTripItems = this.flattenTripDayItems( this.trip?.days!, ); + if (item.place?.id) + this.placesUsedInTable.delete(item.place.id); this.dayStatsCache.delete(item.day_id); this.selectedItem = undefined; this.resetPlaceHighlightMarker(); diff --git a/src/src/app/components/trips/trips.component.html b/src/src/app/components/trips/trips.component.html index 77cbacb..625f058 100644 --- a/src/src/app/components/trips/trips.component.html +++ b/src/src/app/components/trips/trips.component.html @@ -15,7 +15,7 @@
+ [src]="trip.image || 'cover.webp'" />

{{ trip.name }}

diff --git a/src/src/app/modals/category-create-modal/category-create-modal.component.ts b/src/src/app/modals/category-create-modal/category-create-modal.component.ts index 013746f..3bbd5ae 100644 --- a/src/src/app/modals/category-create-modal/category-create-modal.component.ts +++ b/src/src/app/modals/category-create-modal/category-create-modal.component.ts @@ -1,5 +1,10 @@ import { Component } from "@angular/core"; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from "@angular/forms"; import { ButtonModule } from "primeng/button"; import { DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog"; import { FloatLabelModule } from "primeng/floatlabel"; @@ -8,7 +13,13 @@ import { FocusTrapModule } from "primeng/focustrap"; @Component({ selector: "app-category-create-modal", - imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, FocusTrapModule], + imports: [ + FloatLabelModule, + InputTextModule, + ButtonModule, + ReactiveFormsModule, + FocusTrapModule, + ], standalone: true, templateUrl: "./category-create-modal.component.html", styleUrl: "./category-create-modal.component.scss", @@ -21,12 +32,12 @@ export class CategoryCreateModalComponent { constructor( private ref: DynamicDialogRef, private fb: FormBuilder, - private config: DynamicDialogConfig + private config: DynamicDialogConfig, ) { this.categoryForm = this.fb.group({ id: -1, name: ["", Validators.required], - image: ["", Validators.required], + image: null, }); if (this.config.data) { diff --git a/src/src/app/modals/place-create-modal/place-create-modal.component.ts b/src/src/app/modals/place-create-modal/place-create-modal.component.ts index 11d6270..f755338 100644 --- a/src/src/app/modals/place-create-modal/place-create-modal.component.ts +++ b/src/src/app/modals/place-create-modal/place-create-modal.component.ts @@ -99,7 +99,6 @@ export class PlaceCreateModalComponent { if (this.config.data) { let patchValue: Place = this.config.data.place; - if (patchValue.imageDefault) delete patchValue["image"]; this.placeForm.patchValue(patchValue); } diff --git a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html index b9f640b..b72f826 100644 --- a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html +++ b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html @@ -31,7 +31,7 @@
- +
{{ place.name }}
diff --git a/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.html b/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.html index 1df0cba..e4f6163 100644 --- a/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.html +++ b/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.html @@ -24,7 +24,7 @@ @for (p of selectedPlaces; track p.id) {
- +

{{ p.name }}

diff --git a/src/src/app/services/api.service.ts b/src/src/app/services/api.service.ts index 6bf145f..1643a0c 100644 --- a/src/src/app/services/api.service.ts +++ b/src/src/app/services/api.service.ts @@ -4,7 +4,6 @@ import { Category, Place } from "../types/poi"; import { BehaviorSubject, distinctUntilChanged, - map, Observable, shareReplay, tap, @@ -18,7 +17,6 @@ import { Trip, TripBase, TripDay, TripItem } from "../types/trip"; }) export class ApiService { public apiBaseUrl: string = "/api"; - public assetsBaseUrl: string = "/api/assets"; private categoriesSubject = new BehaviorSubject(null); public categories$: Observable = @@ -33,23 +31,6 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + "/info"); } - _normalizeTripImage(trip: Trip | TripBase): Trip | TripBase { - if (trip.image) trip.image = `${this.assetsBaseUrl}/${trip.image}`; - else trip.image = "cover.webp"; - return trip; - } - - _normalizePlaceImage(place: Place): Place { - if (place.image) { - place.image = `${this.assetsBaseUrl}/${place.image}`; - place.imageDefault = false; - } else { - place.image = `${this.assetsBaseUrl}/${(place.category as Category).image}`; - place.imageDefault = true; - } - return place; - } - _categoriesSubjectNext(categories: Category[]) { this.categoriesSubject.next( categories.sort((categoryA: Category, categoryB: Category) => @@ -63,11 +44,6 @@ export class ApiService { return this.httpClient .get(`${this.apiBaseUrl}/categories`) .pipe( - map((resp) => { - return resp.map((c) => { - return { ...c, image: `${this.assetsBaseUrl}/${c.image}` }; - }); - }), tap((categories) => this._categoriesSubjectNext(categories)), distinctUntilChanged(), shareReplay(), @@ -80,12 +56,6 @@ export class ApiService { return this.httpClient .post(this.apiBaseUrl + "/categories", c) .pipe( - map((category) => { - return { - ...category, - image: `${this.assetsBaseUrl}/${category.image}`, - }; - }), tap((category) => this._categoriesSubjectNext([ ...(this.categoriesSubject.value || []), @@ -99,12 +69,6 @@ export class ApiService { return this.httpClient .put(this.apiBaseUrl + `/categories/${c_id}`, c) .pipe( - map((category) => { - return { - ...category, - image: `${this.assetsBaseUrl}/${category.image}`, - }; - }), tap((category) => { let categories = this.categoriesSubject.value || []; let categoryIndex = categories?.findIndex((c) => c.id == c_id) || -1; @@ -133,29 +97,27 @@ export class ApiService { } getPlaces(): Observable { - return this.httpClient.get(`${this.apiBaseUrl}/places`).pipe( - map((resp) => resp.map((p) => this._normalizePlaceImage(p))), - distinctUntilChanged(), - shareReplay(), - ); + return this.httpClient + .get(`${this.apiBaseUrl}/places`) + .pipe(distinctUntilChanged(), shareReplay()); } postPlace(place: Place): Observable { - return this.httpClient - .post(`${this.apiBaseUrl}/places`, place) - .pipe(map((p) => this._normalizePlaceImage(p))); + return this.httpClient.post(`${this.apiBaseUrl}/places`, place); } postPlaces(places: Partial): Observable { - return this.httpClient - .post(`${this.apiBaseUrl}/places/batch`, places) - .pipe(map((resp) => resp.map((p) => this._normalizePlaceImage(p)))); + return this.httpClient.post( + `${this.apiBaseUrl}/places/batch`, + places, + ); } putPlace(place_id: number, place: Partial): Observable { - return this.httpClient - .put(`${this.apiBaseUrl}/places/${place_id}`, place) - .pipe(map((p) => this._normalizePlaceImage(p))); + return this.httpClient.put( + `${this.apiBaseUrl}/places/${place_id}`, + place, + ); } deletePlace(place_id: number): Observable { @@ -165,50 +127,23 @@ export class ApiService { } getPlaceGPX(place_id: number): Observable { - return this.httpClient - .get(`${this.apiBaseUrl}/places/${place_id}`) - .pipe(map((p) => this._normalizePlaceImage(p))); + return this.httpClient.get(`${this.apiBaseUrl}/places/${place_id}`); } getTrips(): Observable { - return this.httpClient.get(`${this.apiBaseUrl}/trips`).pipe( - map((resp) => { - return resp.map((trip: TripBase) => { - trip = this._normalizeTripImage(trip) as TripBase; - return trip; - }); - }), - distinctUntilChanged(), - shareReplay(), - ); + return this.httpClient + .get(`${this.apiBaseUrl}/trips`) + .pipe(distinctUntilChanged(), shareReplay()); } getTrip(id: number): Observable { - return this.httpClient.get(`${this.apiBaseUrl}/trips/${id}`).pipe( - map((trip) => { - trip = this._normalizeTripImage(trip) as Trip; - trip.places = trip.places.map((p) => this._normalizePlaceImage(p)); - trip.days.map((day) => { - day.items.forEach((item) => { - if (item.place) this._normalizePlaceImage(item.place); - }); - }); - return trip; - }), - distinctUntilChanged(), - shareReplay(), - ); + return this.httpClient + .get(`${this.apiBaseUrl}/trips/${id}`) + .pipe(distinctUntilChanged(), shareReplay()); } postTrip(trip: TripBase): Observable { - return this.httpClient - .post(`${this.apiBaseUrl}/trips`, trip) - .pipe( - map((trip) => { - trip = this._normalizeTripImage(trip) as TripBase; - return trip; - }), - ); + return this.httpClient.post(`${this.apiBaseUrl}/trips`, trip); } deleteTrip(trip_id: number): Observable { @@ -216,20 +151,10 @@ export class ApiService { } putTrip(trip: Partial, trip_id: number): Observable { - return this.httpClient - .put(`${this.apiBaseUrl}/trips/${trip_id}`, trip) - .pipe( - map((trip) => { - trip = this._normalizeTripImage(trip) as Trip; - trip.places = trip.places.map((p) => this._normalizePlaceImage(p)); - trip.days.map((day) => { - day.items.forEach((item) => { - if (item.place) this._normalizePlaceImage(item.place); - }); - }); - return trip; - }), - ); + return this.httpClient.put( + `${this.apiBaseUrl}/trips/${trip_id}`, + trip, + ); } postTripDay(tripDay: TripDay, trip_id: number): Observable { @@ -240,19 +165,10 @@ export class ApiService { } putTripDay(tripDay: Partial, trip_id: number): Observable { - return this.httpClient - .put( - `${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`, - tripDay, - ) - .pipe( - map((td) => { - td.items.forEach((item) => { - if (item.place) this._normalizePlaceImage(item.place); - }); - return td; - }), - ); + return this.httpClient.put( + `${this.apiBaseUrl}/trips/${trip_id}/days/${tripDay.id}`, + tripDay, + ); } deleteTripDay(trip_id: number, day_id: number): Observable { @@ -266,17 +182,10 @@ export class ApiService { trip_id: number, day_id: number, ): Observable { - return this.httpClient - .post( - `${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`, - item, - ) - .pipe( - map((item) => { - if (item.place) item.place = this._normalizePlaceImage(item.place); - return item; - }), - ); + return this.httpClient.post( + `${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items`, + item, + ); } putTripDayItem( @@ -285,17 +194,10 @@ export class ApiService { day_id: number, item_id: number, ): Observable { - return this.httpClient - .put( - `${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`, - item, - ) - .pipe( - map((item) => { - if (item.place) item.place = this._normalizePlaceImage(item.place); - return item; - }), - ); + return this.httpClient.put( + `${this.apiBaseUrl}/trips/${trip_id}/days/${day_id}/items/${item_id}`, + item, + ); } deleteTripDayItem( @@ -336,21 +238,10 @@ export class ApiService { settingsUserImport(formdata: FormData): Observable { const headers = { enctype: "multipart/form-data" }; - return this.httpClient - .post< - Place[] - >(`${this.apiBaseUrl}/settings/import`, formdata, { headers: headers }) - .pipe( - map((resp) => { - return resp.map((c) => { - if (c.image) c.image = `${this.assetsBaseUrl}/${c.image}`; - else { - c.image = `${this.assetsBaseUrl}/${(c.category as Category).image}`; - c.imageDefault = true; - } - return c; - }); - }), - ); + return this.httpClient.post( + `${this.apiBaseUrl}/settings/import`, + formdata, + { headers: headers }, + ); } } diff --git a/src/src/app/services/utils.service.ts b/src/src/app/services/utils.service.ts index 2bfade8..8afa3bd 100644 --- a/src/src/app/services/utils.service.ts +++ b/src/src/app/services/utils.service.ts @@ -4,19 +4,33 @@ import { TripStatus } from "../types/trip"; import { ApiService } from "./api.service"; import { map } from "rxjs"; +const DISABLE_LOWNET = "TRIP_DISABLE_LOWNET"; + @Injectable({ providedIn: "root", }) export class UtilsService { private apiService = inject(ApiService); currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? "€")); + public isLowNet: boolean = true; - constructor(private ngMessageService: MessageService) {} + constructor(private ngMessageService: MessageService) { + this.isLowNet = !localStorage.getItem(DISABLE_LOWNET); + } toGithubTRIP() { window.open("https://github.com/itskovacs/trip", "_blank"); } + toggleLowNet() { + if (this.isLowNet) { + localStorage.setItem(DISABLE_LOWNET, "1"); + } else { + localStorage.removeItem(DISABLE_LOWNET); + } + this.isLowNet = !this.isLowNet; + } + get statuses(): TripStatus[] { return [ { label: "pending", color: "#3258A8" }, @@ -35,17 +49,6 @@ export class UtilsService { }); } - getObjectDiffFields(a: T, b: T): Partial { - const diff: Partial = {}; - - for (const key in b) { - if (!Object.is(a[key], b[key]) && JSON.stringify(a[key]) !== JSON.stringify(b[key])) { - diff[key] = b[key]; - } - } - return diff; - } - parseGoogleMapsUrl(url: string): [string, string] { const match = url.match(/place\/(.*)\/@([\d\-.]+,[\d\-.]+)/); diff --git a/src/src/app/shared/map.ts b/src/src/app/shared/map.ts index 946de93..8a71251 100644 --- a/src/src/app/shared/map.ts +++ b/src/src/app/shared/map.ts @@ -69,7 +69,10 @@ export function createClusterGroup(): L.MarkerClusterGroup { }); } -export function placeToMarker(place: Place): L.Marker { +export function placeToMarker( + place: Place, + isLowNet: boolean = true, +): L.Marker { let marker: L.Marker; let options: any = { riseOnHover: true, @@ -79,8 +82,13 @@ export function placeToMarker(place: Place): L.Marker { }; marker = new L.Marker([+place.lat, +place.lng], options); + + const markerImage = isLowNet + ? place.category.image + : (place.image ?? place.category.image); + marker.options.icon = L.icon({ - iconUrl: place.image!, + iconUrl: markerImage, iconSize: [56, 56], iconAnchor: [28, 28], shadowSize: [0, 0], diff --git a/src/src/app/shared/place-box/place-box.component.html b/src/src/app/shared/place-box/place-box.component.html index 771cfcd..b56ce2b 100644 --- a/src/src/app/shared/place-box/place-box.component.html +++ b/src/src/app/shared/place-box/place-box.component.html @@ -2,9 +2,10 @@
-
- -
+
+ +

{{ selectedPlace.name }}

diff --git a/src/src/app/types/poi.ts b/src/src/app/types/poi.ts index 135033b..0b7b776 100644 --- a/src/src/app/types/poi.ts +++ b/src/src/app/types/poi.ts @@ -22,5 +22,4 @@ export interface Place { allowdog?: boolean; visited?: boolean; favorite?: boolean; - imageDefault?: boolean; // Injected in service }