-
-
-
-
+
+ {#if selectedLocation.photos && selectedLocation.photos.length > 0}
+

+ {/if}
{$t('adventures.location_selected')}
-
{selectedLocation.name}
+
{selectedLocation.name}
+
{selectedLocation.location}
+ {#if selectedLocation.rating}
+
+
+ {selectedLocation.rating}
+ {#if selectedLocation.review_count}
+ ({selectedLocation.review_count} reviews)
+ {/if}
+
+ {/if}
+ {#if isEnrichingDescription}
+
+
+ Improving description quality...
+
+ {/if}
{selectedMarker.lat.toFixed(6)}, {selectedMarker.lng.toFixed(6)}
- {#if selectedLocation.category}
-
- {selectedLocation.category} • {selectedLocation.type || 'location'}
-
- {/if}
-
-
- {#if locationData?.city || locationData?.region || locationData?.country}
-
- {#if locationData.city}
-
- 🏙️ {locationData.city.name}
-
- {/if}
- {#if locationData.region}
-
- 🗺️ {locationData.region.name}
-
- {/if}
- {#if locationData.country}
-
- 🌎 {locationData.country.name}
-
- {/if}
+ {#if selectedLocation.types && selectedLocation.types.length > 0}
+
+ {#each selectedLocation.types.slice(0, 5) as typeName}
+ {typeName}
+ {/each}
{/if}
-
- {#if locationData?.display_name}
-
- {locationData.display_name}
-
- {/if}
+
+ {#if googleEnabled}
+
+ {/if}
{/if}
-
-
-
diff --git a/frontend/src/lib/components/shared/LocationSearchMap.svelte b/frontend/src/lib/components/shared/LocationSearchMap.svelte
index a5c8b500..9dbb6515 100644
--- a/frontend/src/lib/components/shared/LocationSearchMap.svelte
+++ b/frontend/src/lib/components/shared/LocationSearchMap.svelte
@@ -72,6 +72,10 @@
let initialTransportationApplied = false;
let isInitializing = false;
+ function isFiniteCoordinatePair(lat: unknown, lng: unknown): boolean {
+ return Number.isFinite(Number(lat)) && Number.isFinite(Number(lng));
+ }
+
// Track any provided codes (airport / station / etc)
let startCode: string | null = null;
let endCode: string | null = null;
@@ -572,7 +576,25 @@
endLocationData = metaData;
} else {
locationData = metaData;
- displayName = data.display_name;
+ const resolvedLocationName = (data.location_name || '').trim();
+ const resolvedDisplayName = (data.display_name || '').trim();
+
+ if (selectedLocation) {
+ const isCoordinatePlaceholder = selectedLocation.name.startsWith('Location at ');
+ selectedLocation = {
+ ...selectedLocation,
+ name:
+ resolvedLocationName ||
+ (isCoordinatePlaceholder && resolvedDisplayName
+ ? resolvedDisplayName
+ : selectedLocation.name),
+ location: resolvedDisplayName || selectedLocation.location
+ };
+ emitUpdate(selectedLocation);
+ }
+
+ displayName = resolvedDisplayName || resolvedLocationName || displayName;
+ searchQuery = selectedLocation?.name || searchQuery;
}
} else {
if (target === 'start') {
@@ -641,7 +663,11 @@
dispatch('clear');
}
- $: if (!initialApplied && initialSelection) {
+ $: if (
+ !initialApplied &&
+ initialSelection &&
+ isFiniteCoordinatePair(initialSelection.lat, initialSelection.lng)
+ ) {
initialApplied = true;
applyInitialSelection(initialSelection);
}
diff --git a/frontend/src/lib/location-save.ts b/frontend/src/lib/location-save.ts
new file mode 100644
index 00000000..07a3b4f1
--- /dev/null
+++ b/frontend/src/lib/location-save.ts
@@ -0,0 +1,92 @@
+import { DEFAULT_CURRENCY, normalizeMoneyPayload } from '$lib/money';
+import type { Location } from '$lib/types';
+
+type SaveLocationInput = {
+ location: Partial
;
+ locationToEdit?: { id: string } | null;
+ collectionId?: string | null;
+ defaultCurrency?: string;
+};
+
+function toFixedCoordinate(value: unknown): number | null {
+ if (value === null || value === undefined) return null;
+ const parsed = typeof value === 'string' ? Number(value) : Number(value);
+ if (Number.isNaN(parsed)) return null;
+ return Number(parsed.toFixed(6));
+}
+
+function sanitizeLink(value: unknown): string | null {
+ if (!value || typeof value !== 'string' || !value.trim()) {
+ return null;
+ }
+
+ try {
+ new URL(value);
+ return value;
+ } catch {
+ return null;
+ }
+}
+
+function parseApiError(errorData: any): string {
+ let errorMsg = errorData?.detail || errorData?.name?.[0] || '';
+ if (errorMsg) return String(errorMsg);
+
+ const fieldErrors = Object.entries(errorData || {})
+ .filter(([_, value]) => Array.isArray(value))
+ .map(([key, value]) => `${key}: ${(value as string[]).join(', ')}`)
+ .join('; ');
+
+ return fieldErrors || 'Failed to save location';
+}
+
+export async function saveLocation({
+ location,
+ locationToEdit = null,
+ collectionId = null,
+ defaultCurrency = DEFAULT_CURRENCY
+}: SaveLocationInput): Promise {
+ const payload: Record = {
+ ...location,
+ latitude: toFixedCoordinate(location.latitude),
+ longitude: toFixedCoordinate(location.longitude),
+ link: sanitizeLink(location.link),
+ description:
+ typeof location.description === 'string' && location.description.trim()
+ ? location.description
+ : null
+ };
+
+ if (collectionId) {
+ payload.collections = [collectionId];
+ }
+
+ if (location.price === null || location.price === undefined) {
+ payload.price = null;
+ payload.price_currency = null;
+ } else {
+ const normalized = normalizeMoneyPayload(payload, 'price', 'price_currency', defaultCurrency);
+ payload.price = normalized.price;
+ payload.price_currency = normalized.price_currency;
+ }
+
+ const isUpdate = Boolean(locationToEdit?.id);
+ if (isUpdate && !collectionId) {
+ delete payload.collections;
+ }
+
+ const res = await fetch(isUpdate ? `/api/locations/${locationToEdit?.id}` : '/api/locations', {
+ method: isUpdate ? 'PATCH' : 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(parseApiError(errorData));
+ }
+
+ return res.json();
+}
diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json
index d0e9e6e1..0783c14e 100644
--- a/frontend/src/locales/ja.json
+++ b/frontend/src/locales/ja.json
@@ -640,8 +640,7 @@
"locations": {
"location": "位置",
"locations": "場所",
- "my_locations": "私の場所",
- "best_happened_at": "最高の出来事が起きた"
+ "my_locations": "私の場所"
},
"lodging": {
"apartment": "アパート",
@@ -712,8 +711,7 @@
},
"users": "ユーザー",
"navigation": "ナビゲーション",
- "worldtravel": "世界旅行",
- "mobile_login": "モバイルログイン"
+ "worldtravel": "世界旅行"
},
"notes": {
"content": "コンテンツ",
@@ -1141,29 +1139,5 @@
"trip_context_info": "旅行コンテキスト項目は、旅行全体に適用されます。たとえば、目的地そのものである場所、一般的なメモ、旅行全体にとって重要な持ち物リストなどです。",
"unscheduled_items": "予定外の項目",
"unscheduled_items_desc": "これらのアイテムはこの旅行にリンクされていますが、まだ特定の日に追加されていません。"
- },
- "api_keys": {
- "copied": "コピーしました!",
- "copy": "キーをコピーする",
- "create": "キーの作成",
- "create_error": "APIキーの作成に失敗しました。",
- "created": "作成されました",
- "description": "プログラムによるアクセスのための個人用 API キーを作成します。\nキーは作成時に 1 回だけ表示されます。",
- "dismiss": "却下する",
- "key_created": "API キーが正常に作成されました。",
- "key_name_placeholder": "キー名 (例: ホームアシスタント)",
- "key_revoked": "API キーが取り消されました。",
- "last_used": "最後に使用した",
- "never_used": "一度も使用されていない",
- "new_key_title": "新しい API キーを保存します",
- "new_key_warning": "このキーは再度表示されません。\nそれをコピーして安全な場所に保管してください。",
- "no_keys": "API キーはまだありません。",
- "revoke": "取り消す",
- "revoke_error": "APIキーの取り消しに失敗しました。",
- "title": "APIキー",
- "copy_error": "キーのコピー中にエラーが発生しました。",
- "usage_middle": "ヘッダーまたはとして",
- "usage_prefix": "このキーを",
- "delete_confirm": "このモバイル API キーを削除してもよろしいですか?"
}
}
diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json
index d6f31032..f02f5126 100644
--- a/frontend/src/locales/ko.json
+++ b/frontend/src/locales/ko.json
@@ -45,7 +45,7 @@
"copied_to_clipboard": "클립 보드에 복사됨!",
"copy_failed": "복사 실패",
"copy_link": "링크 복사",
- "create_new": "새로 만들기",
+ "create_new": "새로 만들기...",
"date": "일자",
"date_constrain": "컬렉션 일자로 제한",
"date_information": "일자 정보",
@@ -652,8 +652,7 @@
"users": "사용자",
"admin_panel": "관리자 패널",
"navigation": "항해",
- "worldtravel": "세계여행",
- "mobile_login": "모바일 로그인"
+ "worldtravel": "세계여행"
},
"notes": {
"content": "콘텐츠",
@@ -1032,8 +1031,7 @@
"locations": {
"location": "위치",
"locations": "위치",
- "my_locations": "내 위치",
- "best_happened_at": "가장 좋은 일이 일어난 시간은 다음과 같습니다."
+ "my_locations": "내 위치"
},
"settings_download_backup": "백업을 다운로드하십시오",
"invites": {
@@ -1141,29 +1139,5 @@
"trip_context_info": "여행 컨텍스트 항목은 전체 여행에 적용됩니다. 예를 들어 목적지 자체인 위치, 일반 참고 사항, 전체 여행에 중요한 짐 목록 등이 있습니다.",
"unscheduled_items": "예정되지 않은 품목",
"unscheduled_items_desc": "이 항목은 이 여행에 연결되어 있지만 아직 특정 날짜에 추가되지 않았습니다."
- },
- "api_keys": {
- "copied": "복사되었습니다!",
- "copy": "키 복사",
- "create": "키 생성",
- "create_error": "API 키를 생성하지 못했습니다.",
- "created": "생성됨",
- "description": "프로그래밍 방식 액세스를 위한 개인 API 키를 만듭니다. \n키는 생성 시 한 번만 표시됩니다.",
- "dismiss": "해고하다",
- "key_created": "API 키가 생성되었습니다.",
- "key_name_placeholder": "키 이름(예: 홈어시스턴트)",
- "key_revoked": "API 키가 취소되었습니다.",
- "last_used": "마지막으로 사용됨",
- "never_used": "한번도 사용하지 않음",
- "new_key_title": "새 API 키 저장",
- "new_key_warning": "이 키는 다시 표시되지 않습니다. \n복사해서 안전한 곳에 보관하세요.",
- "no_keys": "아직 API 키가 없습니다.",
- "revoke": "취소",
- "revoke_error": "API 키를 취소하지 못했습니다.",
- "title": "API 키",
- "copy_error": "키를 복사하는 중에 오류가 발생했습니다.",
- "usage_middle": "헤더 또는 다음과 같이",
- "usage_prefix": "다음에서 이 키를 사용하세요.",
- "delete_confirm": "이 모바일 API 키를 삭제하시겠습니까?"
}
}
diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json
index e073642d..faabbe8d 100644
--- a/frontend/src/locales/nl.json
+++ b/frontend/src/locales/nl.json
@@ -492,8 +492,7 @@
"calendar": "Kalender",
"admin_panel": "Admin -paneel",
"navigation": "Navigatie",
- "worldtravel": "Wereldreizen",
- "mobile_login": "Mobiel inloggen"
+ "worldtravel": "Wereldreizen"
},
"auth": {
"confirm_password": "Bevestig wachtwoord",
@@ -1032,8 +1031,7 @@
"locations": {
"location": "Locatie",
"locations": "Locaties",
- "my_locations": "Mijn locaties",
- "best_happened_at": "Het beste gebeurde om"
+ "my_locations": "Mijn locaties"
},
"settings_download_backup": "Download back -up",
"invites": {
@@ -1141,29 +1139,5 @@
"trip_context_info": "Reiscontextitems zijn van toepassing op de hele reis, bijvoorbeeld locaties die de bestemming zelf vormen, algemene opmerkingen of paklijsten die belangrijk zijn voor de hele reis.",
"unscheduled_items": "Niet-geplande items",
"unscheduled_items_desc": "Deze items zijn gekoppeld aan deze reis, maar nog niet toegevoegd aan een specifieke dag."
- },
- "api_keys": {
- "copied": "Gekopieerd!",
- "copy": "Kopieer sleutel",
- "create": "Sleutel maken",
- "create_error": "Kan API-sleutel niet maken.",
- "created": "Gemaakt",
- "description": "Maak persoonlijke API-sleutels voor programmatische toegang. \nSleutels worden slechts één keer weergegeven tijdens het maken.",
- "dismiss": "Afwijzen",
- "key_created": "API-sleutel is succesvol aangemaakt.",
- "key_name_placeholder": "Sleutelnaam (bijv. Home Assistant)",
- "key_revoked": "API-sleutel ingetrokken.",
- "last_used": "Laatst gebruikt",
- "never_used": "Nooit gebruikt",
- "new_key_title": "Sla uw nieuwe API-sleutel op",
- "new_key_warning": "Deze sleutel wordt niet meer getoond. \nKopieer het en bewaar het op een veilige plek.",
- "no_keys": "Nog geen API-sleutels.",
- "revoke": "Herroepen",
- "revoke_error": "Kan de API-sleutel niet intrekken.",
- "title": "API-sleutels",
- "copy_error": "Fout bij kopiëren van sleutel.",
- "usage_middle": "koptekst of als",
- "usage_prefix": "Gebruik deze sleutel in de",
- "delete_confirm": "Weet u zeker dat u deze mobiele API-sleutel wilt verwijderen?"
}
}
diff --git a/frontend/src/locales/no.json b/frontend/src/locales/no.json
index ef9d48fa..dc0ce58d 100644
--- a/frontend/src/locales/no.json
+++ b/frontend/src/locales/no.json
@@ -28,8 +28,7 @@
"northernLights": "Nordlys"
},
"navigation": "Navigasjon",
- "worldtravel": "Verdensreise",
- "mobile_login": "Mobil pålogging"
+ "worldtravel": "Verdensreise"
},
"about": {
"about": "Om",
@@ -1032,8 +1031,7 @@
"locations": {
"location": "Sted",
"locations": "Lokasjoner",
- "my_locations": "Mine lokasjoner",
- "best_happened_at": "Best skjedde kl"
+ "my_locations": "Mine lokasjoner"
},
"settings_download_backup": "Last ned sikkerhetskopi",
"invites": {
@@ -1141,29 +1139,5 @@
"trip_context_info": "Turkontekstelementer gjelder for hele turen – for eksempel steder som er selve destinasjonen, generelle notater eller pakkelister som er viktige for hele turen.",
"unscheduled_items": "Ikke-planlagte elementer",
"unscheduled_items_desc": "Disse elementene er knyttet til denne turen, men har ikke blitt lagt til en bestemt dag ennå."
- },
- "api_keys": {
- "copied": "Kopiert!",
- "copy": "Kopier nøkkel",
- "create": "Opprett nøkkel",
- "create_error": "Kunne ikke opprette API-nøkkel.",
- "created": "Opprettet",
- "description": "Lag personlige API-nøkler for programmatisk tilgang. \nNøkler vises bare én gang ved opprettelsestidspunktet.",
- "dismiss": "Avskjedige",
- "key_created": "API-nøkkel opprettet.",
- "key_name_placeholder": "Nøkkelnavn (f.eks. Home Assistant)",
- "key_revoked": "API-nøkkel er opphevet.",
- "last_used": "Sist brukt",
- "never_used": "Aldri brukt",
- "new_key_title": "Lagre din nye API-nøkkel",
- "new_key_warning": "Denne nøkkelen vises ikke igjen. \nKopier den og oppbevar den et trygt sted.",
- "no_keys": "Ingen API-nøkler ennå.",
- "revoke": "Oppheve",
- "revoke_error": "Kunne ikke tilbakekalle API-nøkkel.",
- "title": "API-nøkler",
- "copy_error": "Feil ved kopiering av nøkkel.",
- "usage_middle": "header eller as",
- "usage_prefix": "Bruk denne tasten i",
- "delete_confirm": "Er du sikker på at du vil slette denne mobile API-nøkkelen?"
}
}
diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json
index 7c43c74b..5bf699c2 100644
--- a/frontend/src/locales/pl.json
+++ b/frontend/src/locales/pl.json
@@ -28,8 +28,7 @@
"calendar": "Kalendarz",
"admin_panel": "Panel administracyjny",
"navigation": "Nawigacja",
- "worldtravel": "Światowa podróż",
- "mobile_login": "Logowanie mobilne"
+ "worldtravel": "Światowa podróż"
},
"about": {
"about": "O aplikacji",
@@ -1032,8 +1031,7 @@
"locations": {
"location": "Lokalizacja",
"locations": "Lokalizacje",
- "my_locations": "Moje lokalizacje",
- "best_happened_at": "Najlepsze wydarzyło się o godz"
+ "my_locations": "Moje lokalizacje"
},
"settings_download_backup": "Pobierz kopię zapasową",
"invites": {
@@ -1141,29 +1139,5 @@
"trip_context_info": "Elementy kontekstu podróży dotyczą całej podróży — na przykład lokalizacje będące samym celem podróży, uwagi ogólne lub listy rzeczy do spakowania ważne dla całej podróży.",
"unscheduled_items": "Niezaplanowane pozycje",
"unscheduled_items_desc": "Te elementy są powiązane z tą podróżą, ale nie zostały jeszcze dodane do konkretnego dnia."
- },
- "api_keys": {
- "copied": "Skopiowano!",
- "copy": "Skopiuj klucz",
- "create": "Utwórz klucz",
- "create_error": "Nie udało się utworzyć klucza API.",
- "created": "Stworzony",
- "description": "Twórz osobiste klucze API w celu uzyskania dostępu programowego. \nKlucze są wyświetlane tylko raz w momencie tworzenia.",
- "dismiss": "Odrzucać",
- "key_created": "Klucz API został utworzony pomyślnie.",
- "key_name_placeholder": "Nazwa klucza (np. Asystent domowy)",
- "key_revoked": "Klucz API unieważniony.",
- "last_used": "Ostatnio używany",
- "never_used": "Nigdy nie używany",
- "new_key_title": "Zapisz swój nowy klucz API",
- "new_key_warning": "Ten klucz nie będzie już więcej wyświetlany. \nSkopiuj go i przechowuj w bezpiecznym miejscu.",
- "no_keys": "Nie ma jeszcze kluczy API.",
- "revoke": "Unieważnić",
- "revoke_error": "Nie udało się unieważnić klucza API.",
- "title": "Klucze API",
- "copy_error": "Błąd podczas kopiowania klucza.",
- "usage_middle": "nagłówek lub jako",
- "usage_prefix": "Użyj tego klawisza w",
- "delete_confirm": "Czy na pewno chcesz usunąć ten klucz mobilnego API?"
}
}