Sidebar logic
This commit is contained in:
1
frontend/Cargo.lock
generated
1
frontend/Cargo.lock
generated
@@ -129,6 +129,7 @@ dependencies = [
|
|||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"gloo 0.12.0",
|
"gloo 0.12.0",
|
||||||
"gloo-net 0.7.0",
|
"gloo-net 0.7.0",
|
||||||
|
"gloo-storage 0.4.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ web-sys = { version = "0.3.95", features = [
|
|||||||
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent"
|
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent"
|
||||||
] }
|
] }
|
||||||
gloo-net = "0.7.0"
|
gloo-net = "0.7.0"
|
||||||
|
gloo-storage = "0.4.0"
|
||||||
yew-router = "0.20.0"
|
yew-router = "0.20.0"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
gloo = "0.12.0"
|
gloo = "0.12.0"
|
||||||
|
|||||||
1330
frontend/package-lock.json
generated
Normal file
1330
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
frontend/package.json
Normal file
20
frontend/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build:scss": "sass src/styles/main.scss static/main.css --no-source-map --style=compressed",
|
||||||
|
"watch:scss": "sass --watch src/styles/main.scss:static/main.css",
|
||||||
|
"start": "npm-run-all --parallel watch:scss "trunk serve""
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"postcss-cli": "^11.0.1",
|
||||||
|
"sass": "^1.99.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ fn switch(route: Route) -> Html {
|
|||||||
pub fn app() -> Html {
|
pub fn app() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<sidebar::Sidebar/>
|
||||||
<Switch<Route> render={switch} />
|
<Switch<Route> render={switch} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod basic_pages;
|
pub mod basic_pages;
|
||||||
|
pub mod sidebar;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
267
frontend/src/pages/sidebar.rs
Normal file
267
frontend/src/pages/sidebar.rs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
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<bool>,
|
||||||
|
pub toggle_tickets: Callback<()>,
|
||||||
|
pub set_users_open: Callback<bool>,
|
||||||
|
pub toggle_users: Callback<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidebarState {
|
||||||
|
fn new(
|
||||||
|
expand: SidebarExpandState,
|
||||||
|
set_tickets_open: Callback<bool>,
|
||||||
|
toggle_tickets: Callback<()>,
|
||||||
|
set_users_open: Callback<bool>,
|
||||||
|
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! {
|
||||||
|
<ContextProvider<Rc<SidebarState>> context={Rc::new(ctx)}>
|
||||||
|
{ for props.children.iter() }
|
||||||
|
</ContextProvider<Rc<SidebarState>>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(TicketMenu)]
|
||||||
|
pub fn ticket_menu() -> Html {
|
||||||
|
let ctx =
|
||||||
|
use_context::<Rc<SidebarState>>().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! {
|
||||||
|
<li class={ if open { "menu open" } else { "menu" } }>
|
||||||
|
<button
|
||||||
|
class="menu-toggle"
|
||||||
|
onclick={on_toggle}
|
||||||
|
aria-expanded={open.to_string()}
|
||||||
|
>
|
||||||
|
{ "Tickets" }
|
||||||
|
{ if open { " ▾" } else { " ▸" } }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{
|
||||||
|
if open {
|
||||||
|
html! {
|
||||||
|
<ul class="submenu" role="menu">
|
||||||
|
<li role="none">
|
||||||
|
<Link<crate::Route> to={crate::Route::Ticket}><span role="menuitem">{ "Submit Ticket" }</span></Link<crate::Route>>
|
||||||
|
</li>
|
||||||
|
<li role="none">
|
||||||
|
<Link<crate::Route> to={crate::Route::AllTickets}><span role="menuitem">{ "View Tickets" }</span></Link<crate::Route>>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html!{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(UsersMenu)]
|
||||||
|
pub fn users_menu() -> Html {
|
||||||
|
let ctx =
|
||||||
|
use_context::<Rc<SidebarState>>().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! {
|
||||||
|
<li class={ if open { "menu open" } else { "menu" } }>
|
||||||
|
<button
|
||||||
|
class="menu-toggle"
|
||||||
|
onclick={on_toggle}
|
||||||
|
aria-expanded={open.to_string()}
|
||||||
|
>
|
||||||
|
{ "Users" }
|
||||||
|
{ if open { " ▾" } else { " ▸" } }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{
|
||||||
|
if open {
|
||||||
|
html! {
|
||||||
|
<ul class="submenu" role="menu">
|
||||||
|
<li role="none">
|
||||||
|
<Link<crate::Route> to={crate::Route::Register}><span role="menuitem">{ "Create User" }</span></Link<crate::Route>>
|
||||||
|
</li>
|
||||||
|
<li role="none">
|
||||||
|
<Link<crate::Route> to={crate::Route::AllUsers}><span role="menuitem">{ "View Users" }</span></Link<crate::Route>>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html!{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(Sidebar)]
|
||||||
|
pub fn sidebar() -> Html {
|
||||||
|
let is_admin = use_state(|| None::<bool>);
|
||||||
|
|
||||||
|
{
|
||||||
|
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! { <div class="sidebar-loading">{ "Loading..." }</div> },
|
||||||
|
|
||||||
|
// Non-admin: render a condensed user sidebar (no diagnostics, limited links)
|
||||||
|
Some(false) => html! {
|
||||||
|
<SidebarStateProvider>
|
||||||
|
<nav class="sidebar user">
|
||||||
|
<ul>
|
||||||
|
<TicketMenu/>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</SidebarStateProvider>
|
||||||
|
},
|
||||||
|
|
||||||
|
// Admin: full sidebar wrapped in provider so submenu state persists
|
||||||
|
Some(true) => html! {
|
||||||
|
<SidebarStateProvider>
|
||||||
|
<nav class="sidebar admin">
|
||||||
|
<ul>
|
||||||
|
<TicketMenu/>
|
||||||
|
<UsersMenu/>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</SidebarStateProvider>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user