weather_app.rs 8.6 KB

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