mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Add translations for "Loading place details…" and improve place search functionality
- 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.
This commit is contained in:
@@ -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 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">{t('places.formName')} *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
required
|
||||
placeholder={t('places.formNamePlaceholder')}
|
||||
className="form-input"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
required
|
||||
placeholder={t('places.formNamePlaceholder')}
|
||||
className="form-input"
|
||||
/>
|
||||
{isSearchingMaps && (
|
||||
<div className="absolute right-2.5 top-0 bottom-0 flex items-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
|
||||
@@ -855,6 +855,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'places.reservationNotesPlaceholder': 'ملاحظات الحجز، رقم التأكيد...',
|
||||
'places.mapsSearchPlaceholder': 'ابحث عن أماكن...',
|
||||
'places.mapsSearchError': 'فشل البحث عن المكان.',
|
||||
'places.loadingDetails': 'جارٍ تحميل تفاصيل المكان…',
|
||||
'places.osmHint': 'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.',
|
||||
'places.osmActive': 'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.',
|
||||
'places.categoryCreateError': 'فشل إنشاء الفئة',
|
||||
|
||||
@@ -837,6 +837,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -853,6 +853,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -853,6 +853,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -872,6 +872,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -828,6 +828,7 @@ const es: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -852,6 +852,7 @@ const fr: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -853,6 +853,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -853,6 +853,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -852,6 +852,7 @@ const nl: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -811,6 +811,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -852,6 +852,7 @@ const ru: Record<string, string> = {
|
||||
'places.reservationNotesPlaceholder': 'Заметки о бронировании, номер подтверждения...',
|
||||
'places.mapsSearchPlaceholder': 'Поиск мест...',
|
||||
'places.mapsSearchError': 'Ошибка поиска мест.',
|
||||
'places.loadingDetails': 'Загрузка данных о месте…',
|
||||
'places.osmHint': 'Поиск через OpenStreetMap (без фото, часов работы и рейтингов). Добавьте API-ключ Google в настройках для полной информации.',
|
||||
'places.osmActive': 'Поиск через OpenStreetMap (без фото, рейтингов и часов работы). Добавьте API-ключ Google в настройках для расширенных данных.',
|
||||
'places.categoryCreateError': 'Не удалось создать категорию',
|
||||
|
||||
@@ -852,6 +852,7 @@ const zh: Record<string, string> = {
|
||||
'places.reservationNotesPlaceholder': '预订备注、确认号...',
|
||||
'places.mapsSearchPlaceholder': '搜索地点...',
|
||||
'places.mapsSearchError': '地点搜索失败。',
|
||||
'places.loadingDetails': '正在加载地点详情…',
|
||||
'places.osmHint': '使用 OpenStreetMap 搜索(无照片、营业时间或评分)。在设置中添加 Google API 密钥以获取完整信息。',
|
||||
'places.osmActive': '通过 OpenStreetMap 搜索(无照片、评分或营业时间)。在设置中添加 Google API 密钥以获取增强数据。',
|
||||
'places.categoryCreateError': '创建分类失败',
|
||||
|
||||
@@ -832,6 +832,7 @@ const zhTw: Record<string, string> = {
|
||||
'places.reservationNotesPlaceholder': '預訂備註、確認號...',
|
||||
'places.mapsSearchPlaceholder': '搜尋地點...',
|
||||
'places.mapsSearchError': '地點搜尋失敗。',
|
||||
'places.loadingDetails': '正在載入地點詳情…',
|
||||
'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
|
||||
'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。',
|
||||
'places.categoryCreateError': '建立分類失敗',
|
||||
|
||||
@@ -178,6 +178,7 @@ export const useAuthStore = create<AuthState>((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<AuthState>((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'))
|
||||
}
|
||||
|
||||
@@ -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<OverpassElement | null> {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user