Implemented backend endpoints with openweathermap integration. Frontend now talks with the backend API.

master
hheik 2025-05-09 14:12:31 +03:00
parent 813f01f470
commit 94b39228c2
12 changed files with 1553 additions and 77 deletions

782
kelikatti-api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

206
kelikatti-api/src/api.rs Normal file
View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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",

View File

@ -15,6 +15,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.8.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},

18
kelikatti-web/src/api.ts Normal file
View File

@ -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')

View File

@ -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>

View File

@ -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>

View File

@ -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[]
}

View File

@ -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>