Implemented backend endpoints with openweathermap integration. Frontend now talks with the backend API.
parent
813f01f470
commit
94b39228c2
File diff suppressed because it is too large
Load Diff
|
|
@ -7,5 +7,6 @@ edition = "2024"
|
|||
actix-web = "4.10.2"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.27"
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
serde = { version = "1.0.219", features = ["std", "derive"] }
|
||||
serde_json = "1.0.140"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
pub mod openweathermap {
|
||||
use kelikatti_api::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MainData {
|
||||
temp: Option<f32>,
|
||||
feels_like: Option<f32>,
|
||||
humidity: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WeatherData {
|
||||
main: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WindData {
|
||||
speed: Option<f32>,
|
||||
deg: Option<f32>,
|
||||
gust: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CloudsData {
|
||||
all: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RainData {
|
||||
#[serde(rename = "1h")]
|
||||
one_hour: Option<f32>,
|
||||
#[serde(rename = "3h")]
|
||||
three_hour: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SnowData {
|
||||
#[serde(rename = "1h")]
|
||||
one_hour: Option<f32>,
|
||||
#[serde(rename = "3h")]
|
||||
three_hour: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CurrentSysData {
|
||||
sunrise: Option<u64>,
|
||||
sunset: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WeatherResponse {
|
||||
main: MainData,
|
||||
weather: Vec<WeatherData>,
|
||||
wind: WindData,
|
||||
clouds: CloudsData,
|
||||
timezone: Option<i64>,
|
||||
rain: Option<RainData>,
|
||||
snow: Option<SnowData>,
|
||||
sys: CurrentSysData,
|
||||
}
|
||||
|
||||
impl From<WeatherResponse> for WeatherDTO {
|
||||
fn from(value: WeatherResponse) -> Self {
|
||||
Self {
|
||||
sunrise_utc_ms: value.sys.sunrise.map(|seconds| seconds * 1000),
|
||||
sunset_utc_ms: value.sys.sunset.map(|seconds| seconds * 1000),
|
||||
timezone_shift_ms: value.timezone.map(|shift| shift * 1000),
|
||||
temperature: value.main.temp,
|
||||
feels_like: value.main.feels_like,
|
||||
wind_speed: value.wind.speed,
|
||||
gust_speed: value.wind.gust,
|
||||
wind_dir: value.wind.deg,
|
||||
humidity: value.main.humidity.map(|percentage| percentage / 100.0),
|
||||
rain_rate: value.rain.and_then(|rate| {
|
||||
rate.one_hour.or(rate
|
||||
.three_hour
|
||||
.map(|three_hour_cumulative| three_hour_cumulative / 3.0))
|
||||
}),
|
||||
snow_rate: value.snow.and_then(|rate| {
|
||||
rate.one_hour.or(rate
|
||||
.three_hour
|
||||
.map(|three_hour_cumulative| three_hour_cumulative / 3.0))
|
||||
}),
|
||||
cloudiness: value.clouds.all.map(|percentage| percentage / 100.0),
|
||||
conditions: value
|
||||
.weather
|
||||
.iter()
|
||||
.map(|weather| weather.main.to_lowercase())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ForecastSysData {
|
||||
pod: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ForecastDatapoint {
|
||||
dt: u64,
|
||||
main: MainData,
|
||||
weather: Vec<WeatherData>,
|
||||
wind: WindData,
|
||||
clouds: CloudsData,
|
||||
pop: Option<f32>,
|
||||
rain: Option<RainData>,
|
||||
snow: Option<SnowData>,
|
||||
sys: ForecastSysData,
|
||||
}
|
||||
|
||||
impl From<ForecastDatapoint> for ForecastDatapointDTO {
|
||||
fn from(value: ForecastDatapoint) -> Self {
|
||||
Self {
|
||||
timestamp_utc_ms: value.dt * 1000,
|
||||
is_day: value.sys.pod == "d",
|
||||
temperature: value.main.temp,
|
||||
feels_like: value.main.feels_like,
|
||||
wind_speed: value.wind.speed,
|
||||
gust_speed: value.wind.gust,
|
||||
wind_dir: value.wind.deg,
|
||||
humidity: value.main.humidity.map(|percentage| percentage / 100.0),
|
||||
precipitation_chance: value.pop,
|
||||
rain_rate: value.rain.and_then(|rate| {
|
||||
rate.one_hour.or(rate
|
||||
.three_hour
|
||||
.map(|three_hour_cumulative| three_hour_cumulative / 3.0))
|
||||
}),
|
||||
snow_rate: value.snow.and_then(|rate| {
|
||||
rate.one_hour.or(rate
|
||||
.three_hour
|
||||
.map(|three_hour_cumulative| three_hour_cumulative / 3.0))
|
||||
}),
|
||||
cloudiness: value.clouds.all.map(|percentage| percentage / 100.0),
|
||||
conditions: value
|
||||
.weather
|
||||
.iter()
|
||||
.map(|weather| weather.main.to_lowercase())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ForecastResponse {
|
||||
list: Vec<ForecastDatapoint>,
|
||||
}
|
||||
|
||||
impl From<ForecastResponse> for ForecastDTO {
|
||||
fn from(value: ForecastResponse) -> Self {
|
||||
Self {
|
||||
list: value
|
||||
.list
|
||||
.into_iter()
|
||||
.map(ForecastDatapointDTO::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const GEO_POS: Coordinates = Coordinates { lat: 0.0, lon: 0.0 };
|
||||
const UNIT: &str = "metric";
|
||||
const API_HOST: &str = "https://api.openweathermap.org";
|
||||
const API_KEY: &str = "";
|
||||
|
||||
fn new_client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
pub async fn fetch_weather() -> reqwest::Result<WeatherDTO> {
|
||||
let url = format!("{API_HOST}/data/2.5/weather");
|
||||
let response = new_client()
|
||||
.get(url)
|
||||
.query(&[
|
||||
("lat", GEO_POS.lat.to_string()),
|
||||
("lon", GEO_POS.lon.to_string()),
|
||||
("units", UNIT.to_string()),
|
||||
("appid", API_KEY.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<WeatherResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(WeatherDTO::from(response))
|
||||
}
|
||||
|
||||
pub async fn fetch_forecast() -> reqwest::Result<ForecastDTO> {
|
||||
let url = format!("{API_HOST}/data/2.5/forecast");
|
||||
let response = new_client()
|
||||
.get(url)
|
||||
.query(&[
|
||||
("lat", GEO_POS.lat.to_string()),
|
||||
("lon", GEO_POS.lon.to_string()),
|
||||
("units", UNIT.to_string()),
|
||||
("appid", API_KEY.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<ForecastResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(ForecastDTO::from(response))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,102 @@
|
|||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WeatherDTO {}
|
||||
pub struct Coordinates {
|
||||
pub lat: f32,
|
||||
pub lon: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ForecastDTO {}
|
||||
pub struct WeatherDTO {
|
||||
pub sunrise_utc_ms: Option<u64>,
|
||||
pub sunset_utc_ms: Option<u64>,
|
||||
pub timezone_shift_ms: Option<i64>,
|
||||
pub temperature: Option<f32>,
|
||||
pub feels_like: Option<f32>,
|
||||
pub wind_speed: Option<f32>,
|
||||
pub wind_dir: Option<f32>,
|
||||
pub gust_speed: Option<f32>,
|
||||
pub humidity: Option<f32>,
|
||||
pub rain_rate: Option<f32>,
|
||||
pub snow_rate: Option<f32>,
|
||||
pub cloudiness: Option<f32>,
|
||||
pub conditions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ForecastDTO {
|
||||
pub list: Vec<ForecastDatapointDTO>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ForecastDatapointDTO {
|
||||
pub timestamp_utc_ms: u64,
|
||||
pub is_day: bool,
|
||||
pub temperature: Option<f32>,
|
||||
pub feels_like: Option<f32>,
|
||||
pub wind_speed: Option<f32>,
|
||||
pub gust_speed: Option<f32>,
|
||||
pub wind_dir: Option<f32>,
|
||||
pub humidity: Option<f32>,
|
||||
pub precipitation_chance: Option<f32>,
|
||||
pub rain_rate: Option<f32>,
|
||||
pub snow_rate: Option<f32>,
|
||||
pub cloudiness: Option<f32>,
|
||||
pub conditions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CacheData<T> {
|
||||
pub data: T,
|
||||
pub created_at: Instant,
|
||||
pub time_to_live_ms: u128,
|
||||
}
|
||||
|
||||
impl<T> CacheData<T> {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
Instant::now().duration_since(self.created_at).as_millis() < self.time_to_live_ms
|
||||
}
|
||||
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
data,
|
||||
created_at: Instant::now(),
|
||||
time_to_live_ms: 10_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WebCache {
|
||||
map: HashMap<String, CacheData<String>>,
|
||||
}
|
||||
|
||||
impl WebCache {
|
||||
pub async fn try_cached_request<F>(
|
||||
&mut self,
|
||||
key: &HttpRequest,
|
||||
fun: F,
|
||||
) -> Result<String, String>
|
||||
where
|
||||
F: AsyncFnOnce() -> Result<String, String>,
|
||||
{
|
||||
self.try_cached_uri(key.uri().path().to_string(), fun).await
|
||||
}
|
||||
|
||||
pub async fn try_cached_uri<F>(&mut self, key: String, fun: F) -> Result<String, String>
|
||||
where
|
||||
F: AsyncFnOnce() -> Result<String, String>,
|
||||
{
|
||||
if let Some(cached) = self.map.get(&key) {
|
||||
if cached.is_valid() {
|
||||
return Ok(cached.data.clone());
|
||||
}
|
||||
}
|
||||
self.map.remove(&key);
|
||||
let data = fun().await?;
|
||||
self.map.insert(key.clone(), CacheData::new(data.clone()));
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,106 @@
|
|||
use actix_web::{App, HttpResponse, HttpServer, middleware, web};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Example {
|
||||
key: String,
|
||||
value: u32,
|
||||
visible: bool,
|
||||
use actix_web::http::header::{self, HeaderValue};
|
||||
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, dev::Service, middleware, web};
|
||||
use log::{error, info};
|
||||
|
||||
use kelikatti_api::*;
|
||||
|
||||
pub mod api;
|
||||
|
||||
type SharedCache = Mutex<WebCache>;
|
||||
|
||||
fn send_json_response(fetch_result: Result<String, String>) -> HttpResponse {
|
||||
match fetch_result {
|
||||
Ok(body) => HttpResponse::Ok().body(body),
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This handler uses json extractor
|
||||
async fn index() -> HttpResponse {
|
||||
info!("Called index!");
|
||||
HttpResponse::Ok().body("Hello\n")
|
||||
async fn weather(req: HttpRequest, cache: web::Data<SharedCache>) -> HttpResponse {
|
||||
send_json_response(
|
||||
cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.try_cached_request(&req, async || {
|
||||
serde_json::to_string(match &api::openweathermap::fetch_weather().await {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
return Err(err.to_string());
|
||||
}
|
||||
})
|
||||
.map_err(|err| err.to_string())
|
||||
})
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn post(example: web::Json<Example>) -> HttpResponse {
|
||||
HttpResponse::Ok().json(example.0)
|
||||
async fn forecast(req: HttpRequest, cache: web::Data<SharedCache>) -> HttpResponse {
|
||||
send_json_response(
|
||||
cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.try_cached_request(&req, async || {
|
||||
serde_json::to_string(match &api::openweathermap::fetch_forecast().await {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
return Err(err.to_string());
|
||||
}
|
||||
})
|
||||
.map_err(|err| err.to_string())
|
||||
})
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
const PORT: u16 = 8080;
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.parse::<u16>()
|
||||
.expect("Parsing PORT environment variable")
|
||||
})
|
||||
.next()
|
||||
.unwrap_or(8080);
|
||||
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
info!("Server listening on port {port}");
|
||||
|
||||
info!("Server listening on port {PORT}");
|
||||
let shared_cache = web::Data::new(SharedCache::new(WebCache::default()));
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
// enable logger
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap_fn(|req, srv| {
|
||||
let fut = srv.call(req);
|
||||
async {
|
||||
let mut res = fut.await?;
|
||||
res.headers_mut().append(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
res.headers_mut().append(
|
||||
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HeaderValue::from_static("*"),
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
})
|
||||
// global configuration
|
||||
.app_data(web::JsonConfig::default().limit(4096))
|
||||
.service(web::resource("/").route(web::get().to(index)))
|
||||
.service(web::resource("/post").route(web::post().to(post)))
|
||||
.app_data(shared_cache.clone())
|
||||
.service(web::resource("/weather").route(web::get().to(weather)))
|
||||
.service(web::resource("/forecast").route(web::get().to(forecast)))
|
||||
})
|
||||
.bind(("127.0.0.1", PORT))?
|
||||
.bind(("127.0.0.1", port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"name": "kelikatti-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
|
@ -2436,6 +2437,23 @@
|
|||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
|
@ -2532,6 +2550,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -2600,6 +2631,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -2739,6 +2782,29 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.136",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz",
|
||||
|
|
@ -2768,6 +2834,51 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
||||
|
|
@ -3307,6 +3418,41 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
|
|
@ -3337,6 +3483,15 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
|
@ -3347,6 +3502,43 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
|
|
@ -3387,6 +3579,18 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
|
|
@ -3411,6 +3615,45 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
|
|
@ -3805,6 +4048,15 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memorystream": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||
|
|
@ -3838,6 +4090,27 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
|
|
@ -4328,6 +4601,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import axios, { type AxiosInstance } from 'axios'
|
||||
import type { WeatherDTO, ForecastDTO } from './types'
|
||||
|
||||
export const newClient = (): AxiosInstance =>
|
||||
axios.create({
|
||||
baseURL: 'http://localhost:8080',
|
||||
})
|
||||
|
||||
const get = async (client: AxiosInstance, url: string) => {
|
||||
const resp = await client.get(url)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export const fetchWeather = async (client: AxiosInstance): Promise<WeatherDTO> =>
|
||||
get(client, '/weather')
|
||||
|
||||
export const fetchForecast = async (client: AxiosInstance): Promise<ForecastDTO> =>
|
||||
get(client, '/forecast')
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ForecastDTO } from '../types'
|
||||
import type { ForecastDTO } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
data: ForecastDTO
|
||||
|
|
@ -9,10 +9,16 @@ defineProps<{
|
|||
<template>
|
||||
<div class="container">
|
||||
<h2>Forecast!</h2>
|
||||
<div v-for="weather in data.list">
|
||||
<div v-for="forecast in data.list">
|
||||
<!-- <code> -->
|
||||
<!-- {{ JSON.stringify(forecast) }} -->
|
||||
<!-- </code> -->
|
||||
<ul>
|
||||
<li>temparature: {{ weather.temperature }}</li>
|
||||
<li>feels like: {{ weather.feels_like }}</li>
|
||||
<li>time: {{ new Date(forecast.timestamp_utc_ms) }}</li>
|
||||
<li>temparature: {{ forecast.temperature }}°C</li>
|
||||
<li>feels like: {{ forecast.feels_like }}°C</li>
|
||||
<li>humidity: {{ Math.round((forecast.humidity ?? 0) * 100) }}%</li>
|
||||
<li>chance of rain: {{ Math.round((forecast.precipitation_chance ?? 0) * 100) }}%</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { WeatherDTO } from '../types'
|
||||
import type { WeatherDTO } from '@/types';
|
||||
|
||||
|
||||
defineProps<{
|
||||
data: WeatherDTO
|
||||
|
|
@ -10,8 +11,11 @@ defineProps<{
|
|||
<div class="container">
|
||||
<h2>Weather!</h2>
|
||||
<div>
|
||||
<div>temparature: {{ data.temperature }}</div>
|
||||
<div>feels like: {{ data.feels_like }}</div>
|
||||
<li>temparature: {{ data.temperature }}°C</li>
|
||||
<li>feels like: {{ data.feels_like }}°C</li>
|
||||
<li>humidity: {{ Math.round((data.humidity ?? 0) * 100) }}%</li>
|
||||
<li v-if="data.sunset_utc_ms !== undefined">sunrise: {{ new Date(data.sunrise_utc_ms) }}</li>
|
||||
<li v-if="data.sunrise_utc_ms !== undefined">sunset: {{ new Date(data.sunset_utc_ms) }}</li>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
export type Condition = 'Thunderstorm' | 'Drizzle' | 'Rain' | 'Snow' | 'Fog'
|
||||
|
||||
export type Cloudiness = 'Clear' | 'Few' | 'Cloudy' | 'Overcast'
|
||||
export type Condition =
|
||||
| 'thunderstorm'
|
||||
| 'drizzle'
|
||||
| 'rain'
|
||||
| 'snow'
|
||||
| 'fog'
|
||||
| 'mist'
|
||||
| 'haze'
|
||||
| 'tornado'
|
||||
|
||||
export interface WeatherDTO {
|
||||
/** Is the report from time between sunrise and sunset? */
|
||||
is_day: boolean
|
||||
/** UTC timestamp of sunrise in milliseconds */
|
||||
sunrise_utc_ms?: number
|
||||
/** UTC timestamp of sunset in milliseconds */
|
||||
sunset_utc_ms?: number
|
||||
/** How much timestamps need to be shifted to get the local time in milliseconds */
|
||||
timezone_shift_ms?: number
|
||||
/** Temperature in Celsius */
|
||||
temperature?: number
|
||||
/** Perception of temperature in Celsius */
|
||||
feels_like?: number
|
||||
/** Wind speed in meters/second */
|
||||
wind_speed?: number
|
||||
/** Humidity percentage (0 - 100) */
|
||||
humidity?: number
|
||||
/** Meteorological wind direction in degrees
|
||||
*
|
||||
* 0: From north
|
||||
|
|
@ -20,12 +28,56 @@ export interface WeatherDTO {
|
|||
* 180: Form south
|
||||
* 270: From west
|
||||
* */
|
||||
wind_degrees?: number
|
||||
cloudiness?: Cloudiness
|
||||
wind_dir?: number
|
||||
/** Speed of sudden gusts in meters/second */
|
||||
gust_speed?: number
|
||||
/** Humidity percentage (0 - 1) */
|
||||
humidity?: number
|
||||
/** Rain in mm/h */
|
||||
rain_rate?: number
|
||||
/** Snow in mm/h */
|
||||
snow_rate?: number
|
||||
/** Percentage of clouds covering the sky (0 - 1) */
|
||||
cloudiness?: number
|
||||
/** List of possible weather phenomena */
|
||||
conditions: Condition[]
|
||||
}
|
||||
|
||||
export interface ForecastDatapointDTO {
|
||||
/** UTC timestamp of forecast in milliseconds */
|
||||
timestamp_utc_ms: number
|
||||
/** Is the forecast for daytime? */
|
||||
is_day: boolean
|
||||
/** Temperature in Celsius */
|
||||
temperature?: number
|
||||
/** Perception of temperature in Celsius */
|
||||
feels_like?: number
|
||||
/** Wind speed in meters/second */
|
||||
wind_speed?: number
|
||||
/** Meteorological wind direction in degrees
|
||||
*
|
||||
* 0: From north
|
||||
* 90: From east
|
||||
* 180: Form south
|
||||
* 270: From west
|
||||
* */
|
||||
wind_dir?: number
|
||||
/** Speed of sudden gusts in meters/second */
|
||||
gust_speed?: number
|
||||
/** Humidity percentage (0 - 1) */
|
||||
humidity?: number
|
||||
/** Chance of precipitation (0 - 1) */
|
||||
precipitation_chance?: number
|
||||
/** Rain in mm/h */
|
||||
rain_rate?: number
|
||||
/** Snow in mm/h */
|
||||
snow_rate?: number
|
||||
/** Percentage of clouds covering the sky (0 - 1) */
|
||||
cloudiness?: number
|
||||
/** List of possible weather phenomena */
|
||||
conditions: Condition[]
|
||||
}
|
||||
|
||||
export interface ForecastDTO {
|
||||
list: WeatherDTO[]
|
||||
list: ForecastDatapointDTO[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import Weather from '../components/weather.vue'
|
||||
import Forecast from '../components/forecast.vue'
|
||||
import { ForecastDTO, WeatherDTO } from '../types';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const weather: WeatherDTO = {
|
||||
is_day: true,
|
||||
cloudiness: 'Clear',
|
||||
temperature: 10.5,
|
||||
feels_like: 7.0,
|
||||
wind_speed: 3.0,
|
||||
wind_degrees: 170,
|
||||
conditions: [],
|
||||
};
|
||||
import Weather from '@/components/weather.vue'
|
||||
import Forecast from '@/components/forecast.vue'
|
||||
import { weather, forecast } from "@/global"
|
||||
import * as api from "@/api"
|
||||
|
||||
const forecast: ForecastDTO = {
|
||||
list: [weather, weather, weather],
|
||||
};
|
||||
const client = api.newClient();
|
||||
|
||||
const fetchWeather = async (): Promise<void> => {
|
||||
weather.resolve(api.fetchWeather(client))
|
||||
}
|
||||
|
||||
const fetchForecast = async (): Promise<void> => {
|
||||
forecast.resolve(api.fetchForecast(client))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchWeather()
|
||||
fetchForecast()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -26,11 +29,11 @@ const forecast: ForecastDTO = {
|
|||
<h2>It's weather with cats!</h2>
|
||||
</header>
|
||||
<button>Ayy lmao</button>
|
||||
<div class="weather-container">
|
||||
<Weather :data=weather />
|
||||
<div v-if="weather.data" class="weather-container">
|
||||
<Weather :data=weather.data />
|
||||
</div>
|
||||
<div class="forecast-container">
|
||||
<Forecast :data=forecast />
|
||||
<div v-if="forecast.data" class="forecast-container">
|
||||
<Forecast :data=forecast.data />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in New Issue