Add Kontula weather info

This commit is contained in:
Tanguy Gérôme 2025-06-21 00:41:06 +03:00
parent 469bfc5640
commit 7c8f0ce2dc
Signed by: tanguy
GPG key ID: 10B2947233740B88
5 changed files with 1412 additions and 238 deletions

1206
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,16 +8,18 @@ authors = ["Tanguy Gérôme <tanguy@juustodiilerit.fi>"]
[dependencies]
# leptos = { version = "0.7", features = ["nightly"] }
leptos = { version = "0.7", features = ["csr"] }
leptos = { version = "0.8", features = ["csr"] }
# leptos_router = { version = "0.7", features = ["nightly"] }
leptos_router = { version = "0.7" }
leptos_meta = { version = "0.7" }
leptos_router = { version = "0.8" }
leptos_meta = { version = "0.8" }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1"
chrono = { version = "0.4.40", features = ["unstable-locales"] }
calendrier = "1.0.0"
leptos-use = "0.15.7"
leptos-use = "0.16"
reqwest = { version = "0.12.20", features = ["json"] }
serde_json = "1.0.140"
# utils
# strum = { version = "0.25", features = ["derive", "strum_macros"] }

View file

@ -90,9 +90,61 @@ body {
flex-direction: column;
align-items: center;
justify-content: space-between;
padding-bottom: 32px;
padding-top: 16px;
padding-bottom: 16px;
gap: 32px;
.datetime {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 8px;
p {
margin: 0;
}
}
.weather {
.forecast-per-hours {
// background: white;
// color: black;
width: 100%;
max-width: 100vw;
padding: 8px;
overflow-x: auto;
table {
text-align: center;
table-layout: auto;
border-collapse: collapse;
th,td {
border: grey dashed 1px;
width: fit-content;
min-width: fit-content;
padding: 0px 2px;
}
.weather-code {
td {
width: 50px;
height: 50px;
img {
position: relative;
top: 2px;
width: 50px;
height: 50px;
background: radial-gradient(closest-side, #BBB, white);
@media all and (prefers-color-scheme: dark) {
background: radial-gradient(closest-side, #AAA, black);
}
}
}
}
}
}
}
.links {

View file

@ -1,8 +1,95 @@
use leptos::prelude::*;
use leptos_meta::*;
use leptos_use::{use_timestamp};
use chrono::prelude::{DateTime, Local, Timelike};
use chrono::{prelude::{DateTime, Local, Timelike}, NaiveDateTime};
use calendrier::{DateTime as FRDateTime, Timestamp as FRTimestamp};
use serde_json::Value;
pub mod wmo;
#[component]
pub fn Weather() -> impl IntoView {
let weather_json = LocalResource::new(async move || {
let json_response = reqwest::get("https://api.open-meteo.com/v1/forecast?latitude=60.236610&longitude=25.083630&timezone=auto&forecast_hours=16&wind_speed_unit=kn&hourly=temperature_2m,wind_speed_10m,precipitation,precipitation_probability,weather_code,is_day").await
.and_then(|response| Ok(response.json::<Value>()));
match json_response {
Ok(result) => Some(result.await.ok()),
_ => None
}.flatten()
});
let times = move || {
Some(weather_json.get().flatten()?["hourly"]["time"].as_array()?.into_iter().map(|timestamp| {
let datetime = NaiveDateTime::parse_from_str(timestamp.as_str().unwrap_or_default(), "%Y-%m-%dT%H:%M");
view! {<td>{format!("{}", datetime.unwrap_or_default().hour())}</td>}
}).collect::<Vec<_>>())
};
let weather_codes = move || {
let json = weather_json.get().flatten()?;
let is_day = json["hourly"]["is_day"].as_array()?.into_iter();
let codes = json["hourly"]["weather_code"].as_array()?.into_iter();
Some(is_day.zip(codes).map(move |(is_day, code)| {
let wmo_codes = crate::wmo::get_codes();
let image = wmo_codes[code.to_string()][if *is_day == Value::Number(1.into()) {"day"} else {"night"}]["image"].as_str().unwrap_or_default();
let description = wmo_codes[code.to_string()][if *is_day == Value::Number(1.into()) {"day"} else {"night"}]["description"].as_str().unwrap_or_default();
view! { <td><img src={image.to_string()} alt={description.to_string()}/></td> }
}).collect::<Vec<_>>())
};
let temperatures = move || {
Some(weather_json.get().flatten()?["hourly"]["temperature_2m"].as_array()?.into_iter().map(|temperature| {
view! { <td>{format!("{}°C", temperature.to_string())}</td> }
}).collect::<Vec<_>>())
};
let wind = move || {
Some(weather_json.get().flatten()?["hourly"]["wind_speed_10m"].as_array()?.into_iter().map(|wind_speed| {
view! { <td>{format!("{}kn", wind_speed.to_string())}</td> }
}).collect::<Vec<_>>())
};
let precipitation = move || {
let json = weather_json.get().flatten()?;
let amounts = json["hourly"]["precipitation"].as_array()?.into_iter();
let probabilities = json["hourly"]["precipitation"].as_array()?.into_iter();
Some(amounts.zip(probabilities).map(|(amount, probability_float)| {
let probability = (probability_float.as_f64()? * 100.0).round() as i64;
Some(view! {
<td>
<span>{format!("{}mm", amount.to_string())}</span>
<br/>
<span>{format!("{}%", probability)}</span>
</td>
})
}).collect::<Vec<_>>())
};
view! {
<div class="weather">
<Suspense fallback=move || view! { <p>"Loading..."</p> }>
<div class="forecast-per-hours">
<table class="forecast-per-hours">
<tr class="time">
<th>klo</th>
{times}
</tr>
<tr class="weather-code">
<th>sää</th>
{weather_codes}
</tr>
<tr class="temperature">
<th>lämpö</th>
{temperatures}
</tr>
<tr class="wind">
<th>tuuli</th>
{wind}
</tr>
<tr class="temperature">
<th>sade&<br/>mahd.</th>
{precipitation}
</tr>
</table>
</div>
</Suspense>
</div>
}
}
#[component]
pub fn Times() -> impl IntoView {
@ -19,12 +106,12 @@ pub fn Times() -> impl IntoView {
let fr_seconds_from_hour = fr_seconds_from_midnight - (100 * 100 * fr_hours);
let fr_minutes = fr_seconds_from_hour / 100;
let fr_seconds = fr_seconds_from_hour - (100 * fr_minutes);
let fr_time = format!("{}:{}:{}", fr_hours, fr_minutes, fr_seconds);
let fr_time = format!("{:02}:{:02}:{:02}", fr_hours, fr_minutes, fr_seconds);
fr_time
});
let fr_date_time = move || format!("{}, {}", fr_date.get().to_string_default(), fr_time.get());
let fi_date_time = move || local_now.get().format_localized("%A %e %B %Y, %T", chrono::Locale::fi_FI).to_string();
let fi_date_time = move || local_now.get().format_localized("%A %e %B %Y, %H.%M.%S", chrono::Locale::fi_FI).to_string();
view! {
<div class="datetime">
@ -58,6 +145,7 @@ pub fn App() -> impl IntoView {
</header>
<main class="main-width">
<Times/>
<Weather/>
<ul class="links">
<li>
<a href="https://forgejo.juustodiilerit.fi">git (forgejo)</a>

286
src/wmo.rs Normal file
View file

@ -0,0 +1,286 @@
use serde_json::{Value, json};
pub fn get_codes() -> Value {
json!({
"0":{
"day":{
"description":"Sunny",
"image":"http://openweathermap.org/img/wn/01d@2x.png"
},
"night":{
"description":"Clear",
"image":"http://openweathermap.org/img/wn/01n@2x.png"
}
},
"1":{
"day":{
"description":"Mainly Sunny",
"image":"http://openweathermap.org/img/wn/01d@2x.png"
},
"night":{
"description":"Mainly Clear",
"image":"http://openweathermap.org/img/wn/01n@2x.png"
}
},
"2":{
"day":{
"description":"Partly Cloudy",
"image":"http://openweathermap.org/img/wn/02d@2x.png"
},
"night":{
"description":"Partly Cloudy",
"image":"http://openweathermap.org/img/wn/02n@2x.png"
}
},
"3":{
"day":{
"description":"Cloudy",
"image":"http://openweathermap.org/img/wn/03d@2x.png"
},
"night":{
"description":"Cloudy",
"image":"http://openweathermap.org/img/wn/03n@2x.png"
}
},
"45":{
"day":{
"description":"Foggy",
"image":"http://openweathermap.org/img/wn/50d@2x.png"
},
"night":{
"description":"Foggy",
"image":"http://openweathermap.org/img/wn/50n@2x.png"
}
},
"48":{
"day":{
"description":"Rime Fog",
"image":"http://openweathermap.org/img/wn/50d@2x.png"
},
"night":{
"description":"Rime Fog",
"image":"http://openweathermap.org/img/wn/50n@2x.png"
}
},
"51":{
"day":{
"description":"Light Drizzle",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Light Drizzle",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"53":{
"day":{
"description":"Drizzle",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Drizzle",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"55":{
"day":{
"description":"Heavy Drizzle",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Heavy Drizzle",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"56":{
"day":{
"description":"Light Freezing Drizzle",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Light Freezing Drizzle",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"57":{
"day":{
"description":"Freezing Drizzle",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Freezing Drizzle",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"61":{
"day":{
"description":"Light Rain",
"image":"http://openweathermap.org/img/wn/10d@2x.png"
},
"night":{
"description":"Light Rain",
"image":"http://openweathermap.org/img/wn/10n@2x.png"
}
},
"63":{
"day":{
"description":"Rain",
"image":"http://openweathermap.org/img/wn/10d@2x.png"
},
"night":{
"description":"Rain",
"image":"http://openweathermap.org/img/wn/10n@2x.png"
}
},
"65":{
"day":{
"description":"Heavy Rain",
"image":"http://openweathermap.org/img/wn/10d@2x.png"
},
"night":{
"description":"Heavy Rain",
"image":"http://openweathermap.org/img/wn/10n@2x.png"
}
},
"66":{
"day":{
"description":"Light Freezing Rain",
"image":"http://openweathermap.org/img/wn/10d@2x.png"
},
"night":{
"description":"Light Freezing Rain",
"image":"http://openweathermap.org/img/wn/10n@2x.png"
}
},
"67":{
"day":{
"description":"Freezing Rain",
"image":"http://openweathermap.org/img/wn/10d@2x.png"
},
"night":{
"description":"Freezing Rain",
"image":"http://openweathermap.org/img/wn/10n@2x.png"
}
},
"71":{
"day":{
"description":"Light Snow",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Light Snow",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"73":{
"day":{
"description":"Snow",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Snow",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"75":{
"day":{
"description":"Heavy Snow",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Heavy Snow",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"77":{
"day":{
"description":"Snow Grains",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Snow Grains",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"80":{
"day":{
"description":"Light Showers",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Light Showers",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"81":{
"day":{
"description":"Showers",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Showers",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"82":{
"day":{
"description":"Heavy Showers",
"image":"http://openweathermap.org/img/wn/09d@2x.png"
},
"night":{
"description":"Heavy Showers",
"image":"http://openweathermap.org/img/wn/09n@2x.png"
}
},
"85":{
"day":{
"description":"Light Snow Showers",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Light Snow Showers",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"86":{
"day":{
"description":"Snow Showers",
"image":"http://openweathermap.org/img/wn/13d@2x.png"
},
"night":{
"description":"Snow Showers",
"image":"http://openweathermap.org/img/wn/13n@2x.png"
}
},
"95":{
"day":{
"description":"Thunderstorm",
"image":"http://openweathermap.org/img/wn/11d@2x.png"
},
"night":{
"description":"Thunderstorm",
"image":"http://openweathermap.org/img/wn/11n@2x.png"
}
},
"96":{
"day":{
"description":"Light Thunderstorms With Hail",
"image":"http://openweathermap.org/img/wn/11d@2x.png"
},
"night":{
"description":"Light Thunderstorms With Hail",
"image":"http://openweathermap.org/img/wn/11n@2x.png"
}
},
"99":{
"day":{
"description":"Thunderstorm With Hail",
"image":"http://openweathermap.org/img/wn/11d@2x.png"
},
"night":{
"description":"Thunderstorm With Hail",
"image":"http://openweathermap.org/img/wn/11n@2x.png"
}
}
})
}