diff --git a/weather-service/src/airports/model.rs b/weather-service/src/airports/model.rs index 284b6fb..42de092 100644 --- a/weather-service/src/airports/model.rs +++ b/weather-service/src/airports/model.rs @@ -2,6 +2,7 @@ use crate::db; use crate::error_handler::CustomError; use crate::schema::airports; use diesel::prelude::*; +use log::trace; use postgis_diesel::types::*; use postgis_diesel::functions::*; use serde::{Deserialize, Serialize}; @@ -23,7 +24,8 @@ pub struct Airport { pub point: Point } -#[derive(Serialize, Deserialize, Queryable)] +#[derive(Serialize, Deserialize, Queryable, QueryableByName)] +#[diesel(table_name = airports)] pub struct Airports { pub icao: String, pub id: i32, @@ -41,32 +43,28 @@ pub struct Airports { } impl Airports { - pub fn find_all(bounds: Option>, category: Option, limit: i32, page: i32) -> Result, CustomError> { + pub fn get_all(bounds: Option>, category: Option, filter: Option, limit: i32, page: i32) -> Result, CustomError> { let mut conn = db::connection()?; - let airports; - if let Some(category) = category { - airports = airports::table - .limit(limit as i64) - .filter(airports::id.gt(page * limit).and(match bounds { - Some(b) => st_contains(b, airports::point), - None => { - let polygon: Polygon = Polygon::new(Some(4326)); - st_contains(polygon, airports::point) - } - }).and(airports::category.eq(category))).load::(&mut conn)?; - } else { - airports = airports::table - .order(airports::category.asc()) - .limit(limit as i64) - .filter(airports::id.gt(page * limit).and(match bounds { - Some(b) => st_contains(b, airports::point), - None => { - let polygon: Polygon = Polygon::new(Some(4326)); - st_contains(polygon, airports::point) - } - })) - .load::(&mut conn)?; - } + let mut query = airports::table + .limit(limit as i64) + .into_boxed(); + query = query.filter(airports::id.gt(page * limit)); + + if let Some(bounds) = bounds { + query = query.filter(st_contains(bounds, airports::point)); + } + if let Some(category) = category { + query = query.filter(airports::category.eq(category)); + } + if let Some(filter) = filter { + query = query.filter(airports::icao + .ilike(format!("%{}%", filter)) + .or(airports::full_name.ilike(format!("%{}%", filter))) + ) + } + let debug = diesel::debug_query::(&query); + trace!("{}", debug); + let airports: Vec = query.order(airports::category.asc()).load::(&mut conn)?; Ok(airports) } diff --git a/weather-service/src/airports/routes.rs b/weather-service/src/airports/routes.rs index b8b3d00..2651075 100644 --- a/weather-service/src/airports/routes.rs +++ b/weather-service/src/airports/routes.rs @@ -1,63 +1,115 @@ -use crate::{airports::{Airport, Airports}, db}; +use crate::{airports::{Airport, Airports}, db::{self, Metadata}}; use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest}; -use log::error; +use log::{error, warn}; use postgis_diesel::types::{Polygon, Point}; use serde::{Serialize, Deserialize}; -use serde_json::json; #[derive(Debug, Serialize, Deserialize)] -struct FindAllParams { - ne_lat: f64, - ne_lon: f64, - sw_lat: f64, - sw_lon: f64, +struct GetAllParameters { + filter: Option, + bounds: Option, category: Option, limit: i32, page: i32 } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -struct Coordinate { - lon: f64, - lat: f64 +#[get("/import")] +async fn import() -> HttpResponse { + db::import_data(); + HttpResponse::Ok().body({}) } -#[get("/setup")] -async fn setup() -> HttpResponse { - db::import_data(); - HttpResponse::Ok().finish() +#[derive(Serialize, Deserialize)] +pub struct AirportsResponse { + pub data: Vec, + pub meta: Metadata } #[get("/airports")] -async fn find_all(req: HttpRequest) -> HttpResponse { - let params = web::Query::::from_query(req.query_string()).unwrap(); - let mut polygon: Polygon = Polygon::new(Some(4326)); - polygon.add_point(Point { x: params.sw_lon, y: params.sw_lat, srid: Some(4326) }); - polygon.add_point(Point { x: params.ne_lon, y: params.sw_lat, srid: Some(4326) }); - polygon.add_point(Point { x: params.ne_lon, y: params.ne_lat, srid: Some(4326) }); - polygon.add_point(Point { x: params.sw_lon, y: params.ne_lat, srid: Some(4326) }); - polygon.add_point(Point { x: params.sw_lon, y: params.sw_lat, srid: Some(4326) }); +async fn get_all(req: HttpRequest) -> HttpResponse { + let params = web::Query::::from_query(req.query_string()).unwrap(); + let polygon: Option> = match ¶ms.bounds { + Some(b) => { + let bounds: Vec<&str> = b.split(",").collect(); + if bounds.len() != 4 { + warn!("Expected 4 bounds, received {}: {}", bounds.len(), b); + return HttpResponse::UnprocessableEntity().body(format!("Received {}; expected NE_LAT,NE_LON,SW_LAT,SW_LON", b)) + } + let ne_lat = match bounds[0].parse::() { + Ok(b) => b, + Err(err) => { + warn!("{}", err); + return HttpResponse::UnprocessableEntity().body(format!("{}", err)) + } + }; + let ne_lon = match bounds[1].parse::() { + Ok(b) => b, + Err(err) => { + warn!("{}", err); + return HttpResponse::UnprocessableEntity().body(format!("{}", err)) + } + }; + let sw_lat = match bounds[2].parse::() { + Ok(b) => b, + Err(err) => { + warn!("{}", err); + return HttpResponse::UnprocessableEntity().body(format!("{}", err)) + } + }; + let sw_lon = match bounds[3].parse::() { + Ok(b) => b, + Err(err) => { + warn!("{}", err); + return HttpResponse::UnprocessableEntity().body(format!("{}", err)) + } + }; + let mut polygon: Polygon = Polygon::new(Some(4326)); + polygon.add_point(Point { x: sw_lon, y: sw_lat, srid: Some(4326) }); + polygon.add_point(Point { x: ne_lon, y: sw_lat, srid: Some(4326) }); + polygon.add_point(Point { x: ne_lon, y: ne_lat, srid: Some(4326) }); + polygon.add_point(Point { x: sw_lon, y: ne_lat, srid: Some(4326) }); + polygon.add_point(Point { x: sw_lon, y: sw_lat, srid: Some(4326) }); + Some(polygon) + }, + None => None + }; let category = match ¶ms.category { Some(c) => Some(c.to_string()), None => None }; + let filter = match ¶ms.filter { + Some(f) => Some(f.to_string()), + None => None + }; - match web::block(move || Airports::find_all(Some(polygon), category, params.limit, params.page)).await.unwrap() { - Ok(a) => HttpResponse::Ok().json(a), + match web::block(move || Airports::get_all(polygon, category, filter, params.limit, params.page)).await.unwrap() { + Ok(a) => HttpResponse::Ok().json(AirportsResponse { + data: a, + meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } + }), Err(err) => { error!("{}", err); - HttpResponse::InternalServerError().finish() + err.to_http_response() } } } +#[derive(Serialize, Deserialize)] +pub struct AirportResponse { + pub data: Airports, + pub meta: Metadata +} + #[get("/airports/{icao}")] -async fn find(icao: web::Path) -> HttpResponse { +async fn get(icao: web::Path) -> HttpResponse { match Airports::find(icao.into_inner()) { - Ok(a) => HttpResponse::Ok().json(a), + Ok(a) => HttpResponse::Ok().json(AirportResponse { + data: a, + meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } + }), Err(err) => { error!("{}", err); - HttpResponse::InternalServerError().finish() + err.to_http_response() } } } @@ -65,10 +117,10 @@ async fn find(icao: web::Path) -> HttpResponse { #[post("/airports")] async fn create(airport: web::Json) -> HttpResponse { match Airports::create(airport.into_inner()) { - Ok(a) => HttpResponse::Ok().json(a), + Ok(a) => HttpResponse::Created().json(a), Err(err) => { error!("{}", err); - HttpResponse::InternalServerError().finish() + err.to_http_response() } } } @@ -79,7 +131,7 @@ async fn update(id: web::Path, airport: web::Json) -> HttpResponse Ok(a) => HttpResponse::Ok().json(a), Err(err) => { error!("{}", err); - HttpResponse::InternalServerError().finish() + err.to_http_response() } } } @@ -87,19 +139,19 @@ async fn update(id: web::Path, airport: web::Json) -> HttpResponse #[delete("/airports/{id}")] async fn delete(id: web::Path) -> HttpResponse { match Airports::delete(id.into_inner()) { - Ok(a) => HttpResponse::Ok().json(json!({ "deleted": a })), + Ok(_) => HttpResponse::NoContent().finish(), Err(err) => { error!("{}", err); - HttpResponse::InternalServerError().finish() + err.to_http_response() } } } pub fn init_routes(config: &mut web::ServiceConfig) { - config.service(find_all); - config.service(find); + config.service(get_all); + config.service(get); config.service(create); config.service(update); config.service(delete); - config.service(setup); + config.service(import); } \ No newline at end of file diff --git a/weather-service/src/db.rs b/weather-service/src/db.rs index 859a830..666549c 100644 --- a/weather-service/src/db.rs +++ b/weather-service/src/db.rs @@ -1,5 +1,6 @@ use crate::{error_handler::CustomError, airports::{Airport, Airports}}; use diesel::{r2d2::ConnectionManager, PgConnection}; +use serde::{Deserialize, Serialize}; use crate::diesel_migrations::MigrationHarness; use lazy_static::lazy_static; use log::{error, debug, info}; @@ -50,4 +51,18 @@ pub fn import_data() { }; } debug!("Import complete"); -} \ No newline at end of file +} + +#[derive(Serialize, Deserialize)] +pub struct Metadata { + pub page: i32, + pub limit: i32, + pub pages: i32, + pub total: i32, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Coordinate { + pub lon: f64, + pub lat: f64 +} diff --git a/weather-service/src/error_handler.rs b/weather-service/src/error_handler.rs index 0c3b62b..4025800 100644 --- a/weather-service/src/error_handler.rs +++ b/weather-service/src/error_handler.rs @@ -1,6 +1,7 @@ use actix_web::http::StatusCode; use actix_web::{HttpResponse, ResponseError}; use diesel::result::Error as DieselError; +use log::warn; use serde::{Deserialize, Serialize}; use serde_json::json; use std::fmt; @@ -18,6 +19,17 @@ impl CustomError { error_message, } } + + pub fn to_http_response(&self) -> HttpResponse { + let status_code = match StatusCode::from_u16(self.error_status_code) { + Ok(s) => s, + Err(err) => { + warn!("{}", err); + StatusCode::INTERNAL_SERVER_ERROR + } + }; + HttpResponse::build(status_code).body(self.error_message.to_string()) + } } impl fmt::Display for CustomError { diff --git a/weather-service/src/lib.rs.bk b/weather-service/src/lib.rs.bk deleted file mode 100644 index 59d96b2..0000000 --- a/weather-service/src/lib.rs.bk +++ /dev/null @@ -1,162 +0,0 @@ -use std::error::Error; -use std::fmt; -use log::warn; -use std::io::BufRead; -use quick_xml::{Reader, events::{Event, BytesStart}, Writer, de::Deserializer}; -use serde::Deserialize; - -pub struct Airport { - pub name: String, - pub icao: String -} - -impl Airport { - pub fn new(name: String, icao: String) -> Airport { - Airport { name, icao } - } -} - -#[derive(Debug)] -pub struct WeatherError(pub String); - -impl fmt::Display for WeatherError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Error for WeatherError {} - -#[derive(Deserialize, Debug)] -pub struct Metar { - pub raw_text: String, - pub station_id: String, - pub observation_time: String, - pub latitude: f32, - pub longitude: f32, - pub temp_c: f32, - pub dewpoint_c: f32, - pub wind_dir_degrees: i32, - pub wind_speed_kt: i32, - pub visibility_statute_mi: String, - pub altim_in_hg: f32, - pub sea_level_pressure_mb: Option, - pub quality_control_flags: Option, - pub wx_string: Option, - // pub sky_con dition: Option>, // TODO work on attributes - pub flight_category: String, - pub three_hr_pressure_tendency_mb: Option, - pub metar_type: String, - #[serde(rename = "maxT_c")] - pub max_t_c: Option, - #[serde(rename = "minT_c")] - pub min_t_c: Option, - pub precip_in: Option, - pub elevation_m: i32 -} - -#[derive(Deserialize, Debug)] -pub struct QualityControlFlags { - pub auto: Option, - pub auto_station: Option -} - -impl Metar { - pub fn parse(input: String) -> Result, WeatherError> { - if input.is_empty() { - return Err(WeatherError("Input is empty".to_string())) - } - - let mut reader = Reader::from_str(&input); - let mut buf = Vec::new(); - let mut junk_buf: Vec = Vec::new(); - - loop { - match reader.read_event_into(&mut buf) { - Err(e) => panic!("Error at position: {}: {:?}", reader.buffer_position(), e), - Ok(Event::Eof) => break, - Ok(Event::Start(e)) => { - match e.name().as_ref() { - b"METAR" => { - let metar_bytes = Metar::read_to_end_into_buffer(&mut reader, &e, &mut junk_buf).unwrap(); - let str = std::str::from_utf8(&metar_bytes).unwrap(); - let mut deserializer = Deserializer::from_str(str); - let metar = Metar::deserialize(&mut deserializer).unwrap(); - println!("{:#?}", metar); - }, - _ => () - } - }, - _ => () - } - } - - return Ok(vec![]) - } - - // https://capnfabs.net/posts/parsing-huge-xml-quickxml-rust-serde/ - pub fn read_to_end_into_buffer(reader: &mut Reader, start_tag: &BytesStart, junk_buf: &mut Vec) -> Result, quick_xml::Error> { - let mut depth = 0; - let mut output_buf: Vec = Vec::new(); - let mut w = Writer::new(&mut output_buf); - let tag_name = start_tag.name(); - w.write_event(Event::Start(start_tag.clone()))?; - loop { - junk_buf.clear(); - let event = reader.read_event_into(junk_buf)?; - w.write_event(&event)?; - - match event { - Event::Start(e) if e.name() == tag_name => depth += 1, - Event::End(e) if e.name() == tag_name => { - if depth == 0 { - return Ok(output_buf); - } - depth -= 1; - } - Event::Eof => { - panic!("EOF") - } - _ => {} - } - } - } -} - -pub struct Weather { - pub base_url: String -} - -impl Weather { - pub async fn metar(&mut self, airports: Vec) -> Vec { - let mut station_icaos: Vec<&str> = vec![]; - for station in airports.iter() { - station_icaos.push(&station.icao); - } - let station_string = station_icaos.join(","); - let url = format!("{}/metar.php?ids={}&format=xml", self.base_url, station_string); - - let metars: Vec = match reqwest::get(url).await { - Ok(r) => match r.text().await { - Ok(r) => { - match Metar::parse(r) { - Ok(m) => m, - Err(err) => { - warn!("{}", err); - vec![] - } - } - }, - Err(err) => { - warn!("Unable to parse METAR request: {}", err); - vec![] - } - }, - Err(err) => { - warn!("Unable to get METAR request: {}", err); - vec![] - } - }; - return metars; - } -} \ No newline at end of file diff --git a/weather-service/src/metars/routes.rs b/weather-service/src/metars/routes.rs index 5d5d17d..b9a8f71 100644 --- a/weather-service/src/metars/routes.rs +++ b/weather-service/src/metars/routes.rs @@ -1,16 +1,32 @@ -use crate::error_handler::CustomError; +use crate::{error_handler::CustomError, db::Metadata}; use crate::metars::Metars; use actix_web::{get, web, HttpResponse, Responder}; +use log::error; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct MetarsResponse { + pub data: Vec, + pub meta: Metadata +} #[get("metars/{ids}")] async fn get_all(ids: web::Path) -> impl Responder { - let airports = web::block(|| Ok::<_, CustomError>(async {Metars::get_all(ids.into_inner()).await})) + let airports = match web::block(|| Ok::<_, CustomError>(async {Metars::get_all(ids.into_inner()).await})) .await .unwrap() .unwrap() - .await - .unwrap(); - HttpResponse::Ok().json(airports) + .await { + Ok(a) => a, + Err(err) => { + error!("{}", err); + return err.to_http_response(); + } + }; + HttpResponse::Ok().json(MetarsResponse { + data: airports, + meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } + }) } pub fn init_routes(config: &mut web::ServiceConfig) { diff --git a/weather-ui/package.json b/weather-ui/package.json index c2b07ca..1d7c666 100644 --- a/weather-ui/package.json +++ b/weather-ui/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ant-design/cssinjs": "^1.17.0", + "@blueprintjs/core": "^5.3.0", "antd": "^5.9.0", "axios": "^1.4.0", "leaflet": "^1.9.4", diff --git a/weather-ui/src/app/airport/[icao]/page.tsx b/weather-ui/src/app/(routes)/airport/[icao]/page.tsx similarity index 70% rename from weather-ui/src/app/airport/[icao]/page.tsx rename to weather-ui/src/app/(routes)/airport/[icao]/page.tsx index 6efe136..2b3d34d 100644 --- a/weather-ui/src/app/airport/[icao]/page.tsx +++ b/weather-ui/src/app/(routes)/airport/[icao]/page.tsx @@ -1,9 +1,8 @@ -import { getAirport } from '@/js/api/airport'; -import { Airport } from '@/js/api/airport.types'; +import { getAirport } from '@/app/_api/airport'; import Link from 'next/link'; export default async function Page({ params }: { params: { icao: string } }) { - const airport: Airport = await getAirport({ icao: params.icao }); + const { data: airport } = await getAirport({ icao: params.icao }); return ( <>
diff --git a/weather-ui/src/app/layout.tsx b/weather-ui/src/app/(routes)/layout.tsx similarity index 83% rename from weather-ui/src/app/layout.tsx rename to weather-ui/src/app/(routes)/layout.tsx index 7b416f9..1b1d0a5 100644 --- a/weather-ui/src/app/layout.tsx +++ b/weather-ui/src/app/(routes)/layout.tsx @@ -1,10 +1,10 @@ import React from 'react'; import RecoilRootWrapper from '@app/recoil-root-wrapper'; -import Sidebar from '@/components/Sidebar'; -import Topbar from '@/components/Topbar'; +import Sidebar from '@/app/_components/Sidebar'; +import Topbar from '@/app/_components/Topbar'; import 'styles/globals.css'; import 'styles/leaflet.css'; -import StyledComponentsRegistry from '@/lib/AntdRegistry'; +import StyledComponentsRegistry from '@/app/_lib/AntdRegistry'; import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'] }); diff --git a/weather-ui/src/app/page.tsx b/weather-ui/src/app/(routes)/page.tsx similarity index 64% rename from weather-ui/src/app/page.tsx rename to weather-ui/src/app/(routes)/page.tsx index 5685dc2..f30cfaa 100644 --- a/weather-ui/src/app/page.tsx +++ b/weather-ui/src/app/(routes)/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Metar from '@/components/Metars'; +import Metar from '@/app/_components/Metars'; export default function Page() { return ; diff --git a/weather-ui/src/app/profile/page.tsx b/weather-ui/src/app/(routes)/profile/page.tsx similarity index 100% rename from weather-ui/src/app/profile/page.tsx rename to weather-ui/src/app/(routes)/profile/page.tsx diff --git a/weather-ui/src/app/user/page.tsx b/weather-ui/src/app/(routes)/user/page.tsx similarity index 66% rename from weather-ui/src/app/user/page.tsx rename to weather-ui/src/app/(routes)/user/page.tsx index 70eba03..2deac63 100644 --- a/weather-ui/src/app/user/page.tsx +++ b/weather-ui/src/app/(routes)/user/page.tsx @@ -1,3 +1,3 @@ export default function Profile() { - -} \ No newline at end of file + return <>; +} diff --git a/weather-ui/src/app/_api/airport.ts b/weather-ui/src/app/_api/airport.ts new file mode 100644 index 0000000..bb5e79e --- /dev/null +++ b/weather-ui/src/app/_api/airport.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types'; + +interface GetAirportProps { + icao: string; +} + +export async function getAirport({ icao }: GetAirportProps): Promise { + const response = await axios.get(`http://localhost:5000/airports/${icao}`).catch((error) => console.error(error)); + return response?.data || { data: undefined }; +} + +interface GetAirportsProps { + bounds?: Bounds; + category?: string; + filter?: string; + page?: number; + limit?: number; +} + +export async function getAirports({ + bounds, + category, + filter, + limit = 10, + page = 1 +}: GetAirportsProps): Promise { + const response = await axios + .get(`http://localhost:5000/airports`, { + params: { + bounds: bounds + ? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}` + : undefined, + category: category ?? undefined, + filter: filter ?? undefined, + limit, + page + } + }) + .catch((error) => console.error(error)); + return response?.data || { data: [] }; +} diff --git a/weather-ui/src/js/api/airport.types.ts b/weather-ui/src/app/_api/airport.types.ts similarity index 66% rename from weather-ui/src/js/api/airport.types.ts rename to weather-ui/src/app/_api/airport.types.ts index 56073e1..9339c31 100644 --- a/weather-ui/src/js/api/airport.types.ts +++ b/weather-ui/src/app/_api/airport.types.ts @@ -6,6 +6,16 @@ export enum AirportCategory { LARGE = 'large_airport' } +export interface Bounds { + northEast: Coordinate; + southWest: Coordinate; +} + +export interface Coordinate { + lat: number; + lon: number; +} + export interface Airport { icao: string; category: AirportCategory; @@ -25,3 +35,11 @@ export interface Airport { }; metar?: Metar; } + +export interface GetAirportResponse { + data: Airport; +} + +export interface GetAirportsResponse { + data: Airport[]; +} diff --git a/weather-ui/src/js/api/metar.ts b/weather-ui/src/app/_api/metar.ts similarity index 65% rename from weather-ui/src/js/api/metar.ts rename to weather-ui/src/app/_api/metar.ts index 08147c3..d5ea76d 100644 --- a/weather-ui/src/js/api/metar.ts +++ b/weather-ui/src/app/_api/metar.ts @@ -2,12 +2,16 @@ import axios from 'axios'; import { Airport } from './airport.types'; import { Metar } from './metar.types'; -export async function getMetars(airports: Airport[]): Promise { +interface GetMetarsResponse { + data: Metar[]; +} + +export async function getMetars(airports: Airport[]): Promise { if (airports.length == 0) { - return []; + return { data: [] }; } const stationICAOs: string = airports.map((airport) => airport.icao).join(','); const url = `http://localhost:5000/metars/${stationICAOs}`; const response = await axios.get(url).catch((error) => console.error(error)); - return response?.data || []; + return response?.data || { data: [] }; } diff --git a/weather-ui/src/js/api/metar.types.ts b/weather-ui/src/app/_api/metar.types.ts similarity index 100% rename from weather-ui/src/js/api/metar.types.ts rename to weather-ui/src/app/_api/metar.types.ts diff --git a/weather-ui/src/components/Metars/MapTiles.tsx b/weather-ui/src/app/_components/Metars/MapTiles.tsx similarity index 92% rename from weather-ui/src/components/Metars/MapTiles.tsx rename to weather-ui/src/app/_components/Metars/MapTiles.tsx index ee4405b..f0c8a48 100644 --- a/weather-ui/src/components/Metars/MapTiles.tsx +++ b/weather-ui/src/app/_components/Metars/MapTiles.tsx @@ -1,9 +1,9 @@ 'use client'; -import { getAirports } from '@/js/api/airport'; -import { Airport } from '@/js/api/airport.types'; -import { getMetars } from '@/js/api/metar'; -import { Metar } from '@/js/api/metar.types'; +import { getAirports } from '@/app/_api/airport'; +import { Airport } from '@/app/_api/airport.types'; +import { getMetars } from '@/app/_api/metar'; +import { Metar } from '@/app/_api/metar.types'; import { FaLocationPin } from 'react-icons/fa6'; import { DivIcon, LatLngBounds } from 'leaflet'; import { useEffect, useState } from 'react'; @@ -41,7 +41,7 @@ export default function MapTiles() { async function updateAirports(bounds: LatLngBounds) { const ne = bounds.getNorthEast(); const sw = bounds.getSouthWest(); - const _airports = await getAirports({ + const { data: _airports } = await getAirports({ bounds: { northEast: { lat: ne.lat, lon: ne.lng }, southWest: { lat: sw.lat, lon: sw.lng } @@ -49,7 +49,7 @@ export default function MapTiles() { limit: 100, page: 1 }); - const metars = await getMetars(_airports); + const { data: metars } = await getMetars(_airports); metars.forEach((metar) => { _airports.forEach((airport) => { if (metar.station_id == airport.icao) { diff --git a/weather-ui/src/components/Metars/MetarDialog.tsx b/weather-ui/src/app/_components/Metars/MetarDialog.tsx similarity index 79% rename from weather-ui/src/components/Metars/MetarDialog.tsx rename to weather-ui/src/app/_components/Metars/MetarDialog.tsx index 5a3e643..910457c 100644 --- a/weather-ui/src/components/Metars/MetarDialog.tsx +++ b/weather-ui/src/app/_components/Metars/MetarDialog.tsx @@ -1,5 +1,5 @@ -import { Airport } from '@/js/api/airport.types'; -import { Metar } from '@/js/api/metar.types'; +import { Airport } from '@/app/_api/airport.types'; +import { Metar } from '@/app/_api/metar.types'; import { FaArrowsSpin, FaLocationArrow } from 'react-icons/fa6'; import { Modal } from 'antd'; @@ -34,7 +34,7 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro } } return ( - +

{airport.metar?.raw_text}

@@ -45,23 +45,25 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro {airport.metar?.flight_category ? airport.metar?.flight_category : 'UNKN'}
- + {airport.metar && airport.metar.wind_dir_degrees && Number(airport.metar.wind_dir_degrees) > 0 ? ( ) : ( <> )} {airport.metar && airport.metar.wind_dir_degrees && airport.metar.wind_dir_degrees == 'VRB' ? ( - + ) : ( <> )} - {airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0 - ? `${airport.metar?.wind_speed_kt} KT` - : 'CALM'} + + {airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0 + ? `${airport.metar?.wind_speed_kt} KT` + : 'CALM'} +
diff --git a/weather-ui/src/components/Metars/MetarMap.tsx b/weather-ui/src/app/_components/Metars/MetarMap.tsx similarity index 100% rename from weather-ui/src/components/Metars/MetarMap.tsx rename to weather-ui/src/app/_components/Metars/MetarMap.tsx diff --git a/weather-ui/src/components/Metars/index.tsx b/weather-ui/src/app/_components/Metars/index.tsx similarity index 79% rename from weather-ui/src/components/Metars/index.tsx rename to weather-ui/src/app/_components/Metars/index.tsx index fd2abcd..57815ad 100644 --- a/weather-ui/src/components/Metars/index.tsx +++ b/weather-ui/src/app/_components/Metars/index.tsx @@ -1,8 +1,8 @@ -import { Metar } from '@/js/api/metar.types'; +import { Metar } from '@/app/_api/metar.types'; import dynamic from 'next/dynamic'; export default async function Metar({ className = '' }: { className?: string }) { - const Map = dynamic(() => import('@/components/Metars/MetarMap'), { + const Map = dynamic(() => import('@/app/_components/Metars/MetarMap'), { loading: () => (
diff --git a/weather-ui/src/components/Sidebar/Sidebar.css b/weather-ui/src/app/_components/Sidebar/Sidebar.css similarity index 100% rename from weather-ui/src/components/Sidebar/Sidebar.css rename to weather-ui/src/app/_components/Sidebar/Sidebar.css diff --git a/weather-ui/src/components/Sidebar/index.tsx b/weather-ui/src/app/_components/Sidebar/index.tsx similarity index 100% rename from weather-ui/src/components/Sidebar/index.tsx rename to weather-ui/src/app/_components/Sidebar/index.tsx diff --git a/weather-ui/src/app/_components/Topbar/index.tsx b/weather-ui/src/app/_components/Topbar/index.tsx new file mode 100644 index 0000000..b280d2e --- /dev/null +++ b/weather-ui/src/app/_components/Topbar/index.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { AutoComplete, Avatar, Modal } from 'antd'; +import Link from 'next/link'; +import { AiOutlineSearch, AiOutlineUser } from 'react-icons/ai'; +import { Button } from '@blueprintjs/core'; +import { useState } from 'react'; +import { getAirports } from '@/app/_api/airport'; +import { useRouter } from 'next/navigation'; + +const DEFAULT_ICON_SIZE = 40; + +export default function Topbar() { + const [modalOpen, setModalOpen] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]); + const router = useRouter(); + + async function onSearch(value: string) { + setSearchValue(value); + const airportData = await getAirports({ filter: value }); + setAirports( + airportData.data.map((airport) => ({ + key: airport.icao, + value: airport.icao, + label: `${airport.icao} - ${airport.full_name}` + })) + ); + } + + function onSelect(value: string) { + setModalOpen(false); + setSearchValue(''); + router.push(`/airport/${value}`); + } + + function onClose() { + setModalOpen(false); + setSearchValue(''); + } + + return ( + <> + + + + + + ); +} diff --git a/weather-ui/src/lib/AntdRegistry.tsx b/weather-ui/src/app/_lib/AntdRegistry.tsx similarity index 100% rename from weather-ui/src/lib/AntdRegistry.tsx rename to weather-ui/src/app/_lib/AntdRegistry.tsx diff --git a/weather-ui/src/atoms/index.ts b/weather-ui/src/atoms/index.ts deleted file mode 100644 index 46169d3..0000000 --- a/weather-ui/src/atoms/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const airportsState = atom({ - key: 'airportsState', - default: [] as Airport[] -}); - -import { Airport } from "@/js/airport"; -import { atom } from "recoil"; \ No newline at end of file diff --git a/weather-ui/src/components/Topbar/index.tsx b/weather-ui/src/components/Topbar/index.tsx deleted file mode 100644 index ab21317..0000000 --- a/weather-ui/src/components/Topbar/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; - -import { Avatar } from 'antd'; -import Search from 'antd/es/input/Search'; -import { useRouter } from 'next/navigation'; -import { AiOutlineUser } from 'react-icons/ai'; - -export default function Topbar() { - const router = useRouter(); - - function onSearch(value: string) { - router.push(`/airports/${value}`); - } - - return ( - - ); -} diff --git a/weather-ui/src/js/api/airport.ts b/weather-ui/src/js/api/airport.ts deleted file mode 100644 index ca89ce0..0000000 --- a/weather-ui/src/js/api/airport.ts +++ /dev/null @@ -1,45 +0,0 @@ -import axios from 'axios'; -import { Airport } from './airport.types'; - -interface GetAirportsProps { - bounds?: Bounds; - category?: string; - page?: number; - limit?: number; -} - -export interface Bounds { - northEast: Coordinate; - southWest: Coordinate; -} - -export interface Coordinate { - lat: number; - lon: number; -} - -interface GetAirportProps { - icao: string; -} - -export async function getAirport({ icao }: GetAirportProps) { - const response = await axios.get(`http://localhost:5000/airports/${icao}`).catch((error) => console.error(error)); - return response?.data; -} - -export async function getAirports({ bounds, category, limit = 10, page = 1 }: GetAirportsProps): Promise { - const response = await axios - .get(`http://localhost:5000/airports`, { - params: { - ne_lat: bounds?.northEast.lat, - ne_lon: bounds?.northEast.lon, - sw_lat: bounds?.southWest.lat, - sw_lon: bounds?.southWest.lon, - category, - limit, - page - } - }) - .catch((error) => console.error(error)); - return response?.data || []; -} diff --git a/weather-ui/tsconfig.json b/weather-ui/tsconfig.json index b4ff779..54c0641 100755 --- a/weather-ui/tsconfig.json +++ b/weather-ui/tsconfig.json @@ -27,9 +27,10 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], + "@api/*": ["src/app/_api"], "@app/*": ["./src/app/*"], - "@components/*": ["src/components/*"], - "@js/*": ["src/js"] + "@components/*": ["src/app/_components/*"], + "@lib/*": ["src/app/_components/*"] } }, "include": [