perf: major trip planner performance overhaul (#218)

Store & re-render optimization:
- TripPlannerPage uses selective Zustand selectors instead of full store
- placesSlice only updates affected days on place update/delete
- Route calculation only reacts to selected day's assignments
- DayPlanSidebar uses stable action refs instead of full store

Map marker performance:
- Shared photoService for PlaceAvatar and MapView (single cache, no duplicate requests)
- Client-side base64 thumbnail generation via canvas (CORS-safe for Wikimedia)
- Map markers use base64 data URL <img> tags for smooth zoom (no external image decode)
- Sidebar uses same base64 thumbnails with IntersectionObserver for visible-first loading
- Icon cache prevents duplicate L.divIcon creation
- MarkerClusterGroup with animate:false and optimized chunk settings
- Photo fetch deduplication and batched state updates

Server optimizations:
- Wikimedia image size reduced to 400px (from 600px)
- Photo cache: 5min TTL for errors (was 12h), prevents stale 404 caching
- Removed unused image-proxy endpoint

UX improvements:
- Splash screen with plane animation during initial photo preload
- Markdown rendering in DayPlanSidebar place descriptions
- Missing i18n keys added, all 12 languages synced to 1376 keys
This commit is contained in:
Maurice
2026-04-01 14:56:01 +02:00
parent 7d0ae631b8
commit 95cb81b0e5
20 changed files with 456 additions and 212 deletions
+2 -2
View File
@@ -248,6 +248,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'settings.roleAdmin': 'مسؤول',
'settings.oidcLinked': 'مرتبط مع',
'settings.changePassword': 'تغيير كلمة المرور',
'settings.mustChangePassword': 'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
'settings.currentPassword': 'كلمة المرور الحالية',
'settings.currentPasswordRequired': 'كلمة المرور الحالية مطلوبة',
'settings.newPassword': 'كلمة المرور الجديدة',
@@ -695,7 +696,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'الإحصائيات',
'atlas.bucketTab': 'قائمة الأمنيات',
'atlas.addBucket': 'إضافة إلى قائمة الأمنيات',
'atlas.bucketNamePlaceholder': 'مكان أو وجهة...',
'atlas.bucketNotesPlaceholder': 'ملاحظات (اختياري)',
'atlas.bucketEmpty': 'قائمة أمنياتك فارغة',
'atlas.bucketEmptyHint': 'أضف أماكن تحلم بزيارتها',
@@ -708,7 +708,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'atlas.nextTrip': 'الرحلة القادمة',
'atlas.daysLeft': 'يوم متبقٍ',
'atlas.streak': 'سلسلة',
'atlas.year': 'سنة',
'atlas.years': 'سنوات',
'atlas.yearInRow': 'سنة متتالية',
'atlas.yearsInRow': 'سنوات متتالية',
@@ -738,6 +737,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'trip.tabs.budget': 'الميزانية',
'trip.tabs.files': 'الملفات',
'trip.loading': 'جارٍ تحميل الرحلة...',
'trip.loadingPhotos': 'جارٍ تحميل صور الأماكن...',
'trip.mobilePlan': 'الخطة',
'trip.mobilePlaces': 'الأماكن',
'trip.toast.placeUpdated': 'تم تحديث المكان',
+18 -2
View File
@@ -294,6 +294,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'settings.mcp.toast.createError': 'Falha ao criar token',
'settings.mcp.toast.deleted': 'Token excluído',
'settings.mcp.toast.deleteError': 'Falha ao excluir token',
'settings.mustChangePassword': 'Você deve alterar sua senha antes de continuar. Defina uma nova senha abaixo.',
// Login
'login.error': 'Falha no login. Verifique suas credenciais.',
@@ -503,11 +504,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'admin.addons.disabled': 'Desativado',
'admin.addons.type.trip': 'Viagem',
'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Integração',
'admin.addons.tripHint': 'Disponível como aba em cada viagem',
'admin.addons.globalHint': 'Disponível como seção própria na navegação principal',
'admin.addons.toast.updated': 'Complemento atualizado',
'admin.addons.toast.error': 'Falha ao atualizar complemento',
'admin.addons.noAddons': 'Nenhum complemento disponível',
'admin.addons.integrationHint': 'Serviços de backend e integrações de API sem página dedicada',
// Weather info
'admin.weather.title': 'Dados meteorológicos',
'admin.weather.badge': 'Desde 24 de março de 2026',
@@ -675,7 +678,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'Estatísticas',
'atlas.bucketTab': 'Lista de desejos',
'atlas.addBucket': 'Adicionar à lista de desejos',
'atlas.bucketNamePlaceholder': 'Lugar ou destino...',
'atlas.bucketNotesPlaceholder': 'Notas (opcional)',
'atlas.bucketEmpty': 'Sua lista de desejos está vazia',
'atlas.bucketEmptyHint': 'Adicione lugares que sonha em visitar',
@@ -688,7 +690,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'atlas.nextTrip': 'Próxima viagem',
'atlas.daysLeft': 'dias restantes',
'atlas.streak': 'Sequência',
'atlas.year': 'ano',
'atlas.years': 'anos',
'atlas.yearInRow': 'ano seguido',
'atlas.yearsInRow': 'anos seguidos',
@@ -730,6 +731,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'trip.toast.reservationAdded': 'Reserva adicionada',
'trip.toast.deleted': 'Excluído',
'trip.confirm.deletePlace': 'Tem certeza de que deseja excluir este lugar?',
'trip.loadingPhotos': 'Carregando fotos dos lugares...',
// Day Plan Sidebar
'dayplan.emptyDay': 'Nenhum lugar planejado para este dia',
@@ -1414,6 +1416,20 @@ const br: Record<string, string | { name: string; category: string }[]> = {
// Permissions
'admin.tabs.permissions': 'Permissões',
'admin.tabs.mcpTokens': 'Tokens MCP',
'admin.mcpTokens.title': 'Tokens MCP',
'admin.mcpTokens.subtitle': 'Gerenciar tokens de API de todos os usuários',
'admin.mcpTokens.owner': 'Proprietário',
'admin.mcpTokens.tokenName': 'Nome do Token',
'admin.mcpTokens.created': 'Criado',
'admin.mcpTokens.lastUsed': 'Último uso',
'admin.mcpTokens.never': 'Nunca',
'admin.mcpTokens.empty': 'Nenhum token MCP foi criado ainda',
'admin.mcpTokens.deleteTitle': 'Excluir Token',
'admin.mcpTokens.deleteMessage': 'Isso revogará o token imediatamente. O usuário perderá o acesso MCP por este token.',
'admin.mcpTokens.deleteSuccess': 'Token excluído',
'admin.mcpTokens.deleteError': 'Falha ao excluir token',
'admin.mcpTokens.loadError': 'Falha ao carregar tokens',
'perm.title': 'Configurações de Permissões',
'perm.subtitle': 'Controle quem pode realizar ações no aplicativo',
'perm.saved': 'Configurações de permissões salvas',
+1 -1
View File
@@ -695,7 +695,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'Statistiky',
'atlas.bucketTab': 'Bucket List',
'atlas.addBucket': 'Přidat na Bucket List',
'atlas.bucketNamePlaceholder': 'Místo nebo destinace...',
'atlas.bucketNotesPlaceholder': 'Poznámky (volitelné)',
'atlas.bucketEmpty': 'Váš seznam přání je prázdný',
'atlas.bucketEmptyHint': 'Přidejte místa, která sníte navštívit',
@@ -738,6 +737,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'trip.tabs.budget': 'Rozpočet',
'trip.tabs.files': 'Soubory',
'trip.loading': 'Načítání cesty...',
'trip.loadingPhotos': 'Načítání fotek míst...',
'trip.mobilePlan': 'Plán',
'trip.mobilePlaces': 'Místa',
'trip.toast.placeUpdated': 'Místo bylo aktualizováno',
+2 -2
View File
@@ -243,6 +243,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'settings.roleAdmin': 'Administrator',
'settings.oidcLinked': 'Verknüpft mit',
'settings.changePassword': 'Passwort ändern',
'settings.mustChangePassword': 'Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können. Bitte legen Sie unten ein neues Passwort fest.',
'settings.currentPassword': 'Aktuelles Passwort',
'settings.currentPasswordRequired': 'Aktuelles Passwort wird benötigt',
'settings.newPassword': 'Neues Passwort',
@@ -693,7 +694,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'Statistik',
'atlas.bucketTab': 'Bucket List',
'atlas.addBucket': 'Zur Bucket List hinzufügen',
'atlas.bucketNamePlaceholder': 'Ort oder Reiseziel...',
'atlas.bucketNotesPlaceholder': 'Notizen (optional)',
'atlas.bucketEmpty': 'Deine Bucket List ist leer',
'atlas.bucketEmptyHint': 'Füge Orte hinzu, die du besuchen möchtest',
@@ -706,7 +706,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'atlas.nextTrip': 'Nächster Trip',
'atlas.daysLeft': 'Tage',
'atlas.streak': 'Streak',
'atlas.year': 'Jahr',
'atlas.years': 'Jahre',
'atlas.yearInRow': 'Jahr in Folge',
'atlas.yearsInRow': 'Jahre in Folge',
@@ -736,6 +735,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'trip.tabs.budget': 'Budget',
'trip.tabs.files': 'Dateien',
'trip.loading': 'Reise wird geladen...',
'trip.loadingPhotos': 'Fotos der Orte werden geladen...',
'trip.mobilePlan': 'Planung',
'trip.mobilePlaces': 'Orte',
'trip.toast.placeUpdated': 'Ort aktualisiert',
+1
View File
@@ -732,6 +732,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'trip.tabs.budget': 'Budget',
'trip.tabs.files': 'Files',
'trip.loading': 'Loading trip...',
'trip.loadingPhotos': 'Loading place photos...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Places',
'trip.toast.placeUpdated': 'Place updated',
+2 -2
View File
@@ -244,6 +244,7 @@ const es: Record<string, string> = {
'settings.roleAdmin': 'Administrador',
'settings.oidcLinked': 'Vinculado con',
'settings.changePassword': 'Cambiar contraseña',
'settings.mustChangePassword': 'Debe cambiar su contraseña antes de continuar. Establezca una nueva contraseña a continuación.',
'settings.currentPassword': 'Contraseña actual',
'settings.newPassword': 'Nueva contraseña',
'settings.confirmPassword': 'Confirmar nueva contraseña',
@@ -697,9 +698,7 @@ const es: Record<string, string> = {
'atlas.addToBucket': 'Añadir a lista de deseos',
'atlas.addPoi': 'Añadir lugar',
'atlas.searchCountry': 'Buscar un país...',
'atlas.bucketNamePlaceholder': 'Nombre (país, ciudad, lugar…)',
'atlas.month': 'Mes',
'atlas.year': 'Año',
'atlas.addToBucketHint': 'Guardar como lugar que quieres visitar',
'atlas.bucketWhen': '¿Cuándo planeas visitarlo?',
@@ -712,6 +711,7 @@ const es: Record<string, string> = {
'trip.tabs.budget': 'Presupuesto',
'trip.tabs.files': 'Archivos',
'trip.loading': 'Cargando viaje...',
'trip.loadingPhotos': 'Cargando fotos de los lugares...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Lugares',
'trip.toast.placeUpdated': 'Lugar actualizado',
+2 -2
View File
@@ -243,6 +243,7 @@ const fr: Record<string, string> = {
'settings.roleAdmin': 'Administrateur',
'settings.oidcLinked': 'Lié avec',
'settings.changePassword': 'Changer le mot de passe',
'settings.mustChangePassword': 'Vous devez changer votre mot de passe avant de continuer. Veuillez définir un nouveau mot de passe ci-dessous.',
'settings.currentPassword': 'Mot de passe actuel',
'settings.currentPasswordRequired': 'Le mot de passe actuel est requis',
'settings.newPassword': 'Nouveau mot de passe',
@@ -720,9 +721,7 @@ const fr: Record<string, string> = {
'atlas.addToBucket': 'Ajouter à la bucket list',
'atlas.addPoi': 'Ajouter un lieu',
'atlas.searchCountry': 'Rechercher un pays…',
'atlas.bucketNamePlaceholder': 'Nom (pays, ville, lieu…)',
'atlas.month': 'Mois',
'atlas.year': 'Année',
'atlas.addToBucketHint': 'Sauvegarder comme lieu à visiter',
'atlas.bucketWhen': 'Quand prévoyez-vous d\'y aller ?',
@@ -735,6 +734,7 @@ const fr: Record<string, string> = {
'trip.tabs.budget': 'Budget',
'trip.tabs.files': 'Fichiers',
'trip.loading': 'Chargement du voyage…',
'trip.loadingPhotos': 'Chargement des photos des lieux...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Lieux',
'trip.toast.placeUpdated': 'Lieu mis à jour',
+2
View File
@@ -246,6 +246,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'settings.mfa.toastEnabled': 'Kétfaktoros hitelesítés engedélyezve',
'settings.mfa.toastDisabled': 'Kétfaktoros hitelesítés kikapcsolva',
'settings.mfa.demoBlocked': 'Demo módban nem érhető el',
'settings.mustChangePassword': 'A folytatás előtt meg kell változtatnod a jelszavad. Kérjük, adj meg egy új jelszót alább.',
'admin.notifications.title': 'Értesítések',
'admin.notifications.hint': 'Válasszon értesítési csatornát. Egyszerre csak egy lehet aktív.',
'admin.notifications.none': 'Kikapcsolva',
@@ -746,6 +747,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'trip.toast.reservationAdded': 'Foglalás hozzáadva',
'trip.toast.deleted': 'Törölve',
'trip.confirm.deletePlace': 'Biztosan törölni szeretnéd ezt a helyet?',
'trip.loadingPhotos': 'Helyek fotóinak betöltése...',
// Napi terv oldalsáv
'dayplan.emptyDay': 'Nincs tervezett hely erre a napra',
+3 -1
View File
@@ -246,6 +246,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'settings.mfa.toastEnabled': 'Autenticazione a due fattori abilitata',
'settings.mfa.toastDisabled': 'Autenticazione a due fattori disabilitata',
'settings.mfa.demoBlocked': 'Non disponibile in modalità demo',
'settings.mustChangePassword': 'Devi cambiare la password prima di continuare. Imposta una nuova password qui sotto.',
'admin.notifications.title': 'Notifiche',
'admin.notifications.hint': 'Scegli un canale di notifica. Solo uno può essere attivo alla volta.',
'admin.notifications.none': 'Disattivato',
@@ -691,7 +692,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'atlas.statsTab': 'Statistiche',
'atlas.bucketTab': 'Lista desideri',
'atlas.addBucket': 'Aggiungi alla lista desideri',
'atlas.bucketNamePlaceholder': 'Luogo o destinazione...',
'atlas.bucketNotesPlaceholder': 'Note (opzionale)',
'atlas.bucketEmpty': 'La tua lista desideri è vuota',
'atlas.bucketEmptyHint': 'Aggiungi luoghi che sogni di visitare',
@@ -724,6 +724,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'atlas.tripPlural': 'Viaggi',
'atlas.placeVisited': 'Luogo visitato',
'atlas.placesVisited': 'Luoghi visitati',
'atlas.searchCountry': 'Cerca un paese...',
// Trip Planner
'trip.tabs.plan': 'Programma',
@@ -746,6 +747,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'trip.toast.reservationAdded': 'Prenotazione aggiunta',
'trip.toast.deleted': 'Eliminato',
'trip.confirm.deletePlace': 'Sei sicuro di voler eliminare questo luogo?',
'trip.loadingPhotos': 'Caricamento foto dei luoghi...',
// Day Plan Sidebar
'dayplan.emptyDay': 'Nessun luogo programmato per questo giorno',
+2 -2
View File
@@ -243,6 +243,7 @@ const nl: Record<string, string> = {
'settings.roleAdmin': 'Beheerder',
'settings.oidcLinked': 'Gekoppeld met',
'settings.changePassword': 'Wachtwoord wijzigen',
'settings.mustChangePassword': 'U moet uw wachtwoord wijzigen voordat u kunt doorgaan. Stel hieronder een nieuw wachtwoord in.',
'settings.currentPassword': 'Huidig wachtwoord',
'settings.currentPasswordRequired': 'Huidig wachtwoord is verplicht',
'settings.newPassword': 'Nieuw wachtwoord',
@@ -720,9 +721,7 @@ const nl: Record<string, string> = {
'atlas.addToBucket': 'Aan bucket list toevoegen',
'atlas.addPoi': 'Plaats toevoegen',
'atlas.searchCountry': 'Zoek een land...',
'atlas.bucketNamePlaceholder': 'Naam (land, stad, plek…)',
'atlas.month': 'Maand',
'atlas.year': 'Jaar',
'atlas.addToBucketHint': 'Opslaan als plek die je wilt bezoeken',
'atlas.bucketWhen': 'Wanneer ben je van plan te gaan?',
@@ -735,6 +734,7 @@ const nl: Record<string, string> = {
'trip.tabs.budget': 'Budget',
'trip.tabs.files': 'Bestanden',
'trip.loading': 'Reis laden...',
'trip.loadingPhotos': 'Plaatsfoto laden...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Plaatsen',
'trip.toast.placeUpdated': 'Plaats bijgewerkt',
+2 -2
View File
@@ -243,6 +243,7 @@ const ru: Record<string, string> = {
'settings.roleAdmin': 'Администратор',
'settings.oidcLinked': 'Связан с',
'settings.changePassword': 'Изменить пароль',
'settings.mustChangePassword': 'Вы должны сменить пароль перед продолжением. Пожалуйста, установите новый пароль ниже.',
'settings.currentPassword': 'Текущий пароль',
'settings.currentPasswordRequired': 'Текущий пароль обязателен',
'settings.newPassword': 'Новый пароль',
@@ -720,9 +721,7 @@ const ru: Record<string, string> = {
'atlas.addToBucket': 'В список желаний',
'atlas.addPoi': 'Добавить место',
'atlas.searchCountry': 'Поиск страны...',
'atlas.bucketNamePlaceholder': 'Название (страна, город, место…)',
'atlas.month': 'Месяц',
'atlas.year': 'Год',
'atlas.addToBucketHint': 'Сохранить как место для посещения',
'atlas.bucketWhen': 'Когда вы планируете поехать?',
@@ -735,6 +734,7 @@ const ru: Record<string, string> = {
'trip.tabs.budget': 'Бюджет',
'trip.tabs.files': 'Файлы',
'trip.loading': 'Загрузка поездки...',
'trip.loadingPhotos': 'Загрузка фото мест...',
'trip.mobilePlan': 'План',
'trip.mobilePlaces': 'Места',
'trip.toast.placeUpdated': 'Место обновлено',
+2 -2
View File
@@ -243,6 +243,7 @@ const zh: Record<string, string> = {
'settings.roleAdmin': '管理员',
'settings.oidcLinked': '已关联',
'settings.changePassword': '修改密码',
'settings.mustChangePassword': '您必须更改密码才能继续。请在下方设置新密码。',
'settings.currentPassword': '当前密码',
'settings.currentPasswordRequired': '请输入当前密码',
'settings.newPassword': '新密码',
@@ -720,9 +721,7 @@ const zh: Record<string, string> = {
'atlas.addToBucket': '添加到心愿单',
'atlas.addPoi': '添加地点',
'atlas.searchCountry': '搜索国家...',
'atlas.bucketNamePlaceholder': '名称(国家、城市、地点…)',
'atlas.month': '月份',
'atlas.year': '年份',
'atlas.addToBucketHint': '保存为想去的地方',
'atlas.bucketWhen': '你计划什么时候去?',
@@ -735,6 +734,7 @@ const zh: Record<string, string> = {
'trip.tabs.budget': '预算',
'trip.tabs.files': '文件',
'trip.loading': '加载旅行中...',
'trip.loadingPhotos': '正在加载地点照片...',
'trip.mobilePlan': '计划',
'trip.mobilePlaces': '地点',
'trip.toast.placeUpdated': '地点已更新',