From 1d02ce5e435faa9690a9b688999dc82dd1e06f44 Mon Sep 17 00:00:00 2001 From: Plucky Date: Thu, 11 Jul 2024 10:52:50 +0800 Subject: [PATCH] add chart --- Cargo.toml | 14 ++- index.html | 3 +- src/app.rs | 13 +-- src/components/menu/icons.rs | 157 +++++++++++++++++++++++++++ src/components/menu/index.rs | 65 +++++++++++ src/components/menu/mod.rs | 92 ++++++++++++++++ src/components/menu2.rs | 169 +++++++++++++++++++++++++++++ src/components/mod.rs | 3 +- src/components/sidebar.rs | 202 +---------------------------------- src/lib.rs | 3 +- src/main.rs | 3 +- src/modules/demo_data.rs | 123 ++++++++++++++++++--- src/modules/mod.rs | 14 +-- src/utils/mod.rs | 2 + src/utils/time.rs | 21 ++++ src/views/chart.rs | 26 +++++ src/views/dashboard.rs | 121 +++++---------------- src/views/forms.rs | 73 +++++++++---- src/views/js/chart1.js | 49 +++++++++ src/views/js/chart2.js | 56 ++++++++++ src/views/js/mod.rs | 0 src/views/login.rs | 19 +++- src/views/mod.rs | 17 +-- src/views/modal.rs | 4 +- src/views/tables.rs | 184 +++++++++++++------------------ tailwind.config.js | 2 + 26 files changed, 954 insertions(+), 481 deletions(-) create mode 100644 src/components/menu/icons.rs create mode 100644 src/components/menu/index.rs create mode 100644 src/components/menu/mod.rs create mode 100644 src/components/menu2.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/time.rs create mode 100644 src/views/chart.rs create mode 100644 src/views/js/chart1.js create mode 100644 src/views/js/chart2.js create mode 100644 src/views/js/mod.rs diff --git a/Cargo.toml b/Cargo.toml index d664317..002d1c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,25 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus = {version = "0.5", features = ["web", "router"]} #"fermi" +dioxus = {version = "0.5", features = ["web", "router"]} console_error_panic_hook = "0.1" dioxus-html-macro = "0.3" +dioxus-sdk = "*" + tracing = "0" tracing-wasm = "0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = {version = "1", features = ["time"]} + +[target.'cfg(target_arch = "wasm32")'.dependencies] +async-std = "1" +instant = {version = "^0.1", features = ["wasm-bindgen"]} + [profile.release] opt-level = 'z' lto = true diff --git a/index.html b/index.html index df67837..709d8de 100644 --- a/index.html +++ b/index.html @@ -6,12 +6,11 @@ + R-Dashboard
- - \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 5870014..0529956 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ /* * @Date: 2022-10-11 00:07:29 - * @LastEditTime: 2024-07-06 18:46:11 + * @LastEditTime: 2024-07-11 09:37:23 * @Description: */ #![allow(non_snake_case)] @@ -26,24 +26,17 @@ fn Login() -> Element { login::view() } -// fn Dashboard() -> Element { -// let router = router(); -// let url = router.current_route_string(); -// tracing::warn!("url: {}", url); -// Home("dashboard") -// } - #[component] fn NotFound(segments: Vec) -> Element { tracing::info!("segments: {:?}", segments); - if let Some(url) = segments.get(0) { + if let Some(url) = segments.first() { Body(url) } else { Body("dashboard") } } -// Home Page View +// Body Page View fn Body(url: impl AsRef) -> Element { let url = url.as_ref(); rsx! { diff --git a/src/components/menu/icons.rs b/src/components/menu/icons.rs new file mode 100644 index 0000000..960c042 --- /dev/null +++ b/src/components/menu/icons.rs @@ -0,0 +1,157 @@ +/* + * @Date: 2024-07-10 22:51:50 + * @LastEditTime: 2024-07-10 22:51:57 + */ +use dioxus::prelude::*; +use dioxus_html_macro::html; + +pub fn icon_logo() -> Element { + html! { + + + + + } +} +pub fn icon_chart() -> Element { + html! { + + + + + } +} + +pub fn icon_element() -> Element { + html!( + + + + + + + ) +} + +pub fn icon_table() -> Element { + html!( + + + + + + ) +} + +pub fn icon_form() -> Element { + html!( + + + + + ) +} + +pub fn icon_card() -> Element { + html!( + + + + + ) +} + +pub fn icon_model() -> Element { + html!( + + + + + + ) +} + +pub fn icon_blank() -> Element { + html!( + + + + ) +} + +// #[inline_props] +pub fn icon_up_down() -> Element { + html!( + + + + ) +} diff --git a/src/components/menu/index.rs b/src/components/menu/index.rs new file mode 100644 index 0000000..c558b55 --- /dev/null +++ b/src/components/menu/index.rs @@ -0,0 +1,65 @@ +/* + * @Date: 2024-07-09 22:37:33 + * @LastEditTime: 2024-07-11 09:37:45 + */ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_html_macro::html; + +use crate::components::menu::icons; + +use super::MenuItem; + +pub fn View(menus: Vec) -> Element { + html! { + + } +} + +fn render_menu_item(item: MenuItem) -> Element { + #[allow(deprecated)] + let route = use_router(); + let route_name = route.current_route_string(); + + let mut menu_open = use_signal(|| false); + let toggle_menu = if menu_open() { "block" } else { "hidden" }; + + let highlight_class = |e: &str| { + match e == route_name { + true => "flex items-center px-6 py-2 mt-2 duration-200 border-l-4 bg-gray-600 bg-opacity-25 text-gray-100 border-gray-100", + false => "flex items-center px-6 py-2 mt-2 duration-200 border-l-4 border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100", + } + }; + + let children = item.children; + html! { + + { item.icon} + { item.label } + {if !children.is_empty() { + html! { +
+ {icons::icon_up_down()} +
+ } + }else { + html! {} + }} + + { if !children.is_empty() { + html! { +
+ {children.into_iter().map(render_menu_item) } +
+ } + } else { + html! {} + }} + + } +} diff --git a/src/components/menu/mod.rs b/src/components/menu/mod.rs new file mode 100644 index 0000000..01bc6ab --- /dev/null +++ b/src/components/menu/mod.rs @@ -0,0 +1,92 @@ +/* + * @Date: 2024-07-10 09:14:38 + * @LastEditTime: 2024-07-10 22:48:32 + */ + +pub mod icons; +mod index; +use dioxus::dioxus_core::Element; +pub use index::*; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct MenuVo { + pub id: i32, + pub icon: String, + pub name: String, + pub path: String, + pub api_url: String, + pub menu_type: i32, + pub parent_id: i32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MenuItem { + pub id: i32, + pub key: String, + pub label: String, + pub icon: Element, + pub parent_id: i32, + pub children: Vec, +} + +impl From for MenuItem { + fn from(item: MenuVo) -> Self { + MenuItem { + id: item.id, + key: item.path, + label: item.name, + icon: icons::icon_blank(), + parent_id: item.parent_id, + children: Vec::new(), + } + } +} + +pub fn build_tree(data: Vec, pid: i32) -> Vec { + let mut result = Vec::new(); + + for item in data.iter() { + if item.parent_id == pid { + let mut node = item.clone(); + node.children = build_tree(data.clone(), item.id); + result.push(node); + } + } + + result +} + +#[test] +fn tree_test() { + // let data = vec![ + // MenuItem { + // id: 1, + // key: "item1".to_string(), + // label: "Item 1".to_string(), + // icon: "icon1".to_string(), + // parent_id: 0, + // children: vec![], + // }, + // MenuItem { + // id: 2, + // key: "item2".to_string(), + // label: "Item 2".to_string(), + // icon: "icon2".to_string(), + // parent_id: 1, + // children: vec![], + // }, + // MenuItem { + // id: 3, + // key: "item3".to_string(), + // label: "Item 3".to_string(), + // icon: "icon3".to_string(), + // parent_id: 1, + // children: vec![], + // }, + // ]; + + // let tree = build_tree(data, 0); + // dbg!("{:#?}", tree); +} diff --git a/src/components/menu2.rs b/src/components/menu2.rs new file mode 100644 index 0000000..b722d9e --- /dev/null +++ b/src/components/menu2.rs @@ -0,0 +1,169 @@ +use dioxus::prelude::*; +// use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Clone, PartialEq)] +pub struct Menus { + pub items: Vec, + pub on_click: EventHandler, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct MenuItem { + pub key: String, + pub icon: String, + pub label: String, + pub parent_key: Option, +} + +#[component] +pub fn Menu(cx: Menus) -> Element { + let render_item = |item: &MenuItem| { + let onclick = cx.on_click.clone(); + let key = item.key.clone(); + let children = find_children(&cx.items, &key); + let mut menu_open = use_signal(|| vec![false, false]); + + rsx! { + div { + class: "flex items-center px-6 py-2 mt-4 duration-200 border-l-4 border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100 cursor-pointer", + onclick: move |_| onclick.call(key.clone()), + div { + class: "flex items-center space-x-4", + span { "{item.icon}" }, // 这里假设图标是一个字符串 + span { "{item.label}" } + if !children.is_empty() { + div { + class: format!("{}", if menu_open()[0] { "rotate-180" } else { "" }), + icons::icon_up_down {} + } + } + } + if !children.is_empty() { + div { + class: format!("ml-8 mt-1 {}", if menu_open()[0] { "block" } else { "hidden" }), + {children}.iter().map(render_item).collect::>() + } + } + } + } + }; + + rsx! { + nav { + class: "mt-10", + // {cx}.items.iter().filter(|item| item.parent_key.is_none()).map(render_item).collect::>() + } + } +} + +fn find_children<'a>(items: &'a [MenuItem], parent_key: &str) -> Vec<&'a MenuItem> { + items + .iter() + .filter(|item| item.parent_key.as_ref().map_or(false, |key| key == parent_key)) + .collect() +} + +fn build_tree(items: Vec) -> Vec { + let mut map: HashMap = HashMap::new(); + let mut roots: Vec = Vec::new(); + + for item in items { + map.insert(item.key.clone(), item.clone()); + } + + for item in items { + if let Some(parent_key) = &item.parent_key { + if let Some(parent) = map.get_mut(parent_key) { + if parent.children.is_none() { + parent.children = Some(Vec::new()); + } + if let Some(children) = &mut parent.children { + children.push(item); + } + } + } else { + roots.push(item); + } + } + + roots +} + +// 使用示例 +// fn app(cx: Scope) -> Element { +// let menu_items = use_state(cx, || vec![]); + +// use_effect(cx, (), |_| { +// to_owned![menu_items]; +// async move { +// let client = Client::new(); +// let response = client +// .get("https://your-api-endpoint.com/menu") +// .send() +// .await +// .unwrap() +// .json::>() +// .await +// .unwrap(); +// let tree = build_tree(response); +// menu_items.set(tree); +// } +// }); + +// cx.render(rsx! { +// Menu { +// items: menu_items.get().clone(), +// on_click: move |key| { +// // 处理点击事件 +// } +// } +// }) +// } + +mod icons { + use dioxus::prelude::*; + use dioxus_html_macro::html; + pub fn icon_up_down() -> Element { + html!( + + + + ) + } +} + +fn render_menu_item(item: MenuItem) -> Element { + let children = item.children; + let mut menu_open = use_signal(|| false); + let toggle_menu = if menu_open() { "block" } else { "hidden" }; + tracing::info!("menu_open: {}", toggle_menu); + rsx! { + div{ + class:"flex items-center px-6 py-2 mt-4 duration-200 border-l-4 border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100 cursor-pointer ", + + // aria_controls="dropdown-example" + "x-data": "{{ open: false }}", + + a{ id: "dropdown-example", class:"flex items-center space-x-4", + href:{item.key}, + // onclick = { move|_| { menu_open.set(!menu_open())}}> + { item.icon} + span{ class:"mx-4", { item.label } } + } + + } + { if !children.is_empty() { + rsx! { + div{ class:"ml-8 mt-1 { toggle_menu }", + {children.into_iter().map(|item| render_menu_item(item)) } + } + } + } else { + rsx! {} + }} + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 5e8401a..0f2c8d2 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,12 +1,13 @@ /* * @Date: 2022-10-11 23:13:17 - * @LastEditTime: 2024-07-06 00:05:28 + * @LastEditTime: 2024-07-09 22:37:24 * @Description: */ use dioxus::signals::{GlobalSignal, Signal}; pub mod header; +pub mod menu; pub mod sidebar; // #[derive(Clone, Copy)] diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 091b519..93651b2 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,26 +1,20 @@ /* * @Date: 2022-10-11 18:53:17 - * @LastEditTime: 2024-07-07 21:42:00 + * @LastEditTime: 2024-07-11 10:48:17 * @Description: */ use dioxus::prelude::*; +use crate::{components::menu, modules::demo_data::MENUS}; + use super::SIDEBAR_OPEN; pub fn view() -> Element { - let route = router(); - let route_name = route.current_route_string(); // 共享状态 let mut sidebar_open = use_hook(|| SIDEBAR_OPEN.signal()); - let mut menu_open = use_signal(|| vec![false, false]); - let highlight_class = |e: &str| { - match e == route_name { - true => "flex items-center px-6 py-2 mt-4 duration-200 border-l-4 bg-gray-600 bg-opacity-25 text-gray-100 border-gray-100", - false => "flex items-center px-6 py-2 mt-4 duration-200 border-l-4 border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100", - } - }; + let menus = use_hook(|| MENUS.signal()); let toggle_sidebar = if sidebar_open() { "translate-x-0 ease-out" @@ -50,67 +44,8 @@ pub fn view() -> Element { } } // menu - nav { class: "mt-10 ", - Link { class: highlight_class("dashboard"), to: "/dashboard", - icons::icon_chart {} - span { class: "mx-4", "Dashboard" } - } - Link { class: highlight_class("ui-elements"), to: "/ui-elements", - icons::icon_win {} - span { class: "mx-4", "UI Elements" } - } - Link { class: highlight_class("tables"), to: "/tables", - icons::icon_table {} - span { class: "mx-4", "Tables" } - } - Link { class: highlight_class("forms"), to: "/forms", - icons::icon_form {} - span { class: "mx-4", "Forms" } - } - Link { class: highlight_class("cards"), to: "/cards", - icons::icon_card {} - span { class: "mx-4", "Cards" } - } - Link { class: highlight_class("modal"), to: "/modal", - icons::icon_model {} - span { class: "mx-4", "Modal" } - } + {menu::View(menus())} - Link { class: highlight_class("blank"), to: "/blank", - icons::icon_blank {} - span { class: "mx-4", "Blank" } - } - - // ul{li{}} - div { - div { - class: "flex items-center px-6 py-2 mt-4 duration-200 border-l-4 border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100 cursor-pointer", - onclick: move |_| { - let mut is_menu_open = menu_open.write(); - is_menu_open[0] = !is_menu_open[0]; - }, - - div { class: "flex items-center space-x-4", - icons::icon_chart {} - span { "Test" } - div { class: format!("{}", if menu_open()[0] { "rotate-180" } else { "" }), - icons::icon_up_down {} - } - } - } - - div { class: format!("ml-8 mt-1 {}", if menu_open()[0] { "block" } else { "hidden" }), - Link { class: highlight_class("blank"), to: "/blank", - icons::icon_blank {} - span { class: "mx-4", "Blank" } - } - Link { class: highlight_class("blank2"), to: "/blank", - icons::icon_blank {} - span { class: "mx-4", "Blank" } - } - } - } - } } ) } @@ -142,131 +77,4 @@ mod icons { } } - pub fn icon_chart() -> Element { - html! { - - - - - } - } - - pub fn icon_win() -> Element { - html!( - - - - - - - ) - } - - pub fn icon_table() -> Element { - html!( - - - - - - ) - } - - pub fn icon_form() -> Element { - html!( - - - - - ) - } - - pub fn icon_card() -> Element { - html!( - - - - - ) - } - - pub fn icon_model() -> Element { - html!( - - - - - - ) - } - - pub fn icon_blank() -> Element { - html!( - - - - ) - } - - // #[inline_props] - pub fn icon_up_down() -> Element { - html!( - - - - ) - } } diff --git a/src/lib.rs b/src/lib.rs index 07c2f5b..56828ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ /* * @Date: 2022-10-11 00:07:45 - * @LastEditTime: 2024-07-05 18:29:21 + * @LastEditTime: 2024-07-08 23:28:44 * @Description: */ + diff --git a/src/main.rs b/src/main.rs index d1d1ed0..f1a1724 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ /* * @Date: 2022-10-10 23:58:17 - * @LastEditTime: 2024-07-06 18:52:26 + * @LastEditTime: 2024-07-08 23:28:50 * @Description: */ pub mod app; pub mod components; pub mod modules; +pub mod utils; pub mod views; use app::App; diff --git a/src/modules/demo_data.rs b/src/modules/demo_data.rs index a9e442e..481143a 100644 --- a/src/modules/demo_data.rs +++ b/src/modules/demo_data.rs @@ -1,18 +1,22 @@ /* * @Date: 2022-10-14 18:11:55 - * @LastEditTime: 2024-07-05 12:38:05 + * @LastEditTime: 2024-07-10 22:53:56 * @Description: */ use dioxus::signals::{GlobalSignal, Signal}; +use crate::components::{ + menu::icons, + menu::{build_tree, MenuItem}, +}; + use super::*; #[derive(Clone, PartialEq)] pub struct UseTableData { pub simpleTableData: Vec, pub paginatedTableData: Vec, - pub wideTableData: Vec, } /// 表格测试数据 for tables @@ -65,16 +69,109 @@ pub static USE_TABLE_DATA: GlobalSignal = Signal::global(|| { status: "Inactive".to_string(), statusColor: "red".to_string(), }, - ], - wideTableData: (0..5).map(|_i| { - WideTableData { - name: "John Doe".into(), - email: "john@example.com".into(), - title: "Software Engineer".into(), - title2: "Web dev".into(), - status: "Active".into(), - role: "Owner".into(), - } - }).collect(), + ] } }); + +// test data +pub static USERS: GlobalSignal> = Signal::global(|| { + (0..5) + .map(|_i| User { + name: "John Doe".into(), + email: "john@example.com".into(), + title: "Software Engineer".into(), + title2: "Web dev".into(), + status: if _i % 2 == 0 { "Active" } else { "Inactive" }.into(), + role: "Owner".into(), + }) + .collect() +}); + +// test data +pub static MENUS: GlobalSignal> = Signal::global(|| { + let data = vec![ + MenuItem { + id: 1, + key: "/dashboard".to_string(), + label: "Dashboard".to_string(), + // icon: "icon_chart".to_string(), + icon: icons::icon_chart(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 2, + key: "/ui-elements".to_string(), + label: "UI Elements".to_string(), + // icon: "icon_element".to_string(), + icon: icons::icon_element(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 3, + key: "/tables".to_string(), + label: "Tables".to_string(), + // icon: "icon_table".to_string(), + icon: icons::icon_table(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 4, + key: "/forms".to_string(), + label: "Forms".to_string(), + // icon: "icon_form".to_string(), + icon: icons::icon_form(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 5, + key: "/cards".to_string(), + label: "Cards".to_string(), + // icon: "icon_card".to_string(), + icon: icons::icon_card(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 6, + key: "/modal".to_string(), + label: "Modal".to_string(), + // icon: "icon_model".to_string(), + icon: icons::icon_model(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 7, + key: "#".to_string(), + label: "Test".to_string(), + // icon: "icon_element".to_string(), + icon: icons::icon_element(), + parent_id: 0, + children: vec![], + }, + MenuItem { + id: 8, + key: "/blank1".to_string(), + label: "Blank".to_string(), + // icon: "icon_blank".to_string(), + icon: icons::icon_blank(), + parent_id: 7, + children: vec![], + }, + MenuItem { + id: 9, + key: "/blank2".to_string(), + label: "Blank".to_string(), + // icon: "icon_blank".to_string(), + icon: icons::icon_blank(), + parent_id: 7, + children: vec![], + }, + ]; + + build_tree(data, 0) +}); diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 2c8d752..2940e1f 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,3 +1,8 @@ +/* + * @Date: 2024-07-06 00:18:55 + * @LastEditTime: 2024-07-09 11:37:47 + */ + #![allow(non_snake_case)] pub mod demo_data; @@ -28,12 +33,3 @@ pub struct PaginatedTableData { pub status: String, pub statusColor: String, } -#[derive(Debug, Clone, PartialEq)] -pub struct WideTableData { - pub name: String, - pub email: String, - pub title: String, - pub title2: String, - pub status: String, - pub role: String, -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..fbf6a31 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ + +pub mod time; diff --git a/src/utils/time.rs b/src/utils/time.rs new file mode 100644 index 0000000..b1be02a --- /dev/null +++ b/src/utils/time.rs @@ -0,0 +1,21 @@ +/* + * @Date: 2024-07-08 23:25:28 + * @LastEditTime: 2024-07-09 11:19:51 + */ +#![allow(unused_imports)] +#[cfg(target_arch = "wasm32")] +use async_std::task::sleep; +#[cfg(target_arch = "wasm32")] +use instant::{Duration, Instant}; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{Duration, Instant}; +#[cfg(not(target_arch = "wasm32"))] +use tokio::time::sleep; + +pub async fn sleep_ms(ms: u64) { + sleep(Duration::from_millis(ms)).await; +} + +pub async fn instant() -> Instant { + Instant::now() +} diff --git a/src/views/chart.rs b/src/views/chart.rs new file mode 100644 index 0000000..6916116 --- /dev/null +++ b/src/views/chart.rs @@ -0,0 +1,26 @@ +/* + * @Date: 2024-07-08 16:13:27 + * @LastEditTime: 2024-07-11 09:09:54 + */ +use dioxus::prelude::*; + +use crate::utils::time::sleep_ms; + +pub fn eval_chart() { + eval_chart1(); + eval_chart2(); +} +pub fn eval_chart1() { + let script = include_str!("js/chart1.js"); + let _eval = eval(script); + + spawn(async move { + sleep_ms(1000).await; + _eval.send((vec![148, 150, 130, 170]).into()).unwrap(); + }); +} + +pub fn eval_chart2() { + let script = include_str!("js/chart2.js"); + let _eval = eval(script); +} diff --git a/src/views/dashboard.rs b/src/views/dashboard.rs index 6142c66..1ce5625 100644 --- a/src/views/dashboard.rs +++ b/src/views/dashboard.rs @@ -1,29 +1,17 @@ /* * @Date: 2022-10-14 12:31:43 - * @LastEditTime: 2024-07-05 12:56:53 + * @LastEditTime: 2024-07-11 09:10:07 * @Description: */ use dioxus::prelude::*; -use crate::modules::User; - -// test data -pub static USERS: GlobalSignal> = Signal::global(|| { - (0..5) - .map(|_i| User { - name: "John Doe".into(), - email: "john@example.com".into(), - title: "Software Engineer".into(), - title2: "Web dev".into(), - status: "Active".into(), - role: "Owner".into(), - }) - .collect() -}); +use crate::views::chart::*; pub fn view() -> Element { - let users = use_hook(|| USERS.signal()); + spawn(async move { + eval_chart(); + }); rsx! { div { @@ -33,7 +21,7 @@ pub fn view() -> Element { div { class: "w-full px-6 sm:w-1/2 xl:w-1/3", div { class: "flex items-center px-5 py-6 bg-white rounded-md shadow-sm", div { class: "p-3 bg-indigo-600 bg-opacity-75 rounded-full", - icons::icon_1 {} + icons::icon_users {} } div { class: "mx-5", h4 { class: "text-2xl font-semibold text-gray-700", @@ -46,7 +34,7 @@ pub fn view() -> Element { div { class: "w-full px-6 mt-6 sm:w-1/2 xl:w-1/3 sm:mt-0", div { class: "flex items-center px-5 py-6 bg-white rounded-md shadow-sm", div { class: "p-3 bg-blue-600 bg-opacity-75 rounded-full", - icons::icon_2 {} + icons::icon_go {} } div { class: "mx-5", h4 { class: "text-2xl font-semibold text-gray-700", @@ -59,7 +47,7 @@ pub fn view() -> Element { div { class: "w-full px-6 mt-6 sm:w-1/2 xl:w-1/3 xl:mt-0", div { class: "flex items-center px-5 py-6 bg-white rounded-md shadow-sm", div { class: "p-3 bg-pink-600 bg-opacity-75 rounded-full", - icons::icon_3 {} + icons::icon_products {} } div { class: "mx-5", h4 { class: "text-2xl font-semibold text-gray-700", @@ -73,86 +61,25 @@ pub fn view() -> Element { } div { class: "mt-8" } - div { class: "flex flex-col mt-8", - div { class: "py-2 -my-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8", - div { class: "inline-block min-w-full overflow-hidden align-middle border-b border-gray-200 shadow sm:rounded-lg", - table { class: "min-w-full", - thead { - tr { - th { class: "px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50", - "Name" - } - th { class: "px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50", - "Title" - } - th { class: "px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50", - "Status" - } - th { class: "px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50", - "Role" - } - th { class: "px-6 py-3 border-b border-gray-200 bg-gray-50" } - } - } - // 表格数据 - tbody { class: "bg-white", - {users}.iter().map(|u|{ - rsx!{ UserList{user:u.clone()}} - }) - } - } - } - } - } - } - } -} -// #[derive(PartialEq, Clone, Props)] -// pub struct UserListProps { -// user: User, -// } - -#[allow(non_snake_case)] -#[component] -pub fn UserList(user: User) -> Element { - // let u = cx.props.user; - let u = user; - rsx! { - tr { - // key: "{index}", - td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - div { class: "flex items-center", - div { class: "flex-shrink-0 w-10 h-10", - img { - class: "w-10 h-10 rounded-full", - alt: "", - src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" - } + div { class: "flex flex-wrap mt-6", + div { class: "w-full lg:w-1/2 pr-0 lg:pr-2", + p { class: "text-xl pb-3 flex items-center", + "Monthly Reports" } - div { class: "ml-4", - div { class: "text-sm font-medium leading-5 text-gray-900", - "{ u.name }" - } - div { class: "text-sm leading-5 text-gray-500", "{ u.email }" } + div { class: "p-6 bg-white", + canvas {id: "chart1" } } } - } - td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - div { class: "text-sm leading-5 text-gray-900", "{ u.title }" } - div { class: "text-sm leading-5 text-gray-500", "{ u.title2 }" } - } - td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - span { class: "inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full", - "{ u.status }" + div { class: "w-full lg:w-1/2 pl-0 lg:pl-2 mt-6 lg:mt-0", + p { class: "text-xl pb-3 flex items-center", + "Resolved Reports" + } + div { class: "p-6 bg-white", + canvas { id: "chart2" } + } } } - td { class: "px-6 py-4 text-sm leading-5 text-gray-500 border-b border-gray-200 whitespace-nowrap", - "{ u.role }" - } - td { class: "px-6 py-4 text-sm font-medium leading-5 text-right border-b border-gray-200 whitespace-nowrap", - a { class: "text-indigo-600 hover:text-indigo-900", href: "#", "Edit" } - } } } } @@ -161,7 +88,7 @@ mod icons { use dioxus::prelude::*; use dioxus_html_macro::html; - pub fn icon_1() -> Element { + pub fn icon_users() -> Element { html! { Element { + pub fn icon_go() -> Element { html! { Element { + pub fn icon_products() -> Element { html! { Element { @@ -64,7 +64,8 @@ fn Model_form() -> Element { class: "px-3 py-1 text-sm text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 focus:outline-none", "Cancel" } - button { class: "px-3 py-1 text-sm text-white bg-indigo-600 rounded-md hover:bg-indigo-500 focus:outline-none", + button { + class: "px-3 py-1 text-sm text-white bg-indigo-600 rounded-md hover:bg-indigo-500 focus:outline-none", "Save" } } @@ -78,14 +79,10 @@ fn Model_form() -> Element { // Forms fn Forms() -> Element { let mut user = use_signal(User::default); - let ur = user.peek(); - // let User { - // username, - // email, - // password, - // confirm, - // .. - // } = &*user.read(); + let u = user.peek(); + let mut confirm = use_signal(|| "".to_string()); + + let mut errors = use_signal(|| vec![None::; 4]); rsx! { div { class: "mt-8", @@ -95,11 +92,23 @@ fn Forms() -> Element { h2 { class: "text-lg font-semibold text-gray-700 capitalize", "Account settings" } form { - //action="" methods="post" - prevent_default: "onsubmit", - onsubmit: move |e| { - info!("onsubmit: {:?}", e); - info!("onsubmit: {:?}", user.peek()); + // prevent_default: "onsubmit", + onsubmit: move |_e| { + info!("onsubmit: {:?}", user); + let u = user.read(); + let mut errors = errors.write(); + errors[0] = if u.username.is_empty() { Some("Username is required".to_string()) } else { None }; + errors[1] = if u.email.is_empty() { Some("Email is required".to_string()) } else { None }; + errors[2] = if u.password.is_empty() { Some("Password is required".to_string()) } else { None }; + errors[3] = if confirm().is_empty() { Some("Confirm is required".to_string()) } else { None }; + + for e in errors.iter() { + if e.is_some() { + return; + } + }; + + tracing::info!("onsubmit ok"); }, div { class: "grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2", @@ -109,15 +118,18 @@ fn Forms() -> Element { id: "username", class: "w-full mt-2 c-input", r#type: "text", - // "v-model: "user.username", - // 双向绑定 - value: "{ur.username}", + value: "{u.username}", oninput: { move |e| { user.write().username = e.value(); } } } + {errors()[0].as_ref().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } div { label { class: "text-gray-700", r#for: "email", "Email Address" } @@ -125,13 +137,18 @@ fn Forms() -> Element { id: "email", class: "w-full mt-2 c-input", r#type: "email", - value: "{ur.email}", + value: "{u.email}", oninput: { move |e| { user.write().email = e.value(); } } } + {errors()[1].as_ref().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } div { label { class: "text-gray-700", r#for: "password", "Password" } @@ -139,13 +156,18 @@ fn Forms() -> Element { id: "password", class: "w-full mt-2 c-input", r#type: "password", - value: "{ur.password}", + value: "{u.password}", oninput: { move |e| { user.write().password = e.value(); } } } + {errors()[2].as_ref().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } div { label { @@ -157,13 +179,18 @@ fn Forms() -> Element { id: "pwConfirm", class: "w-full mt-2 c-input", r#type: "password", - value: "{ur.confirm}", + value: "{confirm}", oninput: { move |e| { - user.write().confirm = e.value(); + confirm.set(e.value()); } } } + {errors()[3].as_ref().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } } div { class: "flex justify-end mt-4", diff --git a/src/views/js/chart1.js b/src/views/js/chart1.js new file mode 100644 index 0000000..568c46d --- /dev/null +++ b/src/views/js/chart1.js @@ -0,0 +1,49 @@ +const eval_chart1 = (function () { + const ctx = document.getElementById('chart1'); + let chartdata = [148, 150, 130, 170]; + const data = { + labels: [ + 'Food & beverages', + 'Groceries', + 'Gaming', + 'Trip & holiday', + ], + datasets: [{ + label: 'Total Expenses', + data: chartdata, + backgroundColor: [ + '#3B82F6', + '#10B981', + '#6366F1', + '#F59E0B' + ] + }] + }; + const config = { + type: 'bar', //polarArea, bar, line, doughnut, pie, radar, scatter + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + }, + } + } + }; + const chart = new Chart(ctx, config); + // 接收 Rust 发送的数据并更新图表 + (async function () { + while (true) { + const msg = await dioxus.recv(); + // console.log(msg); + chart.data.datasets[0].data = msg; + chart.update(); + } + })(); +}); + +setTimeout(() => { + eval_chart1(); +}, 100); \ No newline at end of file diff --git a/src/views/js/chart2.js b/src/views/js/chart2.js new file mode 100644 index 0000000..de68ee7 --- /dev/null +++ b/src/views/js/chart2.js @@ -0,0 +1,56 @@ +const eval_chart2 = (function () { + const ctx = document.getElementById('chart2'); + let chartdata = [12, 19, 3, 5, 2, 3]; + const data = { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: chartdata, + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }; + const config = { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + }, + }, + + } + }; + const chart = new Chart(ctx, config); + + (async function () { + while (true) { + const msg = await dioxus.recv(); + // console.log(msg); + chart.data.datasets[0].data = msg; + chart.update(); + } + })(); +}); + +setTimeout(() => { + eval_chart2(); +}, 100); \ No newline at end of file diff --git a/src/views/js/mod.rs b/src/views/js/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/views/login.rs b/src/views/login.rs index 7855430..a753f56 100644 --- a/src/views/login.rs +++ b/src/views/login.rs @@ -1,6 +1,6 @@ /* * @Date: 2022-10-12 00:00:54 - * @LastEditTime: 2024-07-05 20:06:58 + * @LastEditTime: 2024-07-11 09:54:59 * @Description: */ use dioxus::prelude::*; @@ -8,6 +8,8 @@ use dioxus::prelude::*; pub fn view() -> Element { let mut email = use_signal(|| "".to_string()); let mut password = use_signal(|| "".to_string()); + let mut email_error = use_signal(|| None::); + let mut password_error = use_signal(|| None::); fn login() { let router = router(); @@ -25,6 +27,11 @@ pub fn view() -> Element { form { class: "mt-4", onsubmit: move |_| { + email_error.set(if email().is_empty() { Some("Email is required".to_string()) } else { None }); + password_error.set(if password().is_empty() { Some("Password is required".to_string())} else {None}); + if email().is_empty() || password().is_empty() { + // return; + } login(); }, label { class: "block", @@ -38,6 +45,11 @@ pub fn view() -> Element { email.set(e.value()); } } + {email_error().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } label { class: "block mt-3", span { class: "text-sm text-gray-700", "Password" } @@ -50,6 +62,11 @@ pub fn view() -> Element { password.set(e.value()); } } + {password_error().map(|error| { + rsx! { + p { class: "mt-2 text-sm text-red-600", "{error}" } + } + })} } div { class: "flex items-center justify-between mt-4", div { diff --git a/src/views/mod.rs b/src/views/mod.rs index 2bc5980..74c107e 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -1,23 +1,16 @@ /* - * @Date: 2022-10-11 23:13:24 - * @LastEditTime: 2022-10-15 09:53:40 - * @Description: - */ +* @Date: 2022-10-11 23:13:24 + * @LastEditTime: 2024-07-09 11:06:42 +* @Description: +*/ pub mod blank; pub mod card; +pub mod chart; pub mod dashboard; pub mod forms; pub mod login; pub mod modal; pub mod tables; pub mod ui_elements; - -// pub fn view()->Element{ - -// (rsx!{ -// div { -// } -// }) -// } diff --git a/src/views/modal.rs b/src/views/modal.rs index 14be279..edc35b5 100644 --- a/src/views/modal.rs +++ b/src/views/modal.rs @@ -1,6 +1,6 @@ /* * @Date: 2022-10-15 09:52:14 - * @LastEditTime: 2024-07-07 21:21:12 + * @LastEditTime: 2024-07-09 16:42:32 * @Description: */ @@ -26,7 +26,7 @@ pub fn view() -> Element { div { class: format!( "transition:opacity 0.25s ease {} z-50 fixed w-full h-full top-0 left-0 flex items-center justify-center", - if open() { "false" } else { "opacity-0 pointer-events-none" }, + if open() { "" } else { "opacity-0 pointer-events-none" }, ), // overlay div { class: "absolute w-full h-full bg-gray-900 opacity-50 modal-overlay" } diff --git a/src/views/tables.rs b/src/views/tables.rs index 0336b3f..dff07b5 100644 --- a/src/views/tables.rs +++ b/src/views/tables.rs @@ -1,12 +1,15 @@ /* * @Date: 2022-10-14 17:43:39 - * @LastEditTime: 2024-07-07 21:20:43 + * @LastEditTime: 2024-07-09 16:38:14 * @Description: */ #![allow(non_snake_case)] use dioxus::prelude::*; -use crate::modules::demo_data::USE_TABLE_DATA; +use crate::modules::{ + demo_data::{USERS, USE_TABLE_DATA}, + User, +}; pub fn view() -> Element { rsx! { @@ -71,16 +74,15 @@ fn Simple_table() -> Element { } // 分页 table -// Table with pagination fn Table_with_pagination() -> Element { let table_data = use_hook(|| USE_TABLE_DATA.signal()); let paginated_table_data = &table_data.read().paginatedTableData; let status_color = |status: &str| match status { - "Active" => ("bg-green-100", "text-green-800"), - "Inactive" => ("bg-red-100", "text-red-800"), - "Suspended" => ("bg-orange-100", "text-orange-800"), - _ => ("bg-gray-100", "text-gray-800"), + "Active" => "bg-green-100 text-green-800", + "Inactive" => "bg-red-100 text-red-800", + "Suspended" => "bg-orange-100 text-orange-800", + _ => "bg-gray-100 text-gray-800", }; rsx! { @@ -97,25 +99,21 @@ fn Table_with_pagination() -> Element { option { "10" } option { "20" } } - div { class: "absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 pointer-events-none", - icons::icon_1 {} - } + } div { class: "relative", - select { class: "block w-full h-full px-4 py-2 pr-8 leading-tight text-gray-700 bg-white border-t border-b border-r border-gray-400 rounded-r appearance-none sm:rounded-r-none sm:border-r-0 focus:outline-none focus:border-l focus:border-r focus:bg-white focus:border-gray-500", + select { class: "block w-full h-full px-4 py-2 pr-8 leading-tight text-gray-700 bg-white border border-gray-400 rounded-r appearance-none sm:rounded-r-none sm:border-r-0 focus:outline-none focus:border-l focus:border-r focus:bg-white focus:border-gray-500", option { "All" } option { "Active" } option { "Inactive" } } - div { class: "absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 pointer-events-none", - icons::icon_2 {} - } + } } div { class: "relative block mt-2 sm:mt-0", span { class: "absolute inset-y-0 left-0 flex items-center pl-2", - icons::icon_3 {} + icons::icon_search {} } input { class: "block w-full py-2 pl-8 pr-6 text-sm text-gray-700 placeholder-gray-400 bg-white border border-b border-gray-400 rounded-l rounded-r appearance-none sm:rounded-l-none focus:bg-white focus:placeholder-gray-600 focus:text-gray-700 focus:outline-none", @@ -188,26 +186,10 @@ fn Table_with_pagination() -> Element { td { class: "px-5 py-5 text-sm bg-white border-b border-gray-200", span { - class: format!("inline-flex px-3 py-1 font-semibold leading-tight rounded-full {} {}",status_color(&u.status).0,status_color(&u.status).1), + class: "inline-flex px-3 py-1 font-semibold leading-tight rounded-full { status_color(&u.status)} ", "{ u.status }" } - // span { - // class: format!("relative inline-block px-3 py-1 font-semibold leading-tight {}", status_color(&u.status).1), - // span { - // aria_hidden: "true", - // class: { - // format!("absolute inset-0 opacity-50 rounded-full {}", status_color(&u.status).0) - // } - // } - // span { - // class: "relative", - // "{u.status}" - - // } - // } - - } //tr end } @@ -238,8 +220,8 @@ fn Table_with_pagination() -> Element { // 宽表格 fn Wide_table() -> Element { - let table_data = use_hook(|| USE_TABLE_DATA.signal()); - let wide_table_data = &table_data.read().wideTableData; + let users = use_hook(|| USERS.signal()); + rsx! { div { class: "mt-8", h4 { class: "text-gray-600", "Wide Table" } @@ -264,68 +246,11 @@ fn Wide_table() -> Element { th { class: "px-6 py-3 bg-gray-100 border-b border-gray-200" } } } - // data + // 表格数据 tbody { class: "bg-white", - // iter start - {wide_table_data}.iter().map(|u|{rsx!{tr{ - - td { - class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - div { - class: "flex items-center", - div { - class: "flex-shrink-0 w-10 h-10", - img { - class: "w-10 h-10 rounded-full", - alt: "profile pic", - src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - } - } - div { - class: "ml-4", - div { - class: "text-sm font-medium leading-5 text-gray-900", - "{u.name}" - } - div { - class: "text-sm leading-5 text-gray-500", - "{ u.email }" - } - } - } - } - td { - class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - div { - class: "text-sm leading-5 text-gray-900", - "{ u.title }" - } - div { - class: "text-sm leading-5 text-gray-500", - "{ u.title2 }" - } - } - td { - class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", - span { - class: "inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full", - "{ u.status }" - } - } - td { - class: "px-6 py-4 text-sm leading-5 text-gray-500 border-b border-gray-200 whitespace-nowrap", - "{ u.role }" - } - td { - class: "px-6 py-4 text-sm font-medium leading-5 text-right border-b border-gray-200 whitespace-nowrap", - a { - class: "text-indigo-600 hover:text-indigo-900", - href: "#","Edit" - } - } - - // iter end - }}}) + {users}.iter().map(|u|{ + rsx!{ UserList{u: u.clone()}} + }) } } } @@ -335,25 +260,62 @@ fn Wide_table() -> Element { } } +#[allow(non_snake_case)] +#[component] +pub fn UserList(u: User) -> Element { + // let u = cx.props.user; + let status_color = |status: &str| match status { + "Active" => "bg-green-100 text-green-800", + "Inactive" => "bg-red-100 text-red-800", + "Suspended" => "bg-orange-100 text-orange-800", + _ => "bg-gray-100 text-gray-800", + }; + + rsx! { + tr { + // key: "{index}", + td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", + div { class: "flex items-center", + div { class: "flex-shrink-0 w-10 h-10", + img { + class: "w-10 h-10 rounded-full", + alt: "", + src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + } + } + div { class: "ml-4", + div { class: "text-sm font-medium leading-5 text-gray-900", + "{ u.name }" + } + div { class: "text-sm leading-5 text-gray-500", "{ u.email }" } + } + } + } + td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", + div { class: "text-sm leading-5 text-gray-900", "{ u.title }" } + div { class: "text-sm leading-5 text-gray-500", "{ u.title2 }" } + } + td { class: "px-6 py-4 border-b border-gray-200 whitespace-nowrap", + span { class: "inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full {status_color(&u.status)}", + "{ u.status }" + } + } + td { class: "px-6 py-4 text-sm leading-5 text-gray-500 border-b border-gray-200 whitespace-nowrap", + "{ u.role }" + } + td { class: "px-6 py-4 text-sm font-medium leading-5 text-right border-b border-gray-200 whitespace-nowrap", + a { class: "text-indigo-600 hover:text-indigo-900", href: "#", "Edit" } + } + } + } +} + mod icons { use dioxus::prelude::*; use dioxus_html_macro::html; - pub fn icon_1() -> Element { - html! { - - - - } - } - - pub fn icon_2() -> Element { + #[allow(unused)] + pub fn icon_down() -> Element { html! { Element { + pub fn icon_search() -> Element { html! {