From c0b5d941dd1f2fa2ba0bf590b86b8b596a1f43b5 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 21 Jun 2026 23:08:25 +0200 Subject: [PATCH] fix(dashboard): show an error instead of a blank trip list when the server is unreachable (#1283) When the backend or identity provider was unreachable, a returning user with a persisted session landed on the dashboard with an empty trip grid and no error. That looks identical to a logged-in user who simply has no trips, so people assumed their data had been lost. Three client-side layers were quietly swallowing the failure: the auth check only cleared state on a 401, so a 5xx or a network error left the stale session in place and kept rendering the protected route; the offline-first trip repo turned a failed fetch into the empty cache without throwing; and the dashboard had neither an error nor an empty state, so a blank grid meant both "outage" and "no trips". The auth check now tells genuine offline (keep serving the cache silently, the PWA happy path) apart from a server outage while online (keep the session but flag it). The dashboard shows a reassuring "couldn't reach the server, your trips are safe" banner with a retry, and a real zero-trip account finally gets a proper empty state so the two cases never look alike. New strings added across all locales. --- client/src/pages/DashboardPage.tsx | 17 +++++++++++++ client/src/pages/dashboard/useDashboard.ts | 13 +++++++++- client/src/store/authStore.ts | 28 ++++++++++++++++++---- client/src/styles/dashboard.css | 27 +++++++++++++++++++++ shared/src/i18n/ar/dashboard.ts | 2 ++ shared/src/i18n/br/dashboard.ts | 2 ++ shared/src/i18n/cs/dashboard.ts | 2 ++ shared/src/i18n/de/dashboard.ts | 2 ++ shared/src/i18n/en/dashboard.ts | 2 ++ shared/src/i18n/es/dashboard.ts | 2 ++ shared/src/i18n/fr/dashboard.ts | 3 +++ shared/src/i18n/gr/dashboard.ts | 3 +++ shared/src/i18n/hu/dashboard.ts | 2 ++ shared/src/i18n/id/dashboard.ts | 2 ++ shared/src/i18n/it/dashboard.ts | 3 +++ shared/src/i18n/ja/dashboard.ts | 2 ++ shared/src/i18n/ko/dashboard.ts | 2 ++ shared/src/i18n/nl/dashboard.ts | 2 ++ shared/src/i18n/pl/dashboard.ts | 2 ++ shared/src/i18n/ru/dashboard.ts | 2 ++ shared/src/i18n/tr/dashboard.ts | 2 ++ shared/src/i18n/uk/dashboard.ts | 3 +++ shared/src/i18n/zh-TW/dashboard.ts | 2 ++ shared/src/i18n/zh/dashboard.ts | 2 ++ 24 files changed, 123 insertions(+), 6 deletions(-) diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 82725064..110ee9a9 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -84,6 +84,7 @@ export default function DashboardPage(): React.ReactElement { const { demoMode, locale, t, navigate, spotlight, heroBundle, stats, upcoming, gridTrips, isLoading, + loadError, retryLoad, tripFilter, setTripFilter, viewMode, toggleViewMode, showForm, setShowForm, editingTrip, setEditingTrip, deleteTrip, setDeleteTrip, copyTrip, setCopyTrip, setTrips, @@ -102,6 +103,15 @@ export default function DashboardPage(): React.ReactElement {
+ {loadError && ( +
+ {t('dashboard.loadErrorBanner')} + +
+ )} {spotlight && (
+ {gridTrips.length === 0 && tripFilter === 'planned' && !isLoading && !loadError && ( +
+

{t('dashboard.emptyTitle')}

+

{t('dashboard.emptyText')}

+
+ )} +
{gridTrips.map(trip => ( (null) const [copyTrip, setCopyTrip] = useState(null) const [tripFilter, setTripFilter] = useState<'planned' | 'archive' | 'completed'>('planned') + const [loadError, setLoadError] = useState(false) const [stats, setStats] = useState(null) const [upcoming, setUpcoming] = useState([]) @@ -42,7 +43,7 @@ export function useDashboard() { const [searchParams, setSearchParams] = useSearchParams() const toast = useToast() const { t, locale } = useTranslation() - const { demoMode } = useAuthStore() + const { demoMode, authCheckFailed, loadUser } = useAuthStore() const toggleViewMode = () => { setViewMode(prev => { @@ -74,13 +75,22 @@ export function useDashboard() { const { trips, archivedTrips } = await tripRepo.list() setTrips(sortTrips(trips)) setArchivedTrips(sortTrips(archivedTrips)) + setLoadError(false) } catch { + setLoadError(true) toast.error(t('dashboard.toast.loadError')) } finally { setIsLoading(false) } } + // Re-run both the trip fetch and the auth check so a recovered backend clears + // the error banner (loadUser resets authCheckFailed on success). #1283 + const retryLoad = () => { + loadUser({ silent: true }) + loadTrips() + } + const today = new Date().toISOString().split('T')[0] const spotlight = trips.find(t => t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) || trips.find(t => t.start_date && t.start_date >= today) @@ -177,6 +187,7 @@ export function useDashboard() { demoMode, locale, t, navigate, // data + derived spotlight, heroBundle, stats, upcoming, gridTrips, isLoading, + loadError: loadError || authCheckFailed, retryLoad, // ui state tripFilter, setTripFilter, viewMode, toggleViewMode, showForm, setShowForm, editingTrip, setEditingTrip, diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts index 4b83d313..05b8b675 100644 --- a/client/src/store/authStore.ts +++ b/client/src/store/authStore.ts @@ -25,6 +25,11 @@ interface AuthState { user: User | null isAuthenticated: boolean isLoading: boolean + /** The auth check (loadUser) failed for a non-401 reason while we were online — + * the server was unreachable or erroring. Surfaced by the UI so a backend/IdP + * outage doesn't render as a blank, error-free page that looks like lost data. + * Transient, never persisted. #1283 */ + authCheckFailed: boolean error: string | null demoMode: boolean devMode: boolean @@ -86,6 +91,7 @@ export const useAuthStore = create()( user: null, isAuthenticated: false, isLoading: true, + authCheckFailed: false, error: null, demoMode: localStorage.getItem('demo_mode') === 'true', devMode: false, @@ -200,6 +206,7 @@ export const useAuthStore = create()( set({ user: null, isAuthenticated: false, + authCheckFailed: false, error: null, }) }, @@ -215,22 +222,33 @@ export const useAuthStore = create()( user: data.user, isAuthenticated: true, isLoading: false, + authCheckFailed: false, }) await onAuthSuccess(data.user.id) connect() } catch (err: unknown) { if (seq !== authSequence) return // stale response — ignore - // Only clear auth state on 401 (invalid/expired token), not on network errors - const isAuthError = err && typeof err === 'object' && 'response' in err && - (err as { response?: { status?: number } }).response?.status === 401 - if (isAuthError) { + const status = err && typeof err === 'object' && 'response' in err + ? (err as { response?: { status?: number } }).response?.status + : undefined + if (status === 401) { + // Invalid/expired token — clear auth so the guard redirects to login. set({ user: null, isAuthenticated: false, isLoading: false, + authCheckFailed: false, }) - } else { + } else if (status === undefined && typeof navigator !== 'undefined' && !navigator.onLine) { + // Genuinely offline — keep the persisted session so the PWA serves cached + // data without a scary error. This is the offline-first happy path. set({ isLoading: false }) + } else { + // Server erroring (5xx) or unreachable while we're online: keep the session + // (don't eject the user over a transient outage), but flag it so the UI can + // say "couldn't reach the server" instead of showing a blank, error-free + // page that looks like the user's trips were lost. #1283 + set({ isLoading: false, authCheckFailed: true }) } } }, diff --git a/client/src/styles/dashboard.css b/client/src/styles/dashboard.css index 12806c05..aaa45fb8 100644 --- a/client/src/styles/dashboard.css +++ b/client/src/styles/dashboard.css @@ -456,6 +456,33 @@ .trek-dash .add-trip-card .ttl { font-size: 16px; font-weight: 500; margin-bottom: 4px; } .trek-dash .add-trip-card .sub { font-size: 13px; color: var(--ink-3); } +/* Error banner — shown when the trip list or the auth check couldn't reach the + server, so a backend/IdP outage no longer looks like an empty (lost-data) + dashboard. Amber rather than red: it reassures (data is safe) more than it alarms. */ +.trek-dash .dash-error { + display: flex; align-items: center; gap: 14px; flex-wrap: wrap; + padding: 14px 18px; margin-bottom: 22px; + background: oklch(0.74 0.14 75 / 0.13); + border: 1px solid oklch(0.74 0.14 75 / 0.45); + border-radius: var(--r-md); + box-shadow: var(--sh-sm); +} +.trek-dash .dash-error-txt { flex: 1; min-width: 200px; font-size: 14px; color: var(--ink); } +.trek-dash .dash-error-retry { + display: inline-flex; align-items: center; gap: 7px; + padding: 8px 14px; border: none; border-radius: var(--r-xs); + background: var(--ink); color: var(--surface); + font-size: 13px; font-weight: 500; cursor: pointer; + transition: opacity .15s ease; +} +.trek-dash .dash-error-retry:hover { opacity: .88; } + +/* Empty state — a genuine "you have no trips yet" message, visually distinct + from the error banner above so an outage and a real empty list never look alike. */ +.trek-dash .trips-empty { margin-bottom: 18px; } +.trek-dash .trips-empty h4 { font-size: 18px; font-weight: 600; color: var(--ink); margin: 0 0 6px; } +.trek-dash .trips-empty p { font-size: 14px; color: var(--ink-3); margin: 0; } + /* ----------------- tools sidebar ----------------- */ .trek-dash .tool { background: var(--glass-bg); border-radius: var(--r-xl); padding: 24px 26px; diff --git a/shared/src/i18n/ar/dashboard.ts b/shared/src/i18n/ar/dashboard.ts index c5cff7e8..d293d232 100644 --- a/shared/src/i18n/ar/dashboard.ts +++ b/shared/src/i18n/ar/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'منتهية', 'dashboard.status.daysLeft': 'متبقي {count} يوم', 'dashboard.toast.loadError': 'فشل تحميل الرحلات', + 'dashboard.loadErrorBanner': 'تعذّر الوصول إلى الخادم. رحلاتك في أمان — يرجى المحاولة مرة أخرى.', + 'dashboard.retry': 'إعادة المحاولة', 'dashboard.toast.created': 'تم إنشاء الرحلة بنجاح', 'dashboard.toast.createError': 'فشل إنشاء الرحلة', 'dashboard.toast.updated': 'تم تحديث الرحلة', diff --git a/shared/src/i18n/br/dashboard.ts b/shared/src/i18n/br/dashboard.ts index d6d56b62..7780bbf9 100644 --- a/shared/src/i18n/br/dashboard.ts +++ b/shared/src/i18n/br/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Passada', 'dashboard.status.daysLeft': 'Faltam {count} dias', 'dashboard.toast.loadError': 'Não foi possível carregar as viagens', + 'dashboard.loadErrorBanner': 'Não foi possível conectar ao servidor. Suas viagens estão seguras — tente novamente.', + 'dashboard.retry': 'Tentar novamente', 'dashboard.toast.created': 'Viagem criada com sucesso!', 'dashboard.toast.createError': 'Não foi possível criar a viagem', 'dashboard.toast.updated': 'Viagem atualizada!', diff --git a/shared/src/i18n/cs/dashboard.ts b/shared/src/i18n/cs/dashboard.ts index 533d1c7a..56e840ca 100644 --- a/shared/src/i18n/cs/dashboard.ts +++ b/shared/src/i18n/cs/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Proběhlé', 'dashboard.status.daysLeft': 'zbývá {count} dní', 'dashboard.toast.loadError': 'Nepodařilo se načíst cesty', + 'dashboard.loadErrorBanner': 'Server nebyl dostupný. Vaše cesty jsou v bezpečí — zkuste to prosím znovu.', + 'dashboard.retry': 'Zkusit znovu', 'dashboard.toast.created': 'Cesta byla úspěšně vytvořena!', 'dashboard.toast.createError': 'Nepodařilo se vytvořit cestu', 'dashboard.toast.updated': 'Cesta byla aktualizována!', diff --git a/shared/src/i18n/de/dashboard.ts b/shared/src/i18n/de/dashboard.ts index 85345437..5f2c797a 100644 --- a/shared/src/i18n/de/dashboard.ts +++ b/shared/src/i18n/de/dashboard.ts @@ -43,6 +43,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Vergangen', 'dashboard.status.daysLeft': 'Noch {count} Tage', 'dashboard.toast.loadError': 'Fehler beim Laden der Reisen', + 'dashboard.loadErrorBanner': 'Server nicht erreichbar. Deine Reisen sind sicher — bitte versuche es erneut.', + 'dashboard.retry': 'Erneut versuchen', 'dashboard.toast.created': 'Reise erfolgreich erstellt!', 'dashboard.toast.createError': 'Fehler beim Erstellen', 'dashboard.toast.updated': 'Reise aktualisiert!', diff --git a/shared/src/i18n/en/dashboard.ts b/shared/src/i18n/en/dashboard.ts index e6e33d54..ac6eeaba 100644 --- a/shared/src/i18n/en/dashboard.ts +++ b/shared/src/i18n/en/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Past', 'dashboard.status.daysLeft': '{count} days left', 'dashboard.toast.loadError': 'Failed to load trips', + 'dashboard.loadErrorBanner': "Couldn't reach the server. Your trips are safe — please try again.", + 'dashboard.retry': 'Retry', 'dashboard.toast.created': 'Trip created successfully!', 'dashboard.toast.createError': 'Failed to create trip', 'dashboard.toast.updated': 'Trip updated!', diff --git a/shared/src/i18n/es/dashboard.ts b/shared/src/i18n/es/dashboard.ts index fe30e624..d079de15 100644 --- a/shared/src/i18n/es/dashboard.ts +++ b/shared/src/i18n/es/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Pasado', 'dashboard.status.daysLeft': 'Quedan {count} días', 'dashboard.toast.loadError': 'No se pudieron cargar los viajes', + 'dashboard.loadErrorBanner': 'No se pudo conectar con el servidor. Tus viajes están a salvo: inténtalo de nuevo.', + 'dashboard.retry': 'Reintentar', 'dashboard.toast.created': '¡Viaje creado correctamente!', 'dashboard.toast.createError': 'No se pudo crear el viaje', 'dashboard.toast.updated': '¡Viaje actualizado!', diff --git a/shared/src/i18n/fr/dashboard.ts b/shared/src/i18n/fr/dashboard.ts index 276bd73a..31eab9ae 100644 --- a/shared/src/i18n/fr/dashboard.ts +++ b/shared/src/i18n/fr/dashboard.ts @@ -42,6 +42,9 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Passé', 'dashboard.status.daysLeft': '{count} jours restants', 'dashboard.toast.loadError': 'Impossible de charger les voyages', + 'dashboard.loadErrorBanner': + "Impossible de joindre le serveur. Vos voyages sont en sécurité — veuillez réessayer.", + 'dashboard.retry': 'Réessayer', 'dashboard.toast.created': 'Voyage créé avec succès !', 'dashboard.toast.createError': 'Impossible de créer le voyage', 'dashboard.toast.updated': 'Voyage mis à jour !', diff --git a/shared/src/i18n/gr/dashboard.ts b/shared/src/i18n/gr/dashboard.ts index bd43df0e..8c347856 100644 --- a/shared/src/i18n/gr/dashboard.ts +++ b/shared/src/i18n/gr/dashboard.ts @@ -41,6 +41,9 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Παρελθόν', 'dashboard.status.daysLeft': '{count} μέρες έμειναν', 'dashboard.toast.loadError': 'Αποτυχία φόρτωσης ταξιδιών', + 'dashboard.loadErrorBanner': + 'Δεν ήταν δυνατή η σύνδεση με τον διακομιστή. Τα ταξίδια σας είναι ασφαλή — δοκιμάστε ξανά.', + 'dashboard.retry': 'Δοκιμάστε ξανά', 'dashboard.toast.created': 'Ταξίδι δημιουργήθηκε επιτυχώς!', 'dashboard.toast.createError': 'Αποτυχία δημιουργίας ταξιδιού', 'dashboard.toast.updated': 'Ταξίδι ενημερώθηκε!', diff --git a/shared/src/i18n/hu/dashboard.ts b/shared/src/i18n/hu/dashboard.ts index 56a810b3..31092a19 100644 --- a/shared/src/i18n/hu/dashboard.ts +++ b/shared/src/i18n/hu/dashboard.ts @@ -43,6 +43,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Múlt', 'dashboard.status.daysLeft': 'Még {count} nap', 'dashboard.toast.loadError': 'Nem sikerült betölteni az utazásokat', + 'dashboard.loadErrorBanner': 'Nem sikerült elérni a kiszolgálót. Az utazásaid biztonságban vannak — kérlek, próbáld újra.', + 'dashboard.retry': 'Újra', 'dashboard.toast.created': 'Utazás sikeresen létrehozva!', 'dashboard.toast.createError': 'Nem sikerült létrehozni', 'dashboard.toast.updated': 'Utazás frissítve!', diff --git a/shared/src/i18n/id/dashboard.ts b/shared/src/i18n/id/dashboard.ts index 358fe7dd..11ae1d8e 100644 --- a/shared/src/i18n/id/dashboard.ts +++ b/shared/src/i18n/id/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Sudah lewat', 'dashboard.status.daysLeft': '{count} hari lagi', 'dashboard.toast.loadError': 'Gagal memuat perjalanan', + 'dashboard.loadErrorBanner': 'Tidak dapat terhubung ke server. Perjalananmu aman — silakan coba lagi.', + 'dashboard.retry': 'Coba lagi', 'dashboard.toast.created': 'Perjalanan berhasil dibuat!', 'dashboard.toast.createError': 'Gagal membuat perjalanan', 'dashboard.toast.updated': 'Perjalanan diperbarui!', diff --git a/shared/src/i18n/it/dashboard.ts b/shared/src/i18n/it/dashboard.ts index b830df59..75a1cf9d 100644 --- a/shared/src/i18n/it/dashboard.ts +++ b/shared/src/i18n/it/dashboard.ts @@ -42,6 +42,9 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Passato', 'dashboard.status.daysLeft': '-{count} giorni', 'dashboard.toast.loadError': 'Impossibile caricare i viaggi', + 'dashboard.loadErrorBanner': + "Impossibile raggiungere il server. I tuoi viaggi sono al sicuro — riprova.", + 'dashboard.retry': 'Riprova', 'dashboard.toast.created': 'Viaggio creato con successo!', 'dashboard.toast.createError': 'Impossibile creare il viaggio', 'dashboard.toast.updated': 'Viaggio aggiornato!', diff --git a/shared/src/i18n/ja/dashboard.ts b/shared/src/i18n/ja/dashboard.ts index 07e0c3b9..f2af5457 100644 --- a/shared/src/i18n/ja/dashboard.ts +++ b/shared/src/i18n/ja/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': '過去', 'dashboard.status.daysLeft': '残り{count}日', 'dashboard.toast.loadError': '旅行の読み込みに失敗しました', + 'dashboard.loadErrorBanner': 'サーバーに接続できませんでした。旅行のデータは安全に保存されています — もう一度お試しください。', + 'dashboard.retry': '再試行', 'dashboard.toast.created': '旅行を作成しました!', 'dashboard.toast.createError': '旅行の作成に失敗しました', 'dashboard.toast.updated': '旅行を更新しました!', diff --git a/shared/src/i18n/ko/dashboard.ts b/shared/src/i18n/ko/dashboard.ts index e47c22c1..28049626 100644 --- a/shared/src/i18n/ko/dashboard.ts +++ b/shared/src/i18n/ko/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': '지난 여행', 'dashboard.status.daysLeft': '{count}일 남음', 'dashboard.toast.loadError': '여행 불러오기 실패', + 'dashboard.loadErrorBanner': '서버에 연결할 수 없습니다. 여행 정보는 안전하게 보관되어 있으니 잠시 후 다시 시도해 주세요.', + 'dashboard.retry': '다시 시도', 'dashboard.toast.created': '여행이 생성되었습니다!', 'dashboard.toast.createError': '여행 생성 실패', 'dashboard.toast.updated': '여행이 업데이트되었습니다!', diff --git a/shared/src/i18n/nl/dashboard.ts b/shared/src/i18n/nl/dashboard.ts index 4d93802e..bb1983f9 100644 --- a/shared/src/i18n/nl/dashboard.ts +++ b/shared/src/i18n/nl/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Afgelopen', 'dashboard.status.daysLeft': 'nog {count} dagen', 'dashboard.toast.loadError': 'Reizen laden mislukt', + 'dashboard.loadErrorBanner': 'De server is niet bereikbaar. Je reizen zijn veilig — probeer het opnieuw.', + 'dashboard.retry': 'Opnieuw proberen', 'dashboard.toast.created': 'Reis aangemaakt!', 'dashboard.toast.createError': 'Reis aanmaken mislukt', 'dashboard.toast.updated': 'Reis bijgewerkt!', diff --git a/shared/src/i18n/pl/dashboard.ts b/shared/src/i18n/pl/dashboard.ts index e61e278b..96b790af 100644 --- a/shared/src/i18n/pl/dashboard.ts +++ b/shared/src/i18n/pl/dashboard.ts @@ -39,6 +39,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Zakończona', 'dashboard.status.daysLeft': '{count} dni do końca', 'dashboard.toast.loadError': 'Nie udało się załadować podróży', + 'dashboard.loadErrorBanner': 'Nie udało się połączyć z serwerem. Twoje podróże są bezpieczne — spróbuj ponownie.', + 'dashboard.retry': 'Spróbuj ponownie', 'dashboard.toast.created': 'Podróż została utworzona pomyślnie!', 'dashboard.toast.createError': 'Nie udało się utworzyć podróży', 'dashboard.toast.updated': 'Podróż została zaktualizowana!', diff --git a/shared/src/i18n/ru/dashboard.ts b/shared/src/i18n/ru/dashboard.ts index 18a9c3ad..012ce02f 100644 --- a/shared/src/i18n/ru/dashboard.ts +++ b/shared/src/i18n/ru/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Прошло', 'dashboard.status.daysLeft': 'осталось {count} дн.', 'dashboard.toast.loadError': 'Не удалось загрузить поездки', + 'dashboard.loadErrorBanner': 'Не удалось подключиться к серверу. Ваши поездки в безопасности — попробуйте снова.', + 'dashboard.retry': 'Повторить', 'dashboard.toast.created': 'Поездка создана!', 'dashboard.toast.createError': 'Не удалось создать поездку', 'dashboard.toast.updated': 'Поездка обновлена!', diff --git a/shared/src/i18n/tr/dashboard.ts b/shared/src/i18n/tr/dashboard.ts index 70c66381..d0dea679 100644 --- a/shared/src/i18n/tr/dashboard.ts +++ b/shared/src/i18n/tr/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Geçmiş', 'dashboard.status.daysLeft': '{count} gün kaldı', 'dashboard.toast.loadError': 'Seyahatler yüklenemedi', + 'dashboard.loadErrorBanner': 'Sunucuya ulaşılamadı. Seyahatleriniz güvende — lütfen tekrar deneyin.', + 'dashboard.retry': 'Tekrar dene', 'dashboard.toast.created': 'Seyahat oluşturuldu!', 'dashboard.toast.createError': 'Seyahat oluşturulamadı', 'dashboard.toast.updated': 'Seyahat güncellendi!', diff --git a/shared/src/i18n/uk/dashboard.ts b/shared/src/i18n/uk/dashboard.ts index 8583e09f..54aabc92 100644 --- a/shared/src/i18n/uk/dashboard.ts +++ b/shared/src/i18n/uk/dashboard.ts @@ -42,6 +42,9 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': 'Минуло', 'dashboard.status.daysLeft': 'залишилось {count} дн.', 'dashboard.toast.loadError': 'Не вдалося завантажити поїздки', + 'dashboard.loadErrorBanner': + "Не вдалося з'єднатися із сервером. Ваші поїздки в безпеці — будь ласка, спробуйте ще раз.", + 'dashboard.retry': 'Спробувати ще раз', 'dashboard.toast.created': 'Поїздка створена!', 'dashboard.toast.createError': 'Не вдалося створити поїздку', 'dashboard.toast.updated': 'Поїздка оновлена!', diff --git a/shared/src/i18n/zh-TW/dashboard.ts b/shared/src/i18n/zh-TW/dashboard.ts index f0077fba..ed47c203 100644 --- a/shared/src/i18n/zh-TW/dashboard.ts +++ b/shared/src/i18n/zh-TW/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': '已結束', 'dashboard.status.daysLeft': '還剩 {count} 天', 'dashboard.toast.loadError': '載入旅行失敗', + 'dashboard.loadErrorBanner': '無法連線到伺服器。你的旅行資料安全無虞——請稍後再試。', + 'dashboard.retry': '重試', 'dashboard.toast.created': '旅行建立成功!', 'dashboard.toast.createError': '建立旅行失敗', 'dashboard.toast.updated': '旅行已更新!', diff --git a/shared/src/i18n/zh/dashboard.ts b/shared/src/i18n/zh/dashboard.ts index 88296680..3b94f12c 100644 --- a/shared/src/i18n/zh/dashboard.ts +++ b/shared/src/i18n/zh/dashboard.ts @@ -42,6 +42,8 @@ const dashboard: TranslationStrings = { 'dashboard.status.past': '已结束', 'dashboard.status.daysLeft': '还剩 {count} 天', 'dashboard.toast.loadError': '加载旅行失败', + 'dashboard.loadErrorBanner': '无法连接到服务器。你的旅行数据安然无恙——请稍后重试。', + 'dashboard.retry': '重试', 'dashboard.toast.created': '旅行创建成功!', 'dashboard.toast.createError': '创建旅行失败', 'dashboard.toast.updated': '旅行已更新!',