use gloo_net::http::Request; use gloo_storage::{LocalStorage, Storage}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::rc::Rc; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; const STORAGE_KEY: &str = "sidebar_state"; #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct SidebarExpandState { pub ticket_open: bool, pub users_open: bool, } impl Default for SidebarExpandState { fn default() -> Self { Self { ticket_open: false, users_open: false, } } } #[derive(Clone, PartialEq)] pub struct SidebarState { pub expand: SidebarExpandState, pub set_tickets_open: Callback, pub toggle_tickets: Callback<()>, pub set_users_open: Callback, pub toggle_users: Callback<()>, } impl SidebarState { fn new( expand: SidebarExpandState, set_tickets_open: Callback, toggle_tickets: Callback<()>, set_users_open: Callback, toggle_users: Callback<()>, ) -> Self { Self { expand, set_tickets_open, toggle_tickets, set_users_open, toggle_users, } } } #[derive(Properties, PartialEq)] pub struct SidebarProps { pub children: Children, } #[component(SidebarStateProvider)] pub fn sidebar_state_provider(props: &SidebarProps) -> Html { let default = LocalStorage::get(STORAGE_KEY).unwrap_or_else(|_| SidebarExpandState::default()); let state = use_state(|| default); { let state = state.clone(); use_effect_with(state, move |s| { LocalStorage::set(STORAGE_KEY, &**s).ok(); || () }); } let set_tickets_open = { let state = state.clone(); Callback::from(move |v: bool| { state.set(SidebarExpandState { ticket_open: v, users_open: (*state).users_open, }) }) }; let toggle_tickets = { let state = state.clone(); Callback::from(move |_| { let current = (*state).ticket_open; state.set(SidebarExpandState { ticket_open: !current, users_open: (*state).users_open, }); }) }; let set_users_open = { let state = state.clone(); Callback::from(move |v: bool| { state.set(SidebarExpandState { ticket_open: (*state).ticket_open, users_open: v, }) }) }; let toggle_users = { let state = state.clone(); Callback::from(move |_| { let current = (*state).users_open; state.set(SidebarExpandState { ticket_open: (*state).ticket_open, users_open: !current, }); }) }; let ctx = SidebarState::new( (*state).clone(), set_tickets_open, toggle_tickets, set_users_open, toggle_users, ); html! { > context={Rc::new(ctx)}> { for props.children.iter() } >> } } #[component(TicketMenu)] pub fn ticket_menu() -> Html { let ctx = use_context::>().expect("TicketsMenu must be inside SidebarStateProvider"); let on_toggle = { let cb = ctx.toggle_tickets.clone(); Callback::from(move |_| cb.emit(())) }; let open = ctx.expand.ticket_open; html! {
  • { if open { html! { } } else { html!{} } }
  • } } #[component(UsersMenu)] pub fn users_menu() -> Html { let ctx = use_context::>().expect("UsersMenu must be inside SidebarStateProvider"); let on_toggle = { let cb = ctx.toggle_users.clone(); Callback::from(move |_| cb.emit(())) }; let open = ctx.expand.users_open; html! {
  • { if open { html! { } } else { html!{} } }
  • } } #[component(Sidebar)] pub fn sidebar() -> Html { let is_admin = use_state(|| None::); { let is_admin = is_admin.clone(); use_effect_with((), move |_| { spawn_local(async move { let response = Request::get("/api/users/current") .credentials(web_sys::RequestCredentials::Include) .send() .await; match response { Ok(resp) if resp.status() == 200 => { let user_data: Value = resp.json().await.unwrap_or_default(); let admin_value = user_data["data"]["is_admin"].as_bool(); is_admin.set(admin_value); } _ => is_admin.set(Some(false)), } }); || () }); } match *is_admin { None => html! { }, // Non-admin: render a condensed user sidebar (no diagnostics, limited links) Some(false) => html! { }, // Admin: full sidebar wrapped in provider so submenu state persists Some(true) => html! { }, } }