Merge branch 'Silas'

This commit is contained in:
2026-06-17 13:32:56 +02:00
21 changed files with 965 additions and 242 deletions
+102 -57
View File
@@ -7,6 +7,11 @@ use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
/// The key used to store and retrieve the sidebar's expansion state in `LocalStorage`.
///
/// This constant ensures consistency when accessing the stored state across different
/// parts of the application. The value associated with this key in `LocalStorage`
/// will be a serialized [`SidebarExpandState`] object.
const STORAGE_KEY: &str = "sidebar_state";
/// Represents the expansion state of collapsible menus within the sidebar.
@@ -128,7 +133,7 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
Callback::from(move |v: bool| {
state.set(SidebarExpandState {
ticket_open: v,
users_open: (*state).users_open,
users_open: state.users_open,
})
})
};
@@ -136,10 +141,10 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let toggle_tickets = {
let state = state.clone();
Callback::from(move |_| {
let current = (*state).ticket_open;
let current = state.ticket_open;
state.set(SidebarExpandState {
ticket_open: !current,
users_open: (*state).users_open,
users_open: state.users_open,
});
})
};
@@ -148,7 +153,7 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let state = state.clone();
Callback::from(move |v: bool| {
state.set(SidebarExpandState {
ticket_open: (*state).ticket_open,
ticket_open: state.ticket_open,
users_open: v,
})
})
@@ -157,9 +162,9 @@ pub fn sidebar_state_provider(props: &SidebarProps) -> Html {
let toggle_users = {
let state = state.clone();
Callback::from(move |_| {
let current = (*state).users_open;
let current = state.users_open;
state.set(SidebarExpandState {
ticket_open: (*state).ticket_open,
ticket_open: state.ticket_open,
users_open: !current,
});
})
@@ -314,6 +319,11 @@ pub fn users_menu() -> Html {
/// and administrative status. It fetches the current user's details via `/api/users/current`
/// to determine what menu items to display.
///
/// # Mobile Support
/// On small screens:
/// - Slides into view from the left when `props.is_open` is `true`.
/// - Renders a close button (`✕`) in the header that emits `props.on_close`.
///
/// # Structure
/// - Wraps its content in a [`SidebarStateProvider`] to allow nested menus to manage their state.
/// - Contains a navigation (`<nav>`) element with an unordered list (`<ul>`) of menu items.
@@ -332,8 +342,22 @@ pub fn users_menu() -> Html {
/// # Logout Functionality
/// The "Logout" button sends a GET request to `/api/logout`, clears the user's session,
/// and then redirects the user to the login page (`crate::Route::Login`).
/// Properties for the [`Sidebar`] component.
///
/// These properties enable managing and controlling the responsive mobile sidebar
/// layout and its visibility settings.
#[derive(Properties, PartialEq)]
pub struct SidebarComponentProps {
/// A boolean flag indicating whether the mobile sidebar is currently slid open (`true`) or hidden (`false`).
#[prop_or_default]
pub is_open: bool,
/// A callback emitted when the user requests to close/hide the mobile sidebar.
#[prop_or_default]
pub on_close: Callback<()>,
}
#[component(Sidebar)]
pub fn sidebar() -> Html {
pub fn sidebar(props: &SidebarComponentProps) -> Html {
let is_admin = use_state(|| None::<bool>);
let navigator = use_navigator().expect("Sidebar must be used within a Router");
@@ -374,60 +398,81 @@ pub fn sidebar() -> Html {
};
match *is_admin {
None => html! { <div class="sidebar-loading">{ "Loading..." }</div> },
None => html! { <div class="sidebar-loading">{ "Lade..." }</div> },
// Non-admin: render a condensed user sidebar (no diagnostics, limited links)
Some(false) => html! {
<SidebarStateProvider>
<nav class="sidebar user">
<ul>
<Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<TicketMenu/>
<li class="logout-item">
<button
class="sidebar-button"
onclick={on_logout.clone()}
>
{ "Logout" }
</button>
</li>
</ul>
</nav>
</SidebarStateProvider>
Some(false) => {
let on_close = props.on_close.clone();
html! {
<SidebarStateProvider>
<nav class={if props.is_open { "sidebar user open" } else { "sidebar user" }}>
<ul>
<li class="sidebar-header">
<Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<button class="sidebar-close" onclick={move |_| on_close.emit(())}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
<TicketMenu/>
<li class="logout-item">
<button
class="sidebar-button"
onclick={on_logout.clone()}
>
{ "Abmelden" }
</button>
</li>
</ul>
</nav>
</SidebarStateProvider>
}
},
// Admin: full sidebar wrapped in provider so submenu state persists
Some(true) => html! {
<SidebarStateProvider>
<nav class="sidebar admin">
<ul>
<Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<TicketMenu/>
<UsersMenu/>
<Link<crate::Route> to={crate::Route::Diagnostics}>{ "Statistiken" }</Link<crate::Route>>
<Link<crate::Route> to={crate::Route::ArchivedTickets}>{ "Archiv" }</Link<crate::Route>>
<li class="logout-item">
<crate::darkmode::ThemeToggle/>
<button
class="sidebar-button"
onclick={on_logout.clone()}
>
{ "Logout" }
</button>
</li>
</ul>
</nav>
</SidebarStateProvider>
Some(true) => {
let on_close = props.on_close.clone();
html! {
<SidebarStateProvider>
<nav class={if props.is_open { "sidebar admin open" } else { "sidebar admin" }}>
<ul>
<li class="sidebar-header">
<Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<button class="sidebar-close" onclick={move |_| on_close.emit(())}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</li>
<TicketMenu/>
<UsersMenu/>
<Link<crate::Route> to={crate::Route::Diagnostics}>{ "Statistiken" }</Link<crate::Route>>
<Link<crate::Route> to={crate::Route::ArchivedTickets}>{ "Archiv" }</Link<crate::Route>>
<li class="logout-item">
<button
class="sidebar-button"
onclick={on_logout.clone()}
>
{ "Abmelden" }
</button>
</li>
</ul>
</nav>
</SidebarStateProvider>
}
},
}
}
}