Skip to main content

logicaffeine_web/ui/components/
module_tabs.rs

1//! Module Tab Bar component for the integrated Learn page.
2//!
3//! Displays tabs: LESSON | EXAMPLES | PRACTICE ∞ | TEST 📝
4//! allowing users to switch between different learning modes within a module.
5
6use dioxus::prelude::*;
7use crate::learn_state::TabMode;
8
9const MODULE_TABS_STYLE: &str = r#"
10.module-tabs {
11    display: flex;
12    gap: 4px;
13    padding: 4px;
14    background: rgba(255, 255, 255, 0.03);
15    border-radius: 12px;
16    border: 1px solid rgba(255, 255, 255, 0.08);
17    width: fit-content;
18}
19
20.module-tab {
21    padding: 10px 18px;
22    border-radius: 8px;
23    border: none;
24    background: transparent;
25    color: rgba(229, 231, 235, 0.56);
26    font-size: 12px;
27    font-weight: 600;
28    text-transform: uppercase;
29    letter-spacing: 0.5px;
30    cursor: pointer;
31    transition: all 0.2s ease;
32    white-space: nowrap;
33}
34
35.module-tab:hover:not(.locked):not(.active) {
36    color: rgba(229, 231, 235, 0.85);
37    background: rgba(255, 255, 255, 0.05);
38}
39
40.module-tab.active {
41    background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(167, 139, 250, 0.2));
42    color: #e5e7eb;
43    box-shadow: 0 2px 8px rgba(96, 165, 250, 0.15);
44}
45
46.module-tab.locked {
47    opacity: 0.4;
48    cursor: not-allowed;
49}
50
51.module-tab.lesson.active {
52    background: linear-gradient(135deg, rgba(96, 165, 250, 0.25), rgba(96, 165, 250, 0.15));
53}
54
55.module-tab.examples.active {
56    background: linear-gradient(135deg, rgba(167, 139, 250, 0.25), rgba(167, 139, 250, 0.15));
57}
58
59.module-tab.practice.active {
60    background: linear-gradient(135deg, rgba(74, 222, 128, 0.25), rgba(74, 222, 128, 0.15));
61}
62
63.module-tab.test.active {
64    background: linear-gradient(135deg, rgba(251, 146, 60, 0.25), rgba(251, 146, 60, 0.15));
65}
66
67/* Compact variant for inline use */
68.module-tabs.compact {
69    padding: 2px;
70    gap: 2px;
71}
72
73.module-tabs.compact .module-tab {
74    padding: 6px 12px;
75    font-size: 11px;
76}
77"#;
78
79/// Props for the ModuleTabs component
80#[derive(Props, Clone, PartialEq)]
81pub struct ModuleTabsProps {
82    /// Currently active tab
83    current: TabMode,
84    /// Handler called when user clicks a tab
85    on_change: EventHandler<TabMode>,
86    /// Tabs that should be locked (disabled)
87    #[props(default)]
88    locked_tabs: Vec<TabMode>,
89    /// Use compact variant
90    #[props(default = false)]
91    compact: bool,
92}
93
94/// Tab bar for switching between module learning modes
95#[component]
96pub fn ModuleTabs(props: ModuleTabsProps) -> Element {
97    let container_class = if props.compact {
98        "module-tabs compact"
99    } else {
100        "module-tabs"
101    };
102
103    rsx! {
104        style { "{MODULE_TABS_STYLE}" }
105        div { class: "{container_class}",
106            for tab in TabMode::all() {
107                {
108                    let is_active = tab == props.current;
109                    let is_locked = props.locked_tabs.contains(&tab);
110                    let tab_class_name = match tab {
111                        TabMode::Lesson => "lesson",
112                        TabMode::Examples => "examples",
113                        TabMode::Practice => "practice",
114                        TabMode::Test => "test",
115                    };
116                    let class = format!(
117                        "module-tab {}{}{}",
118                        tab_class_name,
119                        if is_active { " active" } else { "" },
120                        if is_locked { " locked" } else { "" }
121                    );
122
123                    rsx! {
124                        button {
125                            key: "{tab_class_name}",
126                            class: "{class}",
127                            disabled: is_locked,
128                            onclick: {
129                                let on_change = props.on_change.clone();
130                                move |_| {
131                                    if !is_locked {
132                                        on_change.call(tab);
133                                    }
134                                }
135                            },
136                            "{tab.label()}"
137                        }
138                    }
139                }
140            }
141        }
142    }
143}
144
145/// Individual tab button component for custom layouts
146#[component]
147pub fn TabButton(
148    tab: TabMode,
149    is_active: bool,
150    #[props(default = false)] is_locked: bool,
151    on_click: EventHandler<TabMode>,
152) -> Element {
153    let tab_class_name = match tab {
154        TabMode::Lesson => "lesson",
155        TabMode::Examples => "examples",
156        TabMode::Practice => "practice",
157        TabMode::Test => "test",
158    };
159    let class = format!(
160        "module-tab {}{}{}",
161        tab_class_name,
162        if is_active { " active" } else { "" },
163        if is_locked { " locked" } else { "" }
164    );
165
166    rsx! {
167        style { "{MODULE_TABS_STYLE}" }
168        button {
169            class: "{class}",
170            disabled: is_locked,
171            onclick: move |_| {
172                if !is_locked {
173                    on_click.call(tab);
174                }
175            },
176            "{tab.label()}"
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_tab_class_names() {
187        // Verify class name generation logic
188        assert!(TabMode::Lesson.label().contains("LESSON"));
189        assert!(TabMode::Practice.label().contains("PRACTICE"));
190    }
191}