qorzen_oxide/ui/layout/
main_layout.rs

1// src/ui/layout/main_layout.rs - Main layout component that orchestrates the overall page structure
2
3use dioxus::prelude::*;
4
5use crate::ui::{
6    layout::{Footer, Header, Sidebar},
7    state::{ui::use_mobile_menu, ui::use_sidebar, use_app_state},
8};
9
10/// Main layout component that provides the overall page structure
11#[component]
12pub fn Layout(#[props] children: Element) -> Element {
13    let app_state = use_app_state();
14    let (sidebar_collapsed, toggle_sidebar, _) = use_sidebar();
15    let (mobile_menu_open, toggle_mobile_menu, set_mobile_menu_open) = use_mobile_menu();
16
17    // Close mobile menu when clicking outside
18    let close_mobile_menu = use_callback(move |_: Event<MouseData>| {
19        set_mobile_menu_open(false);
20    });
21
22    rsx! {
23        div {
24            class: "min-h-screen bg-gray-50 flex flex-col",
25
26            // Header
27            Header {
28                on_menu_toggle: toggle_mobile_menu,
29                on_sidebar_toggle: toggle_sidebar
30            }
31
32            // Main content area with sidebar
33            div {
34                class: "flex flex-1 overflow-hidden",
35
36                // Sidebar
37                Sidebar {
38                    collapsed: sidebar_collapsed,
39                    mobile_open: mobile_menu_open,
40                    on_close: close_mobile_menu
41                }
42
43                // Main content
44                main {
45                    class: format!(
46                        "flex-1 overflow-y-auto transition-all duration-200 ease-in-out {}",
47                        if sidebar_collapsed {
48                            "lg:ml-16"
49                        } else {
50                            "lg:ml-64"
51                        }
52                    ),
53
54                    // Content container
55                    div {
56                        class: "container mx-auto px-4 sm:px-6 lg:px-8 py-6 max-w-7xl",
57
58                        // Error message display
59                        if let Some(error) = &app_state.error_message {
60                            div {
61                                class: "mb-6 bg-red-50 border border-red-200 rounded-md p-4",
62                                div {
63                                    class: "flex items-center",
64                                    div {
65                                        class: "flex-shrink-0",
66                                        svg {
67                                            class: "h-5 w-5 text-red-400",
68                                            xmlns: "http://www.w3.org/2000/svg",
69                                            view_box: "0 0 20 20",
70                                            fill: "currentColor",
71                                            path {
72                                                fill_rule: "evenodd",
73                                                d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z",
74                                                clip_rule: "evenodd"
75                                            }
76                                        }
77                                    }
78                                    div {
79                                        class: "ml-3",
80                                        h3 {
81                                            class: "text-sm font-medium text-red-800",
82                                            "Error"
83                                        }
84                                        div {
85                                            class: "mt-2 text-sm text-red-700",
86                                            "{error}"
87                                        }
88                                    }
89                                }
90                            }
91                        }
92
93                        // Loading indicator
94                        if app_state.is_loading {
95                            div {
96                                class: "mb-6 bg-blue-50 border border-blue-200 rounded-md p-4",
97                                div {
98                                    class: "flex items-center",
99                                    div {
100                                        class: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600 mr-3"
101                                    }
102                                    span {
103                                        class: "text-blue-800 text-sm font-medium",
104                                        "Loading..."
105                                    }
106                                }
107                            }
108                        }
109
110                        // Page content
111                        {children}
112                    }
113                }
114
115                // Mobile menu overlay
116                if mobile_menu_open {
117                    div {
118                        class: "fixed inset-0 z-40 lg:hidden",
119                        onclick: close_mobile_menu,
120
121                        // Backdrop
122                        div {
123                            class: "fixed inset-0 bg-gray-600 bg-opacity-75 transition-opacity"
124                        }
125                    }
126                }
127            }
128
129            // Footer
130            Footer {}
131        }
132    }
133}
134
135/// Responsive layout wrapper for different screen sizes
136#[component]
137pub fn ResponsiveLayout(
138    #[props(default = "".to_string())] mobile_class: String,
139    #[props(default = "".to_string())] tablet_class: String,
140    #[props(default = "".to_string())] desktop_class: String,
141    children: Element,
142) -> Element {
143    rsx! {
144        div {
145            class: format!(
146                "{} sm:{} md:{} lg:{}",
147                mobile_class,
148                tablet_class,
149                tablet_class,
150                desktop_class
151            ),
152            {children}
153        }
154    }
155}
156
157/// Content wrapper with consistent padding and max-width
158#[component]
159pub fn ContentWrapper(
160    #[props(default = "".to_string())] class: String,
161    children: Element,
162) -> Element {
163    rsx! {
164        div {
165            class: format!(
166                "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 {}",
167                class
168            ),
169            {children}
170        }
171    }
172}
173
174/// Page header component for consistent page titles and actions
175#[component]
176pub fn PageHeader(
177    title: String,
178    #[props(default = None)] subtitle: Option<String>,
179    #[props(default = None)] actions: Option<Element>,
180) -> Element {
181    rsx! {
182        div {
183            class: "mb-8",
184            div {
185                class: "md:flex md:items-center md:justify-between",
186                div {
187                    class: "flex-1 min-w-0",
188                    h1 {
189                        class: "text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate",
190                        "{title}"
191                    }
192                    if let Some(subtitle) = subtitle {
193                        p {
194                            class: "mt-1 text-sm text-gray-500",
195                            "{subtitle}"
196                        }
197                    }
198                }
199                if let Some(actions) = actions {
200                    div {
201                        class: "mt-4 flex md:mt-0 md:ml-4",
202                        {actions}
203                    }
204                }
205            }
206        }
207    }
208}
209
210/// Card component for consistent content containers
211#[component]
212pub fn Card(
213    #[props(default = "".to_string())] class: String,
214    #[props(default = None)] title: Option<String>,
215    children: Element,
216) -> Element {
217    rsx! {
218        div {
219            class: format!(
220                "bg-white overflow-hidden shadow rounded-lg {}",
221                class
222            ),
223            if let Some(title) = title {
224                div {
225                    class: "px-4 py-5 sm:px-6 border-b border-gray-200",
226                    h3 {
227                        class: "text-lg leading-6 font-medium text-gray-900",
228                        "{title}"
229                    }
230                }
231            }
232            div {
233                class: "px-4 py-5 sm:p-6",
234                {children}
235            }
236        }
237    }
238}
239
240/// Grid layout component for responsive grids
241#[component]
242pub fn Grid(
243    #[props(default = 1)] cols: u32,
244    #[props(default = 2)] sm_cols: u32,
245    #[props(default = 3)] md_cols: u32,
246    #[props(default = 4)] lg_cols: u32,
247    #[props(default = "gap-6".to_string())] gap: String,
248    #[props(default = "".to_string())] class: String,
249    children: Element,
250) -> Element {
251    rsx! {
252        div {
253            class: format!(
254                "grid grid-cols-{} sm:grid-cols-{} md:grid-cols-{} lg:grid-cols-{} {} {}",
255                cols, sm_cols, md_cols, lg_cols, gap, class
256            ),
257            {children}
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use dioxus::prelude::*;
266
267    #[test]
268    fn test_layout_component_creation() {
269        // Test that the component can be created without panicking
270        let _layout = rsx! {
271            Layout {
272                div { "Test content" }
273            }
274        };
275    }
276
277    #[test]
278    fn test_card_component_creation() {
279        let _card = rsx! {
280            Card {
281                title: "Test Card".to_string(),
282                div { "Card content" }
283            }
284        };
285    }
286
287    #[test]
288    fn test_page_header_component_creation() {
289        let _header = rsx! {
290            PageHeader {
291                title: "Test Page".to_string(),
292                subtitle: Some("Test subtitle".to_string())
293            }
294        };
295    }
296}