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)
|
||||
from ..security import verify_exists_and_owns
|
||||
from ..utils.gmaps import (compute_avg_price, compute_description,
|
||||
gmaps_get_boundaries, gmaps_photo, gmaps_textsearch,
|
||||
gmaps_types_mapper)
|
||||
gmaps_get_boundaries, gmaps_textsearch)
|
||||
from ..utils.utils import (b64img_decode, download_file, patch_image,
|
||||
save_image_to_file)
|
||||
|
||||
@ -32,7 +31,7 @@ def read_places(
|
||||
|
||||
|
||||
@router.post("", response_model=PlaceRead)
|
||||
async def create_place(
|
||||
def create_place(
|
||||
place: PlaceCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)]
|
||||
) -> PlaceRead:
|
||||
new_place = Place(
|
||||
@ -51,25 +50,16 @@ async def create_place(
|
||||
)
|
||||
|
||||
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)
|
||||
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_place.image_id = image.id
|
||||
image_bytes = b64img_decode(place.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_place.image_id = image.id
|
||||
|
||||
session.add(new_place)
|
||||
session.commit()
|
||||
@ -144,16 +134,6 @@ async def google_search_text(
|
||||
allowdog=place.get("allowDogs"),
|
||||
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)
|
||||
|
||||
return results
|
||||
|
||||
@ -3,16 +3,6 @@ from typing import Any
|
||||
import httpx
|
||||
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:
|
||||
if not price_range:
|
||||
@ -52,7 +42,7 @@ async def gmaps_textsearch(search: str, api_key: str) -> list[dict[str, Any]]:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"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:
|
||||
|
||||
@ -117,7 +117,7 @@ async def download_file(link: str, raise_on_error: bool = False) -> str:
|
||||
path = assets_folder_path() / generate_filename(infer_extension)
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.content)
|
||||
return str(path)
|
||||
return f.name
|
||||
except Exception as e:
|
||||
if raise_on_error:
|
||||
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({
|
||||
next: () => {
|
||||
this.trip!.attachments = this.trip?.attachments?.filter((att) => att.id != attachmentId);
|
||||
let modifiedItem = false;
|
||||
this.trip?.days.forEach(d => d.items.forEach(i => {
|
||||
if (i.attachments?.length) {
|
||||
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();
|
||||
if (this.selectedItem?.attachments?.length) {
|
||||
if (this.selectedItem.attachments.some((a) => a.id == attachmentId))
|
||||
this.selectedItem.attachments = this.selectedItem.attachments.filter((a) => a.id != attachmentId);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,20 +2,12 @@
|
||||
<div pFocusTrap class="grid grid-cols-2 md:grid-cols-4 gap-4" [formGroup]="placeForm">
|
||||
<div class="relative col-span-2">
|
||||
<p-floatlabel variant="in">
|
||||
<input id="name" formControlName="name" pInputText autofocus fluid (keydown.shift.enter)="gmapsSearchText()"
|
||||
(keydown.control.enter)="closeDialog()" />
|
||||
<input id="name" formControlName="name" pInputText autofocus fluid />
|
||||
<label for="name">Name</label>
|
||||
</p-floatlabel>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
@if (gmapsLoading) {
|
||||
<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()" />
|
||||
}
|
||||
</div>
|
||||
<p-button icon="pi pi-sparkles" variant="text" [disabled]="!placeForm.get('name')!.value"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2" pTooltip="Complete using GMaps API"
|
||||
(click)="gmapsSearchText()" />
|
||||
</div>
|
||||
|
||||
<p-floatlabel variant="in">
|
||||
|
||||
@ -14,40 +14,3 @@
|
||||
.p-floatlabel:has(.p-inputwrapper-focus) input::placeholder {
|
||||
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[]>;
|
||||
previous_image_id: number | null = null;
|
||||
previous_image: string | null = null;
|
||||
gmapsLoading = false;
|
||||
|
||||
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>";
|
||||
@ -207,35 +206,15 @@ export class PlaceCreateModalComponent {
|
||||
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() {
|
||||
this.gmapsLoading = true;
|
||||
const query = this.placeForm.get('name')?.value;
|
||||
if (!query) return;
|
||||
this.apiService.gmapsSearchText(query).subscribe({
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -255,11 +234,15 @@ export class PlaceCreateModalComponent {
|
||||
|
||||
modal.onClose.pipe(take(1)).subscribe({
|
||||
next: (result: GooglePlaceResult | null) => {
|
||||
if (!result) {
|
||||
this.gmapsLoading = false;
|
||||
return;
|
||||
}
|
||||
this.gmapsToForm(result);
|
||||
if (!result) return;
|
||||
const r = result;
|
||||
this.placeForm.patchValue({
|
||||
...r,
|
||||
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];
|
||||
if (errDetails) {
|
||||
console.error(err);
|
||||
let msg = ""
|
||||
msg = err.message || errDetails.detail;
|
||||
if (!Array.isArray(err.error?.detail)) {
|
||||
msg = err.error.detail;
|
||||
}
|
||||
return showAndThrowError(errDetails.title, msg);
|
||||
return showAndThrowError(errDetails.title, `${err.error?.detail || err.message || errDetails.detail}`);
|
||||
}
|
||||
|
||||
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 |