arct_tui/panels/
progress.rs

1//! Progress dashboard panel for displaying user statistics
2
3use crate::icons;
4use crate::theme::Theme;
5use arct_core::{UserStats, Difficulty};
6use ratatui::{
7    layout::{Alignment, Constraint, Direction, Layout, Rect},
8    text::{Line, Span},
9    widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
10    Frame,
11};
12
13/// Progress panel for displaying user stats and progress
14pub struct ProgressPanel;
15
16impl ProgressPanel {
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Render the progress dashboard overlay (centered popup)
22    pub fn render(&self, frame: &mut Frame, theme: &Theme, stats: &UserStats) {
23        let area = Self::centered_rect(70, 60, frame.size());
24
25        // Clear the background
26        frame.render_widget(Clear, area);
27
28        let block = Block::default()
29            .title(format!(" {}Your Progress Dashboard ", icons::target().content))
30            .title_alignment(Alignment::Center)
31            .borders(Borders::ALL)
32            .border_style(theme.style_border_focused())
33            .style(theme.style_block());  // Set background for light themes
34
35        let inner = block.inner(area);
36        frame.render_widget(block, area);
37
38        // Split into sections
39        let chunks = Layout::default()
40            .direction(Direction::Vertical)
41            .constraints([
42                Constraint::Length(8),  // Overview stats
43                Constraint::Length(10), // Progress bars by difficulty
44                Constraint::Min(3),     // Commands section
45                Constraint::Length(2),  // Controls help
46            ])
47            .split(inner);
48
49        // Overview stats
50        let overview_items = vec![
51            ListItem::new(Line::from(vec![
52                Span::raw("  "),
53                Span::styled("", theme.style_accent()),
54                Span::raw("  Current Streak: "),
55                Span::styled(
56                    format!("{} days", stats.current_streak),
57                    theme.style_accent().add_modifier(ratatui::style::Modifier::BOLD),
58                ),
59                Span::raw("  (Best: "),
60                Span::styled(
61                    format!("{}", stats.longest_streak),
62                    theme.style_dim(),
63                ),
64                Span::raw(" days)"),
65            ])),
66            ListItem::new(Line::from("")),
67            ListItem::new(Line::from(vec![
68                Span::raw("  "),
69                icons::lesson(),
70                Span::raw("  Lessons Completed: "),
71                Span::styled(
72                    format!("{}", stats.lessons_completed.len()),
73                    theme.style_accent(),
74                ),
75            ])),
76            ListItem::new(Line::from(vec![
77                Span::raw("  "),
78                icons::shell(),
79                Span::raw("  Commands Mastered: "),
80                Span::styled(
81                    format!("{}", stats.commands_used.len()),
82                    theme.style_accent(),
83                ),
84            ])),
85            ListItem::new(Line::from(vec![
86                Span::raw("  "),
87                icons::celebration(),
88                Span::raw("  Achievements Unlocked: "),
89                Span::styled(
90                    format!("{}", stats.achievements.total_unlocked()),
91                    theme.style_accent(),
92                ),
93                Span::raw(" ("),
94                Span::styled(
95                    format!("{} points", stats.achievements.total_points()),
96                    theme.style_dim(),
97                ),
98                Span::raw(")"),
99            ])),
100            ListItem::new(Line::from(vec![
101                Span::raw("  "),
102                Span::styled(icons::TIMER, theme.style_accent()),
103                Span::raw("  Time Invested: "),
104                Span::styled(
105                    format_time(stats.total_time_seconds),
106                    theme.style_dim(),
107                ),
108            ])),
109        ];
110
111        let overview_list = List::new(overview_items);
112        frame.render_widget(overview_list, chunks[0]);
113
114        // Progress by difficulty
115        let difficulty_section = Self::render_difficulty_progress(theme, stats);
116        frame.render_widget(difficulty_section, chunks[1]);
117
118        // Commands section
119        let command_text = if stats.commands_used.is_empty() {
120            vec![
121                Line::from(""),
122                Line::from(vec![
123                    Span::styled("  No commands used yet. ", theme.style_dim()),
124                    Span::styled("Start practicing to see your progress!", theme.style_normal()),
125                ]),
126            ]
127        } else {
128            let mut lines = vec![
129                Line::from(vec![
130                    Span::styled("  Most Used Commands:", theme.style_header()),
131                ]),
132                Line::from(""),
133            ];
134
135            // Show first 5 commands (alphabetically sorted)
136            let mut commands: Vec<_> = stats.commands_used.iter().collect();
137            commands.sort();
138            for cmd in commands.iter().take(5) {
139                lines.push(Line::from(vec![
140                    Span::raw("    "),
141                    icons::shell(),
142                    Span::styled(cmd.to_string(), theme.style_accent()),
143                ]));
144            }
145
146            if commands.len() > 5 {
147                lines.push(Line::from(vec![
148                    Span::raw("    "),
149                    Span::styled(
150                        format!("... and {} more", commands.len() - 5),
151                        theme.style_dim(),
152                    ),
153                ]));
154            }
155
156            lines
157        };
158
159        let commands_para = Paragraph::new(command_text);
160        frame.render_widget(commands_para, chunks[2]);
161
162        // Controls help
163        let controls = Paragraph::new(vec![Line::from(vec![
164            Span::styled("Esc", theme.style_accent()),
165            Span::raw(" or "),
166            Span::styled("p", theme.style_accent()),
167            Span::raw(" to close"),
168        ])])
169        .alignment(Alignment::Center);
170        frame.render_widget(controls, chunks[3]);
171    }
172
173    /// Render progress bars by difficulty level
174    fn render_difficulty_progress(theme: &Theme, stats: &UserStats) -> Paragraph<'static> {
175        // Assume 10 lessons per difficulty for now (would need lesson library to get actual count)
176        let total_per_difficulty = 10;
177
178        let difficulties = vec![
179            (Difficulty::Beginner, "Beginner"),
180            (Difficulty::Intermediate, "Intermediate"),
181            (Difficulty::Advanced, "Advanced"),
182        ];
183
184        let mut lines = vec![
185            Line::from(vec![
186                Span::styled("  Progress by Difficulty:", theme.style_header()),
187            ]),
188            Line::from(""),
189        ];
190
191        for (difficulty, name) in difficulties {
192            let completed = stats
193                .lessons_by_difficulty
194                .get(&difficulty)
195                .copied()
196                .unwrap_or(0);
197
198            let percentage = (completed as f64 / total_per_difficulty as f64 * 100.0).min(100.0);
199            let bar_width = 30;
200            let filled = (percentage / 100.0 * bar_width as f64) as usize;
201            let empty = bar_width - filled;
202
203            let bar_filled = "█".repeat(filled);
204            let bar_empty = "░".repeat(empty);
205
206            lines.push(Line::from(vec![
207                Span::raw("    "),
208                Span::styled(format!("{:12}", name), theme.style_normal()),
209                Span::raw("  ["),
210                Span::styled(bar_filled, theme.style_accent()),
211                Span::styled(bar_empty, theme.style_dim()),
212                Span::raw("]  "),
213                Span::styled(
214                    format!("{}/{} ({:.0}%)", completed, total_per_difficulty, percentage),
215                    theme.style_dim(),
216                ),
217            ]));
218        }
219
220        Paragraph::new(lines)
221    }
222
223    /// Helper function to create a centered rectangle
224    fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
225        let popup_layout = Layout::default()
226            .direction(Direction::Vertical)
227            .constraints([
228                Constraint::Percentage((100 - percent_y) / 2),
229                Constraint::Percentage(percent_y),
230                Constraint::Percentage((100 - percent_y) / 2),
231            ])
232            .split(r);
233
234        Layout::default()
235            .direction(Direction::Horizontal)
236            .constraints([
237                Constraint::Percentage((100 - percent_x) / 2),
238                Constraint::Percentage(percent_x),
239                Constraint::Percentage((100 - percent_x) / 2),
240            ])
241            .split(popup_layout[1])[1]
242    }
243}
244
245impl Default for ProgressPanel {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251/// Format seconds into human-readable time
252fn format_time(seconds: u64) -> String {
253    let hours = seconds / 3600;
254    let minutes = (seconds % 3600) / 60;
255
256    if hours > 0 {
257        format!("{}h {}m", hours, minutes)
258    } else {
259        format!("{}m", minutes)
260    }
261}