1use 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
13pub struct ProgressPanel;
15
16impl ProgressPanel {
17 pub fn new() -> Self {
18 Self
19 }
20
21 pub fn render(&self, frame: &mut Frame, theme: &Theme, stats: &UserStats) {
23 let area = Self::centered_rect(70, 60, frame.size());
24
25 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()); let inner = block.inner(area);
36 frame.render_widget(block, area);
37
38 let chunks = Layout::default()
40 .direction(Direction::Vertical)
41 .constraints([
42 Constraint::Length(8), Constraint::Length(10), Constraint::Min(3), Constraint::Length(2), ])
47 .split(inner);
48
49 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 let difficulty_section = Self::render_difficulty_progress(theme, stats);
116 frame.render_widget(difficulty_section, chunks[1]);
117
118 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 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 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 fn render_difficulty_progress(theme: &Theme, stats: &UserStats) -> Paragraph<'static> {
175 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 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
251fn 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}