use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::{BsList, BsX}; use dioxus_free_icons::Icon; use crate::components::sidebar::Sidebar; use crate::i18n::{t, tw, Locale}; use crate::infrastructure::auth_check::check_auth; use crate::models::{AuthInfo, ServiceUrlsContext}; use crate::Route; /// Application shell layout that wraps all authenticated pages. /// /// Calls [`check_auth`] on mount to fetch the current user's session. /// If unauthenticated, redirects to `/auth`. Otherwise renders the /// sidebar with real user data and the active child route. #[component] pub fn AppShell() -> Element { let locale = use_context::>(); let mut mobile_menu_open = use_signal(|| false); // use_resource memoises the async call and avoids infinite re-render // loops that use_effect + spawn + signal writes can cause. #[allow(clippy::redundant_closure)] let auth = use_resource(move || check_auth()); // Clone the inner value out of the Signal to avoid holding the // borrow across the rsx! return (Dioxus lifetime constraint). let auth_snapshot: Option> = auth.read().clone(); match auth_snapshot { Some(Ok(info)) if info.authenticated => { // Provide developer tool URLs as context so child pages // can read them without prop-drilling through layouts. use_context_provider(|| { Signal::new(ServiceUrlsContext { langgraph_url: info.langgraph_url.clone(), langflow_url: info.langflow_url.clone(), langfuse_url: info.langfuse_url.clone(), }) }); let menu_open = *mobile_menu_open.read(); let sidebar_cls = if menu_open { "sidebar sidebar--open" } else { "sidebar" }; rsx! { div { class: "app-shell", // Mobile top bar (visible only on small screens via CSS) header { class: "mobile-header", button { class: "mobile-menu-btn", onclick: move |_| { let current = *mobile_menu_open.read(); mobile_menu_open.set(!current); }, if menu_open { Icon { icon: BsX, width: 24, height: 24 } } else { Icon { icon: BsList, width: 24, height: 24 } } } span { class: "mobile-header-title", "CERTifAI" } } // Backdrop overlay when sidebar is open on mobile if menu_open { div { class: "sidebar-backdrop", onclick: move |_| mobile_menu_open.set(false), } } Sidebar { email: info.email, name: info.name, avatar_url: info.avatar_url, librechat_url: info.librechat_url, compliance_scanner_url: info.compliance_scanner_url, class: sidebar_cls, on_nav: move |_| mobile_menu_open.set(false), } main { class: "main-content", Outlet:: {} } } } } Some(Ok(_)) => { // Not authenticated -- redirect to login. let nav = navigator(); nav.push(NavigationTarget::::External("/auth".into())); rsx! { div { class: "app-shell loading", p { {t(*locale.read(), "auth.redirecting_login")} } } } } Some(Err(e)) => { let msg = e.to_string(); let error_text = tw(*locale.read(), "auth.auth_error", &[("msg", &msg)]); rsx! { div { class: "auth-error", p { {error_text} } a { href: "/auth", {t(*locale.read(), "common.login")} } } } } None => { // Still loading. rsx! { div { class: "app-shell loading", p { {t(*locale.read(), "common.loading")} } } } } } }