Show clear offline messages for OWM weather and AI summaries.
Users see localized feedback when OpenWeatherMap or travel-day summary features are used without connectivity, instead of generic API errors. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -152,6 +152,7 @@ export default function LiveLogView({
|
|||||||
const [modal, setModal] = useState<LiveModal>('none')
|
const [modal, setModal] = useState<LiveModal>('none')
|
||||||
const [weatherExpanded, setWeatherExpanded] = useState(false)
|
const [weatherExpanded, setWeatherExpanded] = useState(false)
|
||||||
const [weatherOwmLoading, setWeatherOwmLoading] = useState(false)
|
const [weatherOwmLoading, setWeatherOwmLoading] = useState(false)
|
||||||
|
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||||
const [commentText, setCommentText] = useState('')
|
const [commentText, setCommentText] = useState('')
|
||||||
const [valueInput, setValueInput] = useState('')
|
const [valueInput, setValueInput] = useState('')
|
||||||
const [valueInputSecondary, setValueInputSecondary] = useState('')
|
const [valueInputSecondary, setValueInputSecondary] = useState('')
|
||||||
@@ -269,6 +270,17 @@ export default function LiveLogView({
|
|||||||
}
|
}
|
||||||
}, [logbookId, applyLoadedEntry, t])
|
}, [logbookId, applyLoadedEntry, t])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOnline = () => setIsOnline(true)
|
||||||
|
const handleOffline = () => setIsOnline(false)
|
||||||
|
window.addEventListener('online', handleOnline)
|
||||||
|
window.addEventListener('offline', handleOffline)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', handleOnline)
|
||||||
|
window.removeEventListener('offline', handleOffline)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void runInit()
|
void runInit()
|
||||||
return () => {
|
return () => {
|
||||||
@@ -503,6 +515,10 @@ export default function LiveLogView({
|
|||||||
|
|
||||||
const handleFetchOwmWeather = () => {
|
const handleFetchOwmWeather = () => {
|
||||||
if (!entryId || busy || weatherOwmLoading) return
|
if (!entryId || busy || weatherOwmLoading) return
|
||||||
|
if (!isOnline) {
|
||||||
|
void showAlert(t('logs.weather_offline'), t('logs.live_weather_owm_btn'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const position = getLastPositionFixWithin(
|
const position = getLastPositionFixWithin(
|
||||||
events,
|
events,
|
||||||
@@ -533,6 +549,10 @@ export default function LiveLogView({
|
|||||||
{ analyticsSource: 'live_log' }
|
{ analyticsSource: 'live_log' }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof WeatherApiError && err.code === 'OFFLINE') {
|
||||||
|
void showAlert(t('logs.weather_offline'), t('logs.live_weather_owm_btn'))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
||||||
void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn'))
|
void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn'))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ export default function LogEntryEditor({
|
|||||||
}, [logbookId])
|
}, [logbookId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canSignSkipper || readOnly) {
|
if (!canSignSkipper || readOnly || !isOnline) {
|
||||||
setAiSummaryRemaining(null)
|
setAiSummaryRemaining(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -553,7 +553,7 @@ export default function LogEntryEditor({
|
|||||||
console.warn('Failed to load AI summary usage:', err)
|
console.warn('Failed to load AI summary usage:', err)
|
||||||
})
|
})
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [canSignSkipper, readOnly, logbookId, entryId])
|
}, [canSignSkipper, readOnly, isOnline, logbookId, entryId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const seq = ++entryHashSeqRef.current
|
const seq = ++entryHashSeqRef.current
|
||||||
@@ -986,6 +986,10 @@ export default function LogEntryEditor({
|
|||||||
showAlert('GPS capturing failed, and no location name is entered in "Ort / Hafen" or "Start-Hafen" to look up coordinates.')
|
showAlert('GPS capturing failed, and no location name is entered in "Ort / Hafen" or "Start-Hafen" to look up coordinates.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!isOnline) {
|
||||||
|
showAlert(t('logs.weather_offline'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchOpenWeatherCurrent(
|
const data = await fetchOpenWeatherCurrent(
|
||||||
@@ -999,6 +1003,10 @@ export default function LogEntryEditor({
|
|||||||
showAlert(`Coordinates loaded for "${locationQuery}" via OpenWeatherMap.`)
|
showAlert(`Coordinates loaded for "${locationQuery}" via OpenWeatherMap.`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof WeatherApiError && e.code === 'OFFLINE') {
|
||||||
|
showAlert(t('logs.weather_offline'))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (e instanceof WeatherApiError && e.code === 'NO_KEY') {
|
if (e instanceof WeatherApiError && e.code === 'NO_KEY') {
|
||||||
showAlert(t('settings.no_key'))
|
showAlert(t('settings.no_key'))
|
||||||
return
|
return
|
||||||
@@ -1025,6 +1033,11 @@ export default function LogEntryEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchWeather = async () => {
|
const handleFetchWeather = async () => {
|
||||||
|
if (!isOnline) {
|
||||||
|
showAlert(t('logs.weather_offline'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const localToday = new Date()
|
const localToday = new Date()
|
||||||
const todayStr = `${localToday.getFullYear()}-${String(localToday.getMonth() + 1).padStart(2, '0')}-${String(localToday.getDate()).padStart(2, '0')}`
|
const todayStr = `${localToday.getFullYear()}-${String(localToday.getMonth() + 1).padStart(2, '0')}-${String(localToday.getDate()).padStart(2, '0')}`
|
||||||
|
|
||||||
@@ -1066,6 +1079,10 @@ export default function LogEntryEditor({
|
|||||||
|
|
||||||
showAlert(t('settings.weather_success'))
|
showAlert(t('settings.weather_success'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof WeatherApiError && err.code === 'OFFLINE') {
|
||||||
|
showAlert(t('logs.weather_offline'))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
if (err instanceof WeatherApiError && err.code === 'NO_KEY') {
|
||||||
showAlert(t('settings.no_key'))
|
showAlert(t('settings.no_key'))
|
||||||
return
|
return
|
||||||
@@ -1079,6 +1096,10 @@ export default function LogEntryEditor({
|
|||||||
|
|
||||||
const handleGenerateAiSummary = async () => {
|
const handleGenerateAiSummary = async () => {
|
||||||
if (!canSignSkipper || readOnly || aiSummaryLoading) return
|
if (!canSignSkipper || readOnly || aiSummaryLoading) return
|
||||||
|
if (!isOnline) {
|
||||||
|
setAiSummaryError(t('logs.ai_summary_offline'))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (aiSummaryRemaining === 0) {
|
if (aiSummaryRemaining === 0) {
|
||||||
setAiSummaryError(t('logs.ai_summary_error_rate_limited'))
|
setAiSummaryError(t('logs.ai_summary_error_rate_limited'))
|
||||||
return
|
return
|
||||||
@@ -1135,7 +1156,9 @@ export default function LogEntryEditor({
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TravelDaySummaryApiError) {
|
if (err instanceof TravelDaySummaryApiError) {
|
||||||
if (err.code === 'NO_KEY') {
|
if (err.code === 'OFFLINE') {
|
||||||
|
setAiSummaryError(t('logs.ai_summary_offline'))
|
||||||
|
} else if (err.code === 'NO_KEY') {
|
||||||
setAiSummaryError(t('logs.ai_summary_error_no_key'))
|
setAiSummaryError(t('logs.ai_summary_error_no_key'))
|
||||||
} else if (err.code === 'RATE_LIMITED') {
|
} else if (err.code === 'RATE_LIMITED') {
|
||||||
setAiSummaryError(t('logs.ai_summary_error_rate_limited'))
|
setAiSummaryError(t('logs.ai_summary_error_rate_limited'))
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
"event_remarks": "Bemærkninger / hændelser",
|
"event_remarks": "Bemærkninger / hændelser",
|
||||||
"gps_btn": "Hent GPS-koordinater",
|
"gps_btn": "Hent GPS-koordinater",
|
||||||
"weather_btn": "OpenWeatherMap Kald vejret op",
|
"weather_btn": "OpenWeatherMap Kald vejret op",
|
||||||
|
"weather_offline": "OpenWeatherMap kræver internetforbindelse. Du er offline lige nu.",
|
||||||
"event_wind_pressure": "Lufttryk (hPa)",
|
"event_wind_pressure": "Lufttryk (hPa)",
|
||||||
"event_heel": "Krængning (°)",
|
"event_heel": "Krængning (°)",
|
||||||
"event_sails": "Sejlhåndtering/motor",
|
"event_sails": "Sejlhåndtering/motor",
|
||||||
@@ -381,6 +382,7 @@
|
|||||||
"ai_summary_error_no_key": "Ingen OpenRouter API-nøgle konfigureret på serveren.",
|
"ai_summary_error_no_key": "Ingen OpenRouter API-nøgle konfigureret på serveren.",
|
||||||
"ai_summary_error_rate_limited": "Maksimalt antal genereringer nået for denne rejsedag.",
|
"ai_summary_error_rate_limited": "Maksimalt antal genereringer nået for denne rejsedag.",
|
||||||
"ai_summary_error_forbidden": "Kun skipperen må generere AI-resuméer.",
|
"ai_summary_error_forbidden": "Kun skipperen må generere AI-resuméer.",
|
||||||
|
"ai_summary_offline": "AI-resumé kræver internetforbindelse. Du er offline lige nu.",
|
||||||
"photos_title": "Vedhæftede billeder (E2E-krypteret)",
|
"photos_title": "Vedhæftede billeder (E2E-krypteret)",
|
||||||
"photo_caption_label": "Fotobeskrivelse/etiket (valgfrit)",
|
"photo_caption_label": "Fotobeskrivelse/etiket (valgfrit)",
|
||||||
"photo_caption_placeholder": "f.eks. at sætte sejl nær indsejlingen til havnen",
|
"photo_caption_placeholder": "f.eks. at sætte sejl nær indsejlingen til havnen",
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
"event_remarks": "Bemerkungen / Vorkommnisse",
|
"event_remarks": "Bemerkungen / Vorkommnisse",
|
||||||
"gps_btn": "GPS-Koordinaten abrufen",
|
"gps_btn": "GPS-Koordinaten abrufen",
|
||||||
"weather_btn": "OpenWeatherMap Wetter abrufen",
|
"weather_btn": "OpenWeatherMap Wetter abrufen",
|
||||||
|
"weather_offline": "OpenWeatherMap erfordert eine Internetverbindung. Du bist derzeit offline.",
|
||||||
"event_wind_pressure": "Luftdruck (hPa)",
|
"event_wind_pressure": "Luftdruck (hPa)",
|
||||||
"event_heel": "Krängung (°)",
|
"event_heel": "Krängung (°)",
|
||||||
"event_sails": "Segelführung / Motor",
|
"event_sails": "Segelführung / Motor",
|
||||||
@@ -381,6 +382,7 @@
|
|||||||
"ai_summary_error_no_key": "Kein OpenRouter API-Schlüssel auf dem Server konfiguriert.",
|
"ai_summary_error_no_key": "Kein OpenRouter API-Schlüssel auf dem Server konfiguriert.",
|
||||||
"ai_summary_error_rate_limited": "Maximale Anzahl an Generierungen für diesen Reisetag erreicht.",
|
"ai_summary_error_rate_limited": "Maximale Anzahl an Generierungen für diesen Reisetag erreicht.",
|
||||||
"ai_summary_error_forbidden": "Nur der Skipper darf KI-Zusammenfassungen generieren.",
|
"ai_summary_error_forbidden": "Nur der Skipper darf KI-Zusammenfassungen generieren.",
|
||||||
|
"ai_summary_offline": "Die KI-Zusammenfassung erfordert eine Internetverbindung. Du bist derzeit offline.",
|
||||||
"photos_title": "Foto-Anhänge (E2E-verschlüsselt)",
|
"photos_title": "Foto-Anhänge (E2E-verschlüsselt)",
|
||||||
"photo_caption_label": "Foto-Beschreibung / Label (Optional)",
|
"photo_caption_label": "Foto-Beschreibung / Label (Optional)",
|
||||||
"photo_caption_placeholder": "z.B. Segel setzen nahe Hafeneinfahrt",
|
"photo_caption_placeholder": "z.B. Segel setzen nahe Hafeneinfahrt",
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
"event_remarks": "Remarks / Events",
|
"event_remarks": "Remarks / Events",
|
||||||
"gps_btn": "Get GPS Location",
|
"gps_btn": "Get GPS Location",
|
||||||
"weather_btn": "Fetch OpenWeatherMap Weather",
|
"weather_btn": "Fetch OpenWeatherMap Weather",
|
||||||
|
"weather_offline": "OpenWeatherMap requires an internet connection. You are currently offline.",
|
||||||
"event_wind_pressure": "Barometer (hPa)",
|
"event_wind_pressure": "Barometer (hPa)",
|
||||||
"event_heel": "Heel Angle (°)",
|
"event_heel": "Heel Angle (°)",
|
||||||
"event_sails": "Sails / Motor Status",
|
"event_sails": "Sails / Motor Status",
|
||||||
@@ -381,6 +382,7 @@
|
|||||||
"ai_summary_error_no_key": "No OpenRouter API key configured on the server.",
|
"ai_summary_error_no_key": "No OpenRouter API key configured on the server.",
|
||||||
"ai_summary_error_rate_limited": "Maximum number of generations reached for this travel day.",
|
"ai_summary_error_rate_limited": "Maximum number of generations reached for this travel day.",
|
||||||
"ai_summary_error_forbidden": "Only the skipper may generate AI summaries.",
|
"ai_summary_error_forbidden": "Only the skipper may generate AI summaries.",
|
||||||
|
"ai_summary_offline": "AI summary generation requires an internet connection. You are currently offline.",
|
||||||
"photos_title": "Photo Attachments (E2E Encrypted)",
|
"photos_title": "Photo Attachments (E2E Encrypted)",
|
||||||
"photo_caption_label": "Photo Caption / Label (Optional)",
|
"photo_caption_label": "Photo Caption / Label (Optional)",
|
||||||
"photo_caption_placeholder": "e.g. Setting sails near harbor entrance",
|
"photo_caption_placeholder": "e.g. Setting sails near harbor entrance",
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
"event_remarks": "Merknader / hendelser",
|
"event_remarks": "Merknader / hendelser",
|
||||||
"gps_btn": "Hent GPS-koordinater",
|
"gps_btn": "Hent GPS-koordinater",
|
||||||
"weather_btn": "OpenWeatherMap Ring opp været",
|
"weather_btn": "OpenWeatherMap Ring opp været",
|
||||||
|
"weather_offline": "OpenWeatherMap krever internettforbindelse. Du er frakoblet.",
|
||||||
"event_wind_pressure": "Lufttrykk (hPa)",
|
"event_wind_pressure": "Lufttrykk (hPa)",
|
||||||
"event_heel": "Helning (°)",
|
"event_heel": "Helning (°)",
|
||||||
"event_sails": "Seilhåndtering / motor",
|
"event_sails": "Seilhåndtering / motor",
|
||||||
@@ -381,6 +382,7 @@
|
|||||||
"ai_summary_error_no_key": "Ingen OpenRouter API-nøkkel konfigurert på serveren.",
|
"ai_summary_error_no_key": "Ingen OpenRouter API-nøkkel konfigurert på serveren.",
|
||||||
"ai_summary_error_rate_limited": "Maksimalt antall genereringer nådd for denne reisedagen.",
|
"ai_summary_error_rate_limited": "Maksimalt antall genereringer nådd for denne reisedagen.",
|
||||||
"ai_summary_error_forbidden": "Kun skipperen kan generere AI-sammendrag.",
|
"ai_summary_error_forbidden": "Kun skipperen kan generere AI-sammendrag.",
|
||||||
|
"ai_summary_offline": "AI-sammendrag krever internettforbindelse. Du er frakoblet.",
|
||||||
"photos_title": "Bildevedlegg (E2E-kryptert)",
|
"photos_title": "Bildevedlegg (E2E-kryptert)",
|
||||||
"photo_caption_label": "Fotobeskrivelse/etikett (valgfritt)",
|
"photo_caption_label": "Fotobeskrivelse/etikett (valgfritt)",
|
||||||
"photo_caption_placeholder": "f.eks. å sette seil nær innseilingen til havnen",
|
"photo_caption_placeholder": "f.eks. å sette seil nær innseilingen til havnen",
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
"event_remarks": "Anmärkningar / incidenter",
|
"event_remarks": "Anmärkningar / incidenter",
|
||||||
"gps_btn": "Hämta GPS-koordinater",
|
"gps_btn": "Hämta GPS-koordinater",
|
||||||
"weather_btn": "OpenWeatherMap Ring upp väder",
|
"weather_btn": "OpenWeatherMap Ring upp väder",
|
||||||
|
"weather_offline": "OpenWeatherMap kräver internetanslutning. Du är offline.",
|
||||||
"event_wind_pressure": "Lufttryck (hPa)",
|
"event_wind_pressure": "Lufttryck (hPa)",
|
||||||
"event_heel": "Krängning (°)",
|
"event_heel": "Krängning (°)",
|
||||||
"event_sails": "Segelhantering / motor",
|
"event_sails": "Segelhantering / motor",
|
||||||
@@ -381,6 +382,7 @@
|
|||||||
"ai_summary_error_no_key": "Ingen OpenRouter API-nyckel konfigurerad på servern.",
|
"ai_summary_error_no_key": "Ingen OpenRouter API-nyckel konfigurerad på servern.",
|
||||||
"ai_summary_error_rate_limited": "Maximalt antal genereringar nått för denna resedag.",
|
"ai_summary_error_rate_limited": "Maximalt antal genereringar nått för denna resedag.",
|
||||||
"ai_summary_error_forbidden": "Endast skepparen får generera AI-sammanfattningar.",
|
"ai_summary_error_forbidden": "Endast skepparen får generera AI-sammanfattningar.",
|
||||||
|
"ai_summary_offline": "AI-sammanfattning kräver internetanslutning. Du är offline.",
|
||||||
"photos_title": "Fotobilagor (E2E-krypterade)",
|
"photos_title": "Fotobilagor (E2E-krypterade)",
|
||||||
"photo_caption_label": "Fotobeskrivning/etikett (valfritt)",
|
"photo_caption_label": "Fotobeskrivning/etikett (valfritt)",
|
||||||
"photo_caption_placeholder": "t.ex. sätta segel nära hamninloppet",
|
"photo_caption_placeholder": "t.ex. sätta segel nära hamninloppet",
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayl
|
|||||||
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
|
||||||
|
|
||||||
export class TravelDaySummaryApiError extends Error {
|
export class TravelDaySummaryApiError extends Error {
|
||||||
code: 'NO_KEY' | 'FORBIDDEN' | 'RATE_LIMITED' | 'REQUEST_FAILED'
|
code: 'NO_KEY' | 'FORBIDDEN' | 'RATE_LIMITED' | 'OFFLINE' | 'REQUEST_FAILED'
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
code: 'NO_KEY' | 'FORBIDDEN' | 'RATE_LIMITED' | 'REQUEST_FAILED' = 'REQUEST_FAILED'
|
code: 'NO_KEY' | 'FORBIDDEN' | 'RATE_LIMITED' | 'OFFLINE' | 'REQUEST_FAILED' = 'REQUEST_FAILED'
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'TravelDaySummaryApiError'
|
this.name = 'TravelDaySummaryApiError'
|
||||||
@@ -146,6 +146,10 @@ export async function generateTravelDaySummary(params: {
|
|||||||
language: string
|
language: string
|
||||||
context: TravelDaySummaryContext
|
context: TravelDaySummaryContext
|
||||||
}): Promise<{ summary: string; remainingAttempts: number; maxAttempts: number }> {
|
}): Promise<{ summary: string; remainingAttempts: number; maxAttempts: number }> {
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
throw new TravelDaySummaryApiError('Offline', 'OFFLINE')
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = window.setTimeout(() => controller.abort(), SUMMARY_FETCH_TIMEOUT_MS)
|
const timeoutId = window.setTimeout(() => controller.abort(), SUMMARY_FETCH_TIMEOUT_MS)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ describe('fetchOpenWeatherCurrent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('throws OFFLINE when navigator.onLine is false', async () => {
|
||||||
|
vi.stubGlobal('navigator', { ...navigator, onLine: false })
|
||||||
|
|
||||||
|
const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js')
|
||||||
|
const err = await fetchOpenWeatherCurrent({ lat: '54', lon: '10' }).catch((e) => e)
|
||||||
|
expect(err).toBeInstanceOf(WeatherApiError)
|
||||||
|
expect((err as InstanceType<typeof WeatherApiError>).code).toBe('OFFLINE')
|
||||||
|
|
||||||
|
expect(apiFetch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('does not track when the API request fails', async () => {
|
it('does not track when the API request fails', async () => {
|
||||||
apiFetch.mockResolvedValue({
|
apiFetch.mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
} from './analytics.js'
|
} from './analytics.js'
|
||||||
|
|
||||||
export class WeatherApiError extends Error {
|
export class WeatherApiError extends Error {
|
||||||
code: 'NO_KEY' | 'REQUEST_FAILED'
|
code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED'
|
||||||
|
|
||||||
constructor(message: string, code: 'NO_KEY' | 'REQUEST_FAILED' = 'REQUEST_FAILED') {
|
constructor(message: string, code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED' = 'REQUEST_FAILED') {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'WeatherApiError'
|
this.name = 'WeatherApiError'
|
||||||
this.code = code
|
this.code = code
|
||||||
@@ -26,6 +26,10 @@ export async function fetchOpenWeatherCurrent(
|
|||||||
},
|
},
|
||||||
options?: { analyticsSource: OwmAnalyticsSource }
|
options?: { analyticsSource: OwmAnalyticsSource }
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
throw new WeatherApiError('Offline', 'OFFLINE')
|
||||||
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
|
|
||||||
if (params.lat && params.lon) {
|
if (params.lat && params.lon) {
|
||||||
|
|||||||
Reference in New Issue
Block a user