Compare commits
No commits in common. "babef48ce747323a90f960b979402b3d6af86c71" and "dec5eebd72e76e2260262f21e8746f731381e7c7" have entirely different histories.
babef48ce7
...
dec5eebd72
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
**/storage/**
|
|
||||||
|
|
||||||
**/**.jpeg
|
|
||||||
3
backend/.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
/.venv/
|
|
||||||
|
|
||||||
**/__pycache__/
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@ -11,8 +11,7 @@ from ..models.models import (Category, GooglePlaceResult, Image, Place,
|
|||||||
User)
|
User)
|
||||||
from ..security import verify_exists_and_owns
|
from ..security import verify_exists_and_owns
|
||||||
from ..utils.gmaps import (compute_avg_price, compute_description,
|
from ..utils.gmaps import (compute_avg_price, compute_description,
|
||||||
gmaps_get_boundaries, gmaps_photo, gmaps_textsearch,
|
gmaps_get_boundaries, gmaps_textsearch)
|
||||||
gmaps_types_mapper)
|
|
||||||
from ..utils.utils import (b64img_decode, download_file, patch_image,
|
from ..utils.utils import (b64img_decode, download_file, patch_image,
|
||||||
save_image_to_file)
|
save_image_to_file)
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ def read_places(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=PlaceRead)
|
@router.post("", response_model=PlaceRead)
|
||||||
async def create_place(
|
def create_place(
|
||||||
place: PlaceCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
place: PlaceCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
||||||
) -> PlaceRead:
|
) -> PlaceRead:
|
||||||
new_place = Place(
|
new_place = Place(
|
||||||
@ -51,20 +50,11 @@ async def create_place(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if place.image:
|
if place.image:
|
||||||
if place.image[:4] == "http":
|
|
||||||
fp = await download_file(place.image)
|
|
||||||
if fp:
|
|
||||||
patch_image(fp)
|
|
||||||
image = Image(filename=fp.split("/")[-1], user=current_user)
|
|
||||||
session.add(image)
|
|
||||||
session.flush()
|
|
||||||
session.refresh(image)
|
|
||||||
new_place.image_id = image.id
|
|
||||||
else:
|
|
||||||
image_bytes = b64img_decode(place.image)
|
image_bytes = b64img_decode(place.image)
|
||||||
filename = save_image_to_file(image_bytes, settings.PLACE_IMAGE_SIZE)
|
filename = save_image_to_file(image_bytes, settings.PLACE_IMAGE_SIZE)
|
||||||
if not filename:
|
if not filename:
|
||||||
raise HTTPException(status_code=400, detail="Bad request")
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
image = Image(filename=filename, user=current_user)
|
image = Image(filename=filename, user=current_user)
|
||||||
session.add(image)
|
session.add(image)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -144,16 +134,6 @@ async def google_search_text(
|
|||||||
allowdog=place.get("allowDogs"),
|
allowdog=place.get("allowDogs"),
|
||||||
description=compute_description(place),
|
description=compute_description(place),
|
||||||
)
|
)
|
||||||
if place.get("photos"):
|
|
||||||
photo_name = place.get("photos")[0].get("name")
|
|
||||||
if photo_name:
|
|
||||||
result.image = await gmaps_photo(photo_name, db_user.google_apikey)
|
|
||||||
# FIXME: Using default categories, update Settings/Categories to integrate types mapping
|
|
||||||
place_types = set(place.get("types", []))
|
|
||||||
for key, v in gmaps_types_mapper.items():
|
|
||||||
if any(any(substring in place_type for place_type in place_types) for substring in v):
|
|
||||||
result.category = key
|
|
||||||
break
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@ -3,16 +3,6 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
gmaps_types_mapper: dict[str, list] = {
|
|
||||||
"Nature & Outdoor": ["natural_feature", "landmark"],
|
|
||||||
"Entertainment & Leisure": ["amusement", "aquarium"],
|
|
||||||
"Culture": ["museum", "historical", "art_", "church"],
|
|
||||||
"Food & Drink": ["food", "bar", "bakery", "coffee_shop", "restaurant"],
|
|
||||||
"Adventure & Sports": ["adventure_sports_center"],
|
|
||||||
"Wellness": ["wellness"],
|
|
||||||
"Accommodation": ["hotel", "camping"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def compute_avg_price(price_range: dict | None) -> float | None:
|
def compute_avg_price(price_range: dict | None) -> float | None:
|
||||||
if not price_range:
|
if not price_range:
|
||||||
@ -52,7 +42,7 @@ async def gmaps_textsearch(search: str, api_key: str) -> list[dict[str, Any]]:
|
|||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Goog-Api-Key": api_key,
|
"X-Goog-Api-Key": api_key,
|
||||||
"X-Goog-FieldMask": "places.id,places.types,places.location,places.priceRange,places.formattedAddress,places.websiteUri,places.internationalPhoneNumber,places.displayName,places.allowsDogs,places.photos",
|
"X-Goog-FieldMask": "places.id,places.types,places.location,places.priceRange,places.formattedAddress,places.websiteUri,places.internationalPhoneNumber,places.displayName,places.allowsDogs",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -117,7 +117,7 @@ async def download_file(link: str, raise_on_error: bool = False) -> str:
|
|||||||
path = assets_folder_path() / generate_filename(infer_extension)
|
path = assets_folder_path() / generate_filename(infer_extension)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
return str(path)
|
return f.name
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise HTTPException(status_code=400, detail=f"Failed to download file: {e}")
|
raise HTTPException(status_code=400, detail=f"Failed to download file: {e}")
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"/api": {
|
|
||||||
"target": "http://localhost:8000",
|
|
||||||
"secure": false,
|
|
||||||
"changeOrigin": true,
|
|
||||||
"logLevel": "warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2026,15 +2026,10 @@ export class TripComponent implements AfterViewInit {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.trip!.attachments = this.trip?.attachments?.filter((att) => att.id != attachmentId);
|
this.trip!.attachments = this.trip?.attachments?.filter((att) => att.id != attachmentId);
|
||||||
let modifiedItem = false;
|
if (this.selectedItem?.attachments?.length) {
|
||||||
this.trip?.days.forEach(d => d.items.forEach(i => {
|
if (this.selectedItem.attachments.some((a) => a.id == attachmentId))
|
||||||
if (i.attachments?.length) {
|
this.selectedItem.attachments = this.selectedItem.attachments.filter((a) => a.id != attachmentId);
|
||||||
i.attachments = i.attachments.filter(attachment => attachment.id != attachmentId);
|
|
||||||
modifiedItem = true;
|
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
if (this.selectedItem?.attachments?.length) this.selectedItem.attachments = this.selectedItem.attachments.filter((a) => a.id != attachmentId);
|
|
||||||
if (modifiedItem) this.flattenTripDayItems();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,20 +2,12 @@
|
|||||||
<div pFocusTrap class="grid grid-cols-2 md:grid-cols-4 gap-4" [formGroup]="placeForm">
|
<div pFocusTrap class="grid grid-cols-2 md:grid-cols-4 gap-4" [formGroup]="placeForm">
|
||||||
<div class="relative col-span-2">
|
<div class="relative col-span-2">
|
||||||
<p-floatlabel variant="in">
|
<p-floatlabel variant="in">
|
||||||
<input id="name" formControlName="name" pInputText autofocus fluid (keydown.shift.enter)="gmapsSearchText()"
|
<input id="name" formControlName="name" pInputText autofocus fluid />
|
||||||
(keydown.control.enter)="closeDialog()" />
|
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
</p-floatlabel>
|
</p-floatlabel>
|
||||||
<div class="absolute right-2 top-1/2 -translate-y-1/2">
|
<p-button icon="pi pi-sparkles" variant="text" [disabled]="!placeForm.get('name')!.value"
|
||||||
@if (gmapsLoading) {
|
class="absolute right-2 top-1/2 -translate-y-1/2" pTooltip="Complete using GMaps API"
|
||||||
<svg viewBox="25 25 50 50">
|
|
||||||
<circle r="20" cy="50" cx="50"></circle>
|
|
||||||
</svg>
|
|
||||||
} @else {
|
|
||||||
<p-button icon="pi pi-sparkles" variant="text" [disabled]="!placeForm.get('name')!.value" pTooltip="Complete using GMaps API"
|
|
||||||
(click)="gmapsSearchText()" />
|
(click)="gmapsSearchText()" />
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p-floatlabel variant="in">
|
<p-floatlabel variant="in">
|
||||||
|
|||||||
@ -14,40 +14,3 @@
|
|||||||
.p-floatlabel:has(.p-inputwrapper-focus) input::placeholder {
|
.p-floatlabel:has(.p-inputwrapper-focus) input::placeholder {
|
||||||
color: var(--p-inputtext-placeholder-color) !important;
|
color: var(--p-inputtext-placeholder-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 2rem;
|
|
||||||
animation: loading-rotate 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
circle {
|
|
||||||
fill: none;
|
|
||||||
stroke: #4f46e5;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-dasharray: 1, 200;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
stroke-linecap: round;
|
|
||||||
animation: loading-dash 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading-rotate {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading-dash {
|
|
||||||
0% {
|
|
||||||
stroke-dasharray: 1, 200;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
stroke-dasharray: 90, 200;
|
|
||||||
stroke-dashoffset: -35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: -125px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -47,7 +47,6 @@ export class PlaceCreateModalComponent {
|
|||||||
categories$?: Observable<Category[]>;
|
categories$?: Observable<Category[]>;
|
||||||
previous_image_id: number | null = null;
|
previous_image_id: number | null = null;
|
||||||
previous_image: string | null = null;
|
previous_image: string | null = null;
|
||||||
gmapsLoading = false;
|
|
||||||
|
|
||||||
placeInputTooltip: string =
|
placeInputTooltip: string =
|
||||||
"<div class='text-center'>You can paste a Google Maps Place link to fill <i>Name</i>, <i>Place</i>, <i>Lat</i>, <i>Lng</i>.</div>\n<div class='text-sm text-center'>https://google.com/maps/place/XXX</div>\n<div class='text-xs text-center'>Either « click » on a point of interest or « search » for it (eg: British Museum) and copy the URL</div>";
|
"<div class='text-center'>You can paste a Google Maps Place link to fill <i>Name</i>, <i>Place</i>, <i>Lat</i>, <i>Lng</i>.</div>\n<div class='text-sm text-center'>https://google.com/maps/place/XXX</div>\n<div class='text-xs text-center'>Either « click » on a point of interest or « search » for it (eg: British Museum) and copy the URL</div>";
|
||||||
@ -207,35 +206,15 @@ export class PlaceCreateModalComponent {
|
|||||||
this.placeForm.get('gpx')?.markAsDirty();
|
this.placeForm.get('gpx')?.markAsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
gmapsToForm(r: GooglePlaceResult) {
|
|
||||||
this.placeForm.patchValue({ ...r, lat: formatLatLng(r.lat), lng: formatLatLng(r.lng), place: r.name || '' });
|
|
||||||
this.placeForm.get('category')?.markAsDirty();
|
|
||||||
this.gmapsLoading = false;
|
|
||||||
if (r.category) {
|
|
||||||
this.categories$?.pipe(take(1)).subscribe({
|
|
||||||
next: categories => {
|
|
||||||
const category: Category | undefined = categories.find(c => c.name == r.category);
|
|
||||||
if (!category) return;
|
|
||||||
this.placeForm.get('category')?.setValue(category.id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gmapsSearchText() {
|
gmapsSearchText() {
|
||||||
this.gmapsLoading = true;
|
|
||||||
const query = this.placeForm.get('name')?.value;
|
const query = this.placeForm.get('name')?.value;
|
||||||
if (!query) return;
|
if (!query) return;
|
||||||
this.apiService.gmapsSearchText(query).subscribe({
|
this.apiService.gmapsSearchText(query).subscribe({
|
||||||
next: (results) => {
|
next: (results) => {
|
||||||
if (!results.length) {
|
|
||||||
this.utilsService.toast('warn', 'No result', 'No result available for this autocompletion');
|
|
||||||
this.gmapsLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.length == 1) {
|
if (results.length == 1) {
|
||||||
this.gmapsToForm(results[0])
|
const r = results[0];
|
||||||
|
this.placeForm.patchValue({ ...r, lat: formatLatLng(r.lat), lng: formatLatLng(r.lng), place: r.name || '' });
|
||||||
|
this.placeForm.get('category')?.markAsDirty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,11 +234,15 @@ export class PlaceCreateModalComponent {
|
|||||||
|
|
||||||
modal.onClose.pipe(take(1)).subscribe({
|
modal.onClose.pipe(take(1)).subscribe({
|
||||||
next: (result: GooglePlaceResult | null) => {
|
next: (result: GooglePlaceResult | null) => {
|
||||||
if (!result) {
|
if (!result) return;
|
||||||
this.gmapsLoading = false;
|
const r = result;
|
||||||
return;
|
this.placeForm.patchValue({
|
||||||
}
|
...r,
|
||||||
this.gmapsToForm(result);
|
lat: formatLatLng(r.lat),
|
||||||
|
lng: formatLatLng(r.lng),
|
||||||
|
place: r.name || '',
|
||||||
|
});
|
||||||
|
this.placeForm.get('category')?.markAsDirty();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -54,12 +54,7 @@ export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Obs
|
|||||||
const errDetails = ERROR_CONFIG[err.status];
|
const errDetails = ERROR_CONFIG[err.status];
|
||||||
if (errDetails) {
|
if (errDetails) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
let msg = ""
|
return showAndThrowError(errDetails.title, `${err.error?.detail || err.message || errDetails.detail}`);
|
||||||
msg = err.message || errDetails.detail;
|
|
||||||
if (!Array.isArray(err.error?.detail)) {
|
|
||||||
msg = err.error.detail;
|
|
||||||
}
|
|
||||||
return showAndThrowError(errDetails.title, msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.status == 401 && authService.accessToken) {
|
if (err.status == 401 && authService.accessToken) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |