weather_app.rs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. #![allow(non_snake_case)]
  2. use dioxus::prelude::*;
  3. use serde::{Deserialize, Serialize};
  4. fn main() {
  5. launch(app);
  6. }
  7. fn app() -> Element {
  8. let country = use_signal(|| WeatherLocation {
  9. name: "Berlin".to_string(),
  10. country: "Germany".to_string(),
  11. latitude: 52.5244,
  12. longitude: 13.4105,
  13. id: 2950159,
  14. });
  15. let current_weather =
  16. use_resource(move || async move { get_weather(&country.read().clone()).await });
  17. rsx! {
  18. link {
  19. rel: "stylesheet",
  20. href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css"
  21. }
  22. div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
  23. div { class: "flex items-center justify-center flex-row",
  24. div { class: "flex items-start justify-center flex-row",
  25. SearchBox { country: country }
  26. div { class: "flex flex-wrap w-full px-2",
  27. div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full bg-white dark:bg-gray-600",
  28. div { class: "px-6 py-6 relative",
  29. if let Some(Ok(weather)) = current_weather.read().as_ref() {
  30. CountryData {
  31. country: country.read().clone(),
  32. weather: weather.clone(),
  33. }
  34. Forecast {
  35. weather: weather.clone(),
  36. }
  37. } else {
  38. p {
  39. "Loading.."
  40. }
  41. }
  42. }
  43. }
  44. }
  45. }
  46. }
  47. }
  48. }
  49. }
  50. #[allow(non_snake_case)]
  51. #[component]
  52. fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element {
  53. let today = "Today";
  54. let max_temp = weather.daily.temperature_2m_max.first().unwrap();
  55. let min_temp = weather.daily.temperature_2m_min.first().unwrap();
  56. rsx! {
  57. div { class: "flex mb-4 justify-between items-center",
  58. div {
  59. h5 { class: "mb-0 font-medium text-xl", "{country.name} 🏞️" }
  60. h6 { class: "mb-0", "{today}" }
  61. }
  62. div {
  63. div { class: "flex items-center",
  64. span { "Temp min" }
  65. span { class: "px-2 inline-block", "👉 {min_temp}°" }
  66. }
  67. div { class: "flex items-center",
  68. span { "Temp max" }
  69. span { class: "px-2 inline-block ", "👉 {max_temp}º" }
  70. }
  71. }
  72. }
  73. }
  74. }
  75. #[allow(non_snake_case)]
  76. #[component]
  77. fn Forecast(weather: WeatherResponse) -> Element {
  78. let today = (weather.daily.temperature_2m_max.first().unwrap()
  79. + weather.daily.temperature_2m_max.first().unwrap())
  80. / 2.0;
  81. let tomorrow = (weather.daily.temperature_2m_max.get(1).unwrap()
  82. + weather.daily.temperature_2m_max.get(1).unwrap())
  83. / 2.0;
  84. let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap()
  85. + weather.daily.temperature_2m_max.get(2).unwrap())
  86. / 2.0;
  87. rsx! {
  88. div { class: "px-6 pt-4 relative",
  89. div { class: "w-full h-px bg-gray-100 mb-4" }
  90. div { p { class: "text-center w-full mb-4", "👇 Forecast 📆" } }
  91. div { class: "text-center justify-between items-center flex",
  92. div { class: "text-center mb-0 flex items-center justify-center flex-col mx-4 w-16",
  93. span { class: "block my-1", "Today" }
  94. span { class: "block my-1", "{today}°" }
  95. }
  96. div { class: "text-center mb-0 flex items-center justify-center flex-col mx-8 w-16",
  97. span { class: "block my-1", "Tomorrow" }
  98. span { class: "block my-1", "{tomorrow}°" }
  99. }
  100. div { class: "text-center mb-0 flex items-center justify-center flex-col mx-2 w-30",
  101. span { class: "block my-1", "Past Tomorrow" }
  102. span { class: "block my-1", "{past_tomorrow}°" }
  103. }
  104. }
  105. }
  106. }
  107. }
  108. #[component]
  109. fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
  110. let mut input = use_signal(|| "".to_string());
  111. let locations = use_resource(move || async move {
  112. let current_location = input.read().clone();
  113. get_locations(&current_location).await
  114. });
  115. rsx! {
  116. div {
  117. div { class: "inline-flex flex-col justify-center relative text-gray-500",
  118. div { class: "relative",
  119. input {
  120. class: "p-2 pl-8 rounded-lg border border-gray-200 bg-gray-200 focus:bg-white focus:outline-none focus:ring-2 focus:ring-yellow-600 focus:border-transparent",
  121. placeholder: "Country name",
  122. "type": "text",
  123. autofocus: true,
  124. oninput: move |e| input.set(e.value())
  125. }
  126. svg {
  127. class: "w-4 h-4 absolute left-2.5 top-3.5",
  128. "viewBox": "0 0 24 24",
  129. fill: "none",
  130. stroke: "currentColor",
  131. xmlns: "http://www.w3.org/2000/svg",
  132. path {
  133. d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
  134. "stroke-linejoin": "round",
  135. "stroke-linecap": "round",
  136. "stroke-width": "2"
  137. }
  138. }
  139. }
  140. ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto",
  141. {
  142. if let Some(Ok(locs)) = locations.read().as_ref() {
  143. rsx! {
  144. {
  145. locs.iter().cloned().map(move |wl| {
  146. rsx! {
  147. li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
  148. onclick: move |_| country.set(wl.clone()),
  149. MapIcon {}
  150. b {
  151. "{wl.name}"
  152. }
  153. " · {wl.country}"
  154. }
  155. }
  156. })
  157. }
  158. }
  159. } else {
  160. rsx! { "loading locations..." }
  161. }
  162. }
  163. }
  164. }
  165. }
  166. }
  167. }
  168. fn MapIcon() -> Element {
  169. rsx! {
  170. svg {
  171. class: "stroke-current absolute w-4 h-4 left-2 top-2",
  172. stroke: "currentColor",
  173. xmlns: "http://www.w3.org/2000/svg",
  174. "viewBox": "0 0 24 24",
  175. fill: "none",
  176. path {
  177. "stroke-linejoin": "round",
  178. "stroke-width": "2",
  179. "stroke-linecap": "round",
  180. d: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
  181. }
  182. path {
  183. "stroke-linecap": "round",
  184. "stroke-linejoin": "round",
  185. d: "M15 11a3 3 0 11-6 0 3 3 0 016 0z",
  186. "stroke-width": "2"
  187. }
  188. }
  189. }
  190. }
  191. #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
  192. struct WeatherLocation {
  193. id: usize,
  194. name: String,
  195. latitude: f32,
  196. longitude: f32,
  197. country: String,
  198. }
  199. type WeatherLocations = Vec<WeatherLocation>;
  200. #[derive(Debug, Default, Serialize, Deserialize)]
  201. struct SearchResponse {
  202. results: WeatherLocations,
  203. }
  204. async fn get_locations(input: &str) -> reqwest::Result<WeatherLocations> {
  205. let res = reqwest::get(&format!(
  206. "https://geocoding-api.open-meteo.com/v1/search?name={input}"
  207. ))
  208. .await?
  209. .json::<SearchResponse>()
  210. .await?;
  211. Ok(res.results)
  212. }
  213. #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
  214. struct WeatherResponse {
  215. daily: DailyWeather,
  216. hourly: HourlyWeather,
  217. }
  218. #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
  219. struct HourlyWeather {
  220. time: Vec<String>,
  221. temperature_2m: Vec<f32>,
  222. }
  223. #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
  224. struct DailyWeather {
  225. temperature_2m_min: Vec<f32>,
  226. temperature_2m_max: Vec<f32>,
  227. }
  228. async fn get_weather(location: &WeatherLocation) -> reqwest::Result<WeatherResponse> {
  229. let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
  230. .await
  231. ?
  232. .json::<WeatherResponse>()
  233. .await
  234. ?;
  235. Ok(res)
  236. }