From 1a51f8e3e1ebef3fac1b05d3ad37d5c794ca340f Mon Sep 17 00:00:00 2001 From: Ben Haas Date: Mon, 13 Apr 2026 08:28:34 -0700 Subject: [PATCH] =?UTF-8?q?Add=20translations=20for=20"Loading=20place=20d?= =?UTF-8?q?etails=E2=80=A6"=20and=20improve=20place=20search=20functionali?= =?UTF-8?q?ty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate a loading spinner for "Name" input field during place search. - Enhance OpenStreetMap place detail retrieval with Nominatim lookup. - Update `authStore` to track Google Maps API key presence. --- .../src/components/Planner/PlaceFormModal.tsx | 26 +++++++---- client/src/i18n/translations/ar.ts | 1 + client/src/i18n/translations/br.ts | 1 + client/src/i18n/translations/cs.ts | 1 + client/src/i18n/translations/de.ts | 1 + client/src/i18n/translations/en.ts | 1 + client/src/i18n/translations/es.ts | 1 + client/src/i18n/translations/fr.ts | 1 + client/src/i18n/translations/hu.ts | 1 + client/src/i18n/translations/it.ts | 1 + client/src/i18n/translations/nl.ts | 1 + client/src/i18n/translations/pl.ts | 1 + client/src/i18n/translations/ru.ts | 1 + client/src/i18n/translations/zh.ts | 1 + client/src/i18n/translations/zhTw.ts | 1 + client/src/store/authStore.ts | 4 ++ server/src/services/mapsService.ts | 46 +++++++++++++++++-- 17 files changed, 78 insertions(+), 12 deletions(-) diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx index 83aed50d..6cd71f8d 100644 --- a/client/src/components/Planner/PlaceFormModal.tsx +++ b/client/src/components/Planner/PlaceFormModal.tsx @@ -6,7 +6,7 @@ import { useAuthStore } from '../../store/authStore' import { useCanDo } from '../../store/permissionsStore' import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' -import { Search, Paperclip, X, AlertTriangle } from 'lucide-react' +import { Search, Paperclip, X, AlertTriangle, Loader2 } from 'lucide-react' import { useTranslation } from '../../i18n' import CustomTimePicker from '../shared/CustomTimePicker' import type { Place, Category, Assignment } from '../../types' @@ -234,6 +234,7 @@ export default function PlaceFormModal({ setAcHighlight(-1) const previousSearch = mapsSearch setMapsSearch('') + setForm(prev => ({ ...prev, name: suggestion.mainText })) setIsSearchingMaps(true) try { const result = await mapsApi.details(suggestion.placeId, language) @@ -425,14 +426,21 @@ export default function PlaceFormModal({ {/* Name */}
- handleChange('name', e.target.value)} - required - placeholder={t('places.formNamePlaceholder')} - className="form-input" - /> +
+ handleChange('name', e.target.value)} + required + placeholder={t('places.formNamePlaceholder')} + className="form-input" + /> + {isSearchingMaps && ( +
+ +
+ )} +
{/* Description */} diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index cdcac563..5251b244 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -855,6 +855,7 @@ const ar: Record = { 'places.reservationNotesPlaceholder': 'ملاحظات الحجز، رقم التأكيد...', 'places.mapsSearchPlaceholder': 'ابحث عن أماكن...', 'places.mapsSearchError': 'فشل البحث عن المكان.', + 'places.loadingDetails': 'جارٍ تحميل تفاصيل المكان…', 'places.osmHint': 'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.', 'places.osmActive': 'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.', 'places.categoryCreateError': 'فشل إنشاء الفئة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index d3b11e66..40d24bfe 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -837,6 +837,7 @@ const br: Record = { 'places.reservationNotesPlaceholder': 'Notas da reserva, código de confirmação...', 'places.mapsSearchPlaceholder': 'Buscar lugares...', 'places.mapsSearchError': 'Falha na busca de lugares.', + 'places.loadingDetails': 'Carregando detalhes do lugar…', 'places.osmHint': 'Busca via OpenStreetMap (sem fotos, horários ou avaliações). Adicione uma chave Google nas configurações para detalhes completos.', 'places.osmActive': 'Busca via OpenStreetMap (sem fotos, avaliações ou horário de funcionamento). Adicione uma chave Google em Configurações para mais dados.', 'places.categoryCreateError': 'Falha ao criar categoria', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 130f9623..086de3bc 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -853,6 +853,7 @@ const cs: Record = { 'places.reservationNotesPlaceholder': 'Poznámky k rezervaci, potvrzovací kód...', 'places.mapsSearchPlaceholder': 'Hledat místa...', 'places.mapsSearchError': 'Hledání místa se nezdařilo.', + 'places.loadingDetails': 'Načítání podrobností místa…', 'places.osmHint': 'Používáte hledání přes OpenStreetMap (bez fotek a hodnocení). Pro plné detaily přidejte Google API klíč v nastavení.', 'places.osmActive': 'Hledání přes OpenStreetMap.', 'places.categoryCreateError': 'Nepodařilo se vytvořit kategorii', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index c9ebf453..811c7682 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -853,6 +853,7 @@ const de: Record = { 'places.reservationNotesPlaceholder': 'Reservierungsnotizen, Bestätigungsnummer...', 'places.mapsSearchPlaceholder': 'Ortssuche...', 'places.mapsSearchError': 'Ortssuche fehlgeschlagen.', + 'places.loadingDetails': 'Ortsdetails werden geladen…', 'places.osmHint': 'OpenStreetMap-Suche aktiv (ohne Bilder, Öffnungszeiten, Bewertungen). Für erweiterte Daten Google API Key in den Einstellungen hinterlegen.', 'places.osmActive': 'Suche via OpenStreetMap (ohne Bilder, Bewertungen & Öffnungszeiten). Google API Key in den Einstellungen hinterlegen für erweiterte Daten.', 'places.categoryCreateError': 'Fehler beim Erstellen der Kategorie', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 2cb895a1..ad1a4824 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -872,6 +872,7 @@ const en: Record = { 'places.reservationNotesPlaceholder': 'Reservation notes, confirmation number...', 'places.mapsSearchPlaceholder': 'Search places...', 'places.mapsSearchError': 'Place search failed.', + 'places.loadingDetails': 'Loading place details…', 'places.osmHint': 'Using OpenStreetMap search (no photos, opening hours, or ratings). Add a Google API key in settings for full details.', 'places.osmActive': 'Search via OpenStreetMap (no photos, ratings or opening hours). Add a Google API key in Settings for enhanced data.', 'places.categoryCreateError': 'Failed to create category', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 41219432..1ce404e7 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -828,6 +828,7 @@ const es: Record = { 'places.reservationNotesPlaceholder': 'Notas de reserva, número de confirmación...', 'places.mapsSearchPlaceholder': 'Buscar lugares...', 'places.mapsSearchError': 'La búsqueda de lugares falló.', + 'places.loadingDetails': 'Cargando detalles del lugar…', 'places.osmHint': 'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.', 'places.osmActive': 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.', 'places.categoryCreateError': 'No se pudo crear la categoría', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index cbc2e09c..c3825ab7 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -852,6 +852,7 @@ const fr: Record = { 'places.reservationNotesPlaceholder': 'Notes de réservation, numéro de confirmation…', 'places.mapsSearchPlaceholder': 'Rechercher des lieux…', 'places.mapsSearchError': 'La recherche de lieu a échoué.', + 'places.loadingDetails': 'Chargement des détails du lieu…', 'places.osmHint': 'Recherche via OpenStreetMap (pas de photos, horaires ni notes). Ajoutez une clé API Google dans les paramètres pour plus de détails.', 'places.osmActive': 'Recherche via OpenStreetMap (pas de photos, notes ni horaires). Ajoutez une clé API Google dans les paramètres pour des données enrichies.', 'places.categoryCreateError': 'Impossible de créer la catégorie', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 40ce49a2..3046d2eb 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -853,6 +853,7 @@ const hu: Record = { 'places.reservationNotesPlaceholder': 'Foglalási jegyzetek, visszaigazolási szám...', 'places.mapsSearchPlaceholder': 'Helyek keresése...', 'places.mapsSearchError': 'Helykeresés sikertelen.', + 'places.loadingDetails': 'Hely adatainak betöltése…', 'places.osmHint': 'OpenStreetMap keresés aktív (képek, nyitvatartás és értékelések nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.', 'places.osmActive': 'Keresés OpenStreetMap-en keresztül (képek, értékelések és nyitvatartás nélkül). Bővített adatokhoz add meg a Google API kulcsot a beállításokban.', 'places.categoryCreateError': 'Nem sikerült létrehozni a kategóriát', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index d5449ff8..60ffb247 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -853,6 +853,7 @@ const it: Record = { 'places.reservationNotesPlaceholder': 'Note della prenotazione, numero di conferma...', 'places.mapsSearchPlaceholder': 'Cerca luoghi...', 'places.mapsSearchError': 'Impossibile cercare i luoghi.', + 'places.loadingDetails': 'Caricamento dettagli del luogo…', 'places.osmHint': 'Uso della ricerca OpenStreetMap (senza foto, orari di apertura o valutazioni). Aggiungi una chiave API Google nelle impostazioni per i dettagli completi.', 'places.osmActive': 'Ricerca tramite OpenStreetMap (senza foto, valutazioni o orari di apertura). Aggiungi una chiave API Google nelle Impostazioni per dati avanzati.', 'places.categoryCreateError': 'Impossibile creare la categoria', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 2e7495da..50c174dc 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -852,6 +852,7 @@ const nl: Record = { 'places.reservationNotesPlaceholder': 'Reserveringsnotities, bevestigingsnummer...', 'places.mapsSearchPlaceholder': 'Plaatsen zoeken...', 'places.mapsSearchError': 'Zoeken naar plaatsen mislukt.', + 'places.loadingDetails': 'Plaatsgegevens laden…', 'places.osmHint': 'Zoeken via OpenStreetMap (geen foto\'s, openingstijden of beoordelingen). Voeg een Google API-sleutel toe in instellingen voor volledige details.', 'places.osmActive': 'Zoeken via OpenStreetMap (geen foto\'s, beoordelingen of openingstijden). Voeg een Google API-sleutel toe in Instellingen voor uitgebreide gegevens.', 'places.categoryCreateError': 'Categorie aanmaken mislukt', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 4f60b983..5870f5c9 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -811,6 +811,7 @@ const pl: Record = { 'places.reservationNotesPlaceholder': 'Notatki z rezerwacji, numer potwierdzenia...', 'places.mapsSearchPlaceholder': 'Szukaj miejsc...', 'places.mapsSearchError': 'Nie udało się wyszukać miejsca.', + 'places.loadingDetails': 'Ładowanie szczegółów miejsca…', 'places.osmHint': 'Korzystając z OpenStreetMap (brak zdjęć, godzin otwarcia czy ocen). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.', 'places.osmActive': 'Szukaj przez OpenStreetMap (brak zdjęć, ocen czy godzin otwarcia). Dodaj klucz API Google w ustawieniach aby uzyskać pełne dane.', 'places.categoryCreateError': 'Nie udało się utworzyć kategorii', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 18001fc9..a9624ad7 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -852,6 +852,7 @@ const ru: Record = { 'places.reservationNotesPlaceholder': 'Заметки о бронировании, номер подтверждения...', 'places.mapsSearchPlaceholder': 'Поиск мест...', 'places.mapsSearchError': 'Ошибка поиска мест.', + 'places.loadingDetails': 'Загрузка данных о месте…', 'places.osmHint': 'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.', 'places.osmActive': 'Поиск через OpenStreetMap (без фото, рейтингов и часов работы). Добавьте API-ключ Google в настройках для расширенных данных.', 'places.categoryCreateError': 'Не удалось создать категорию', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index d0af81d3..11326296 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -852,6 +852,7 @@ const zh: Record = { 'places.reservationNotesPlaceholder': '预订备注、确认号...', 'places.mapsSearchPlaceholder': '搜索地点...', 'places.mapsSearchError': '地点搜索失败。', + 'places.loadingDetails': '正在加载地点详情…', 'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。', 'places.osmActive': '通过 OpenStreetMap 搜索(无照片、评分或营业时间)。在设置中添加 Google API 密钥以获取增强数据。', 'places.categoryCreateError': '创建分类失败', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 86fa1418..9f270f58 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -832,6 +832,7 @@ const zhTw: Record = { 'places.reservationNotesPlaceholder': '預訂備註、確認號...', 'places.mapsSearchPlaceholder': '搜尋地點...', 'places.mapsSearchError': '地點搜尋失敗。', + 'places.loadingDetails': '正在載入地點詳情…', 'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。', 'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。', 'places.categoryCreateError': '建立分類失敗', diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts index d7a11de1..2fb289ad 100644 --- a/client/src/store/authStore.ts +++ b/client/src/store/authStore.ts @@ -178,6 +178,7 @@ export const useAuthStore = create((set, get) => ({ await authApi.updateMapsKey(key) set((state) => ({ user: state.user ? { ...state.user, maps_api_key: key || null } : null, + hasMapsKey: !!key, })) } catch (err: unknown) { throw new Error(getApiErrorMessage(err, 'Error saving API key')) @@ -188,6 +189,9 @@ export const useAuthStore = create((set, get) => ({ try { const data = await authApi.updateApiKeys(keys) set({ user: data.user }) + if ('maps_api_key' in keys) { + set({ hasMapsKey: !!keys.maps_api_key }) + } } catch (err: unknown) { throw new Error(getApiErrorMessage(err, 'Error saving API keys')) } diff --git a/server/src/services/mapsService.ts b/server/src/services/mapsService.ts index 119089cb..2f537850 100644 --- a/server/src/services/mapsService.ts +++ b/server/src/services/mapsService.ts @@ -115,6 +115,34 @@ export async function searchNominatim(query: string, lang?: string) { })); } +// ── Nominatim lookup (by OSM ID) ──────────────────────────────────────────── + +export async function lookupNominatim(osmType: string, osmId: string, lang?: string): Promise<{ + name: string; address: string; lat: number | null; lng: number | null; +} | null> { + const typePrefix = osmType.charAt(0).toUpperCase(); // N, W, R + const params = new URLSearchParams({ + osm_ids: `${typePrefix}${osmId}`, + format: 'json', + 'accept-language': lang || 'en', + }); + try { + const res = await fetch(`https://nominatim.openstreetmap.org/lookup?${params}`, { + headers: { 'User-Agent': UA }, + }); + if (!res.ok) return null; + const data = await res.json() as NominatimResult[]; + const item = data[0]; + if (!item) return null; + return { + name: item.name || item.display_name?.split(',')[0] || '', + address: item.display_name || '', + lat: parseFloat(item.lat) || null, + lng: parseFloat(item.lon) || null, + }; + } catch { return null; } +} + // ── Overpass API (OSM details) ─────────────────────────────────────────────── export async function fetchOverpassDetails(osmType: string, osmId: string): Promise { @@ -396,9 +424,21 @@ export async function getPlaceDetails(userId: number, placeId: string, lang?: st // OSM details: placeId is "node:123456" or "way:123456" etc. if (placeId.includes(':')) { const [osmType, osmId] = placeId.split(':'); - const element = await fetchOverpassDetails(osmType, osmId); - if (!element?.tags) return { place: buildOsmDetails({}, osmType, osmId) }; - return { place: buildOsmDetails(element.tags, osmType, osmId) }; + const [element, nominatim] = await Promise.all([ + fetchOverpassDetails(osmType, osmId), + lookupNominatim(osmType, osmId, lang), + ]); + const details = buildOsmDetails(element?.tags || {}, osmType, osmId); + return { + place: { + ...details, + name: nominatim?.name || element?.tags?.name || '', + address: nominatim?.address || '', + lat: nominatim?.lat ?? null, + lng: nominatim?.lng ?? null, + osm_id: placeId, + }, + }; } // Google details