init
This commit is contained in:
207
frontend/lib/api.ts
Normal file
207
frontend/lib/api.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
CurrentWeatherResponse,
|
||||
ForecastResponse,
|
||||
HistoricalResponse,
|
||||
AssessmentResponse,
|
||||
Thresholds,
|
||||
APIError,
|
||||
} from './types'
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1'
|
||||
|
||||
class APIClient {
|
||||
private baseURL: string
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${this.baseURL}${endpoint}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error: APIError = await response.json().catch(() => ({
|
||||
error: 'An error occurred',
|
||||
detail: response.statusText,
|
||||
}))
|
||||
throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
// Unwrap {success, data} response if present
|
||||
return (json.data || json) as T
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('An unexpected error occurred')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current weather conditions and flyability assessment
|
||||
*/
|
||||
async getCurrentWeather(
|
||||
lat?: number,
|
||||
lon?: number
|
||||
): Promise<CurrentWeatherResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (lat !== undefined) params.append('lat', lat.toString())
|
||||
if (lon !== undefined) params.append('lon', lon.toString())
|
||||
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
const data = await this.request<any>(`/weather/current${query}`)
|
||||
|
||||
// Transform backend response to frontend format
|
||||
return {
|
||||
location: data.location || { lat: 0, lon: 0 },
|
||||
current: {
|
||||
timestamp: data.current?.time || data.current?.timestamp,
|
||||
wind_speed: data.current?.windSpeed || data.current?.wind_speed || 0,
|
||||
wind_direction: data.current?.windDirection || data.current?.wind_direction || 0,
|
||||
wind_gust: data.current?.windGust || data.current?.wind_gust || 0,
|
||||
temperature: data.current?.temperature || 0,
|
||||
cloud_cover: data.current?.cloudCover || data.current?.cloud_cover || 0,
|
||||
precipitation: data.current?.precipitation || 0,
|
||||
visibility: data.current?.visibility || 0,
|
||||
pressure: data.current?.pressure || 0,
|
||||
humidity: data.current?.humidity || 0,
|
||||
},
|
||||
assessment: {
|
||||
is_flyable: data.assessment?.FlyableNow || data.assessment?.is_flyable || false,
|
||||
reasons: data.assessment?.Reason ? [data.assessment.Reason] : data.assessment?.reasons || [],
|
||||
score: data.assessment?.score || 0,
|
||||
},
|
||||
last_updated: data.timestamp || data.last_updated || new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weather forecast for the next 7 days
|
||||
*/
|
||||
async getForecast(
|
||||
lat?: number,
|
||||
lon?: number
|
||||
): Promise<ForecastResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (lat !== undefined) params.append('lat', lat.toString())
|
||||
if (lon !== undefined) params.append('lon', lon.toString())
|
||||
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
const data = await this.request<any>(`/weather/forecast${query}`)
|
||||
|
||||
// Transform backend response to frontend format
|
||||
return {
|
||||
location: data.location || { lat: 0, lon: 0 },
|
||||
forecast: (data.forecast || []).map((point: any) => ({
|
||||
timestamp: point.Time || point.timestamp,
|
||||
wind_speed: point.WindSpeedMPH || point.wind_speed || 0,
|
||||
wind_direction: point.WindDirection || point.wind_direction || 0,
|
||||
wind_gust: point.WindGustMPH || point.wind_gust || 0,
|
||||
temperature: point.temperature || 0,
|
||||
cloud_cover: point.cloud_cover || 0,
|
||||
precipitation: point.precipitation || 0,
|
||||
visibility: point.visibility || 0,
|
||||
pressure: point.pressure || 0,
|
||||
humidity: point.humidity || 0,
|
||||
})),
|
||||
flyable_windows: (data.flyableWindows || data.flyable_windows || []).map((win: any) => ({
|
||||
start: win.start,
|
||||
end: win.end,
|
||||
duration_hours: win.durationHours || win.duration_hours || 0,
|
||||
avg_conditions: {
|
||||
wind_speed: win.avgConditions?.windSpeed || win.avg_conditions?.wind_speed || 0,
|
||||
wind_gust: win.avgConditions?.windGust || win.avg_conditions?.wind_gust || 0,
|
||||
temperature: win.avgConditions?.temperature || win.avg_conditions?.temperature || 0,
|
||||
cloud_cover: win.avgConditions?.cloudCover || win.avg_conditions?.cloud_cover || 0,
|
||||
},
|
||||
})),
|
||||
generated_at: data.generated || data.generated_at || new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get historical weather data for a specific date
|
||||
* @param date - Date in YYYY-MM-DD format
|
||||
*/
|
||||
async getHistorical(
|
||||
date: string,
|
||||
lat?: number,
|
||||
lon?: number
|
||||
): Promise<HistoricalResponse> {
|
||||
const params = new URLSearchParams({ date })
|
||||
if (lat !== undefined) params.append('lat', lat.toString())
|
||||
if (lon !== undefined) params.append('lon', lon.toString())
|
||||
|
||||
const data = await this.request<any>(`/weather/historical?${params.toString()}`)
|
||||
|
||||
// Transform backend response to frontend format
|
||||
return {
|
||||
location: data.location || { lat: 0, lon: 0 },
|
||||
date: data.date || date,
|
||||
data: (data.data || []).map((point: any) => ({
|
||||
timestamp: point.Time || point.timestamp,
|
||||
wind_speed: point.WindSpeedMPH || point.wind_speed || 0,
|
||||
wind_direction: point.WindDirection || point.wind_direction || 0,
|
||||
wind_gust: point.WindGustMPH || point.wind_gust || 0,
|
||||
temperature: point.temperature || 0,
|
||||
cloud_cover: point.cloud_cover || 0,
|
||||
precipitation: point.precipitation || 0,
|
||||
visibility: point.visibility || 0,
|
||||
pressure: point.pressure || 0,
|
||||
humidity: point.humidity || 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess current conditions with custom thresholds
|
||||
*/
|
||||
async assessWithThresholds(
|
||||
thresholds: Thresholds,
|
||||
lat?: number,
|
||||
lon?: number
|
||||
): Promise<AssessmentResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (lat !== undefined) params.append('lat', lat.toString())
|
||||
if (lon !== undefined) params.append('lon', lon.toString())
|
||||
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
|
||||
return this.request<AssessmentResponse>(`/weather/assess${query}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ thresholds }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiClient = new APIClient(API_BASE_URL)
|
||||
|
||||
// Export individual functions for convenience
|
||||
export const getCurrentWeather = (lat?: number, lon?: number) =>
|
||||
apiClient.getCurrentWeather(lat, lon)
|
||||
|
||||
export const getForecast = (lat?: number, lon?: number) =>
|
||||
apiClient.getForecast(lat, lon)
|
||||
|
||||
export const getHistorical = (date: string, lat?: number, lon?: number) =>
|
||||
apiClient.getHistorical(date, lat, lon)
|
||||
|
||||
export const assessWithThresholds = (
|
||||
thresholds: Thresholds,
|
||||
lat?: number,
|
||||
lon?: number
|
||||
) => apiClient.assessWithThresholds(thresholds, lat, lon)
|
||||
Reference in New Issue
Block a user