Skip to main content

reinhardt_admin/pages/components/
layout.rs

1//! Layout components
2//!
3//! Provides layout components for the admin panel:
4//! - `Header` - Top navigation bar
5//! - `Sidebar` - Side navigation menu
6//! - `Footer` - Footer component
7//! - `MainLayout` - Main layout wrapper
8
9use reinhardt_pages::component::{IntoPage, Page, PageElement};
10
11/// Model information for navigation
12#[derive(Debug, Clone)]
13pub struct ModelInfo {
14	/// Model name (display name)
15	pub name: String,
16	/// URL path for the model list view
17	pub url: String,
18}
19
20/// Header component
21///
22/// Displays the top navigation bar with site title and user menu.
23///
24/// # Example
25///
26/// ```ignore
27/// use reinhardt_admin::pages::components::layout::header;
28///
29/// header("My Admin Panel", Some("john_doe"))
30/// ```
31pub fn header(site_name: &str, user_name: Option<&str>) -> Page {
32	let user_display = user_name.unwrap_or("Guest");
33
34	PageElement::new("nav")
35		.attr("class", "navbar navbar-dark bg-dark")
36		.child(
37			PageElement::new("div")
38				.attr("class", "container-fluid")
39				.child(
40					PageElement::new("a")
41						.attr("class", "navbar-brand")
42						.attr("href", "/admin/")
43						.child(site_name.to_string()),
44				)
45				.child(
46					PageElement::new("span")
47						.attr("class", "navbar-text")
48						.child(format!("User: {}", user_display)),
49				),
50		)
51		.into_page()
52}
53
54/// Sidebar component
55///
56/// Displays the side navigation menu with model links.
57/// Uses Link component for SPA navigation.
58///
59/// # Example
60///
61/// ```ignore
62/// use reinhardt_admin::pages::components::layout::{sidebar, ModelInfo};
63///
64/// let models = vec![
65///     ModelInfo { name: "Users".to_string(), url: "/admin/users/".to_string() },
66///     ModelInfo { name: "Posts".to_string(), url: "/admin/posts/".to_string() },
67/// ];
68/// sidebar(&models, Some("/admin/users/"))
69/// ```
70pub fn sidebar(models: &[ModelInfo], current_path: Option<&str>) -> Page {
71	use reinhardt_pages::component::Component;
72	use reinhardt_pages::router::Link;
73
74	let nav_items: Vec<Page> = models
75		.iter()
76		.map(|model| {
77			let is_active = current_path.is_some_and(|path| path.starts_with(&model.url));
78			let item_class = if is_active {
79				"nav-link active"
80			} else {
81				"nav-link"
82			};
83
84			PageElement::new("li")
85				.attr("class", "nav-item")
86				.child(
87					Link::new(model.url.clone(), model.name.clone())
88						.class(item_class)
89						.render(),
90				)
91				.into_page()
92		})
93		.collect();
94
95	PageElement::new("div")
96		.attr("class", "sidebar bg-light border-end")
97		.attr(
98			"style",
99			"width: 250px; height: 100vh; position: fixed; top: 56px; left: 0; overflow-y: auto;",
100		)
101		.child(
102			PageElement::new("ul")
103				.attr("class", "nav flex-column")
104				.children(nav_items),
105		)
106		.into_page()
107}
108
109/// Footer component
110///
111/// Displays the footer with copyright and version information.
112///
113/// # Example
114///
115/// ```ignore
116/// use reinhardt_admin::pages::components::layout::footer;
117///
118/// footer("0.1.0")
119/// ```
120pub fn footer(version: &str) -> Page {
121	PageElement::new("footer")
122		.attr("class", "footer bg-light text-center py-3 border-top")
123		.attr("style", "margin-left: 250px;")
124		.child(
125			PageElement::new("div")
126				.attr("class", "container-fluid")
127				.child(format!("Reinhardt Admin Panel v{}", version)),
128		)
129		.into_page()
130}
131
132/// Main layout wrapper
133///
134/// Wraps the main content area with header, sidebar, and footer.
135/// Uses RouterOutlet for dynamic content rendering.
136///
137/// # Example
138///
139/// ```ignore
140/// use reinhardt_admin::pages::components::layout::{main_layout, ModelInfo};
141/// use reinhardt_pages::router::Router;
142/// use std::sync::Arc;
143///
144/// let models = vec![
145///     ModelInfo { name: "Users".to_string(), url: "/admin/users/".to_string() },
146/// ];
147/// let router = Arc::new(Router::new());
148/// main_layout("My Admin", &models, None, "0.1.0", router)
149/// ```
150pub fn main_layout(
151	site_name: &str,
152	models: &[ModelInfo],
153	user_name: Option<&str>,
154	version: &str,
155	router: std::sync::Arc<reinhardt_pages::router::Router>,
156) -> Page {
157	use reinhardt_pages::component::Component;
158	use reinhardt_pages::router::RouterOutlet;
159
160	PageElement::new("div")
161		.attr("class", "admin-layout")
162		.child(header(site_name, user_name))
163		.child(sidebar(models, None))
164		.child(
165			PageElement::new("main")
166				.attr("class", "main-content")
167				.attr(
168					"style",
169					"margin-left: 250px; margin-top: 56px; padding: 20px; min-height: calc(100vh - 120px);",
170				)
171				.child(
172					RouterOutlet::new(router)
173						.id("admin-outlet")
174						.class("router-content")
175						.render(),
176				),
177		)
178		.child(footer(version))
179		.into_page()
180}