use reinhardt_pages::component::Page;
use reinhardt_pages::page;
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub name: String,
pub url: String,
}
pub fn header(site_name: &str, user_name: Option<&str>) -> Page {
let site_name = site_name.to_string();
let user_display = user_name.unwrap_or("Guest").to_string();
page!(|| {
nav {
class: "flex items-center justify-between px-6 py-3 bg-slate-900 text-white animate__animated animate__fadeInDown",
style: "position: fixed; top: 0; left: 0; right: 0; z-index: 50; height: 56px;",
div {
class: "flex items-center gap-3",
a {
class: "font-display text-lg font-bold tracking-tight text-white no-underline hover:text-amber-400",
href: "/admin/",
{ site_name }
}
}
div {
class: "flex items-center gap-2 text-sm text-slate-400",
span {
{ format!("User: {}", user_display) }
}
}
}
})()
}
fn is_active_path(model_url: &str, current_path: Option<&str>) -> bool {
current_path.is_some_and(|path| {
let normalized_url = model_url.trim_end_matches('/');
path == model_url
|| path == normalized_url
|| path.starts_with(&format!("{}/", normalized_url))
})
}
pub fn sidebar(models: &[ModelInfo], current_path: Option<&str>) -> Page {
use reinhardt_pages::component::Component;
use reinhardt_pages::router::Link;
let nav_items: Vec<Page> = models
.iter()
.map(|model| {
let is_active = is_active_path(&model.url, current_path);
let item_class = if is_active {
"block px-4 py-2.5 text-sm no-underline border-l-3 border-transparent admin-nav-active"
} else {
"block px-4 py-2.5 text-sm text-slate-400 no-underline border-l-3 border-transparent hover:text-white hover:bg-slate-800"
};
let link = Link::new(model.url.clone(), model.name.clone())
.class(item_class)
.render();
page!(|| {
li {
class: "list-none",
{ link }
}
})()
})
.collect();
page!(|| {
div {
class: "admin-sidebar bg-slate-900 border-r border-slate-800 animate__animated animate__fadeInLeft",
style: "width: 240px; height: 100vh; position: fixed; top: 56px; left: 0; overflow-y: auto; padding-top: 1rem;",
div {
class: "px-4 pb-3 mb-2 border-b border-slate-800",
span {
class: "text-xs font-semibold uppercase tracking-wider text-slate-500",
"Models"
}
}
ul {
class: "flex flex-col gap-0.5 px-0 m-0",
{ nav_items }
}
}
})()
}
pub fn footer(version: &str) -> Page {
let version = version.to_string();
page!(|| {
footer {
class: "text-center py-4 text-xs text-slate-400 border-t border-slate-200 animate__animated animate__fadeIn",
style: "margin-left: 240px;",
{ format!("Reinhardt Admin v{}", version) }
}
})()
}
#[allow(deprecated)]
pub fn main_layout(
site_name: &str,
models: &[ModelInfo],
user_name: Option<&str>,
version: &str,
router: std::sync::Arc<reinhardt_pages::router::Router>,
) -> Page {
use reinhardt_pages::component::Component;
use reinhardt_pages::router::RouterOutlet;
let current_path = router.current_path().get();
let header_page = header(site_name, user_name);
let sidebar_page = sidebar(models, Some(¤t_path));
let footer_page = footer(version);
let outlet = RouterOutlet::new(router)
.id("admin-outlet")
.class("router-content")
.render();
page!(|| {
div {
class: "admin-layout min-h-screen bg-slate-50",
{ header_page }
{ sidebar_page }
main {
class: "bg-slate-50",
style: "margin-left: 240px; margin-top: 56px; padding: 1.5rem 2rem; min-height: calc(100vh - 120px);",
{ outlet }
}
{ footer_page }
}
})()
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::is_active_path;
#[rstest]
fn test_exact_match_with_trailing_slash() {
let model_url = "/admin/users/";
let current_path = Some("/admin/users/");
let result = is_active_path(model_url, current_path);
assert!(result);
}
#[rstest]
fn test_match_without_trailing_slash() {
let model_url = "/admin/users/";
let current_path = Some("/admin/users");
let result = is_active_path(model_url, current_path);
assert!(result);
}
#[rstest]
fn test_sub_page_matches() {
let model_url = "/admin/users/";
let current_path = Some("/admin/users/42/change/");
let result = is_active_path(model_url, current_path);
assert!(result);
}
#[rstest]
fn test_similar_prefix_does_not_match() {
let model_url = "/admin/users/";
let current_path = Some("/admin/usergroups/");
let result = is_active_path(model_url, current_path);
assert!(!result);
}
#[rstest]
fn test_root_admin_path_matches() {
let model_url = "/admin/";
let current_path = Some("/admin/");
let result = is_active_path(model_url, current_path);
assert!(result);
}
#[rstest]
fn test_none_current_path_does_not_match() {
let model_url = "/admin/users/";
let current_path = None;
let result = is_active_path(model_url, current_path);
assert!(!result);
}
#[rstest]
fn test_different_path_does_not_match() {
let model_url = "/admin/users/";
let current_path = Some("/admin/posts/");
let result = is_active_path(model_url, current_path);
assert!(!result);
}
}