Skip to content

Commit a1f4a36

Browse files
authored
feat: UI Integration Iteration 4 - Profile/Statistics Screens (#33)
* feat: UI Integration Iteration 4 - Profile/Statistics Screens Implements Profile and Statistics screens, completing Iteration 4 of UI Integration phase. ## Changes **New Files**: - src/ui/render/profile.rs (225 lines) - Profile screen with level, XP bar, stats, quests - src/ui/render/statistics.rs (238 lines) - Statistics screen with performance breakdown **Modified Files**: - src/ui/render/mod.rs - Added new modules and ViewMode dispatch - src/main.rs - Enhanced key handling for navigation (p, s, m keys) - Cargo.lock - Updated dependencies ## Features **Profile Screen**: - Level display with star emoji - XP progress bar using Gauge widget - Statistics: total scenarios, perfect runs, total XP - Daily quests: active count, completed today - Navigation: 's' for statistics, 'm' for menu **Statistics Screen**: - Performance breakdown with horizontal bars (Perfect/Excellent/Good/Fair/Poor) - Quest statistics: completed count, current streak, longest streak - Session history: average score, total time played - Navigation: 'p' for profile, 'm' for menu **Navigation Flow**: - Results → Profile (press 'p') - Profile ⇄ Statistics (press 's'/'p') - Any screen → Menu (press 'm') ## Testing - ✅ 308/308 tests passing (100%) - ✅ Zero clippy warnings (strict mode) - ✅ Clean release build (27.74s) - ✅ Follows Elm architecture pattern ## Code Quality - Modular design with separate render modules - RefCell safety with scoped borrows - Doc comments for all public functions - Consistent styling and color schemes * fix: Address critical issues from agent review Fixes two critical issues identified during comprehensive agent review before merging Phase 1 UI Integration. ## Issues Fixed ### 1. XP Overflow Protection (Critical) **File**: src/gamification/profile.rs:100 **Issue**: Used unchecked addition which could overflow and panic **Fix**: Replace `+=` with `saturating_add()` **Before**: ```rust self.total_xp += xp; // Could overflow u64 and panic ``` **After**: ```rust self.total_xp = self.total_xp.saturating_add(xp); ``` **Risk**: Very low (would take 21 billion years at 1 scenario/sec to overflow), but better to be safe and prevent any possibility of panic. ### 2. Event Loop Error Handling (High Priority) **File**: src/main.rs:246 **Issue**: Used `.unwrap()` in event loop without explanation **Fix**: Replace with `.expect()` with clear message **Before**: ```rust let digit = c.to_digit(10).unwrap() as usize; ``` **After**: ```rust let digit = c .to_digit(10) .expect("char is validated as ascii_digit by guard condition") as usize; ``` **Safety**: Guard condition `c.is_ascii_digit()` ensures this never panics, but explicit expect makes intent clear and aids debugging. ## Verification - ✅ All 308 tests passing - ✅ Zero clippy warnings - ✅ Code formatted with nightly rustfmt - ✅ Clean release build ## Agent Review Results - ✅ Performance: APPROVED (5ms render, 69% headroom) - ✅ Security: APPROVED (0 unsafe, 0 CVEs, proper RefCell usage) - 🟡 Testing: CONDITIONAL GO (75% coverage, these fixes address critical issues) - ✅ Code Review: GO (excellent architecture, Rust best practices)
1 parent 60725b7 commit a1f4a36

File tree

6 files changed

+564
-85
lines changed

6 files changed

+564
-85
lines changed

Cargo.lock

Lines changed: 85 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/gamification/profile.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl UserProfile {
9797
/// ```
9898
pub fn add_xp(&mut self, xp: u64) -> bool {
9999
let old_level = self.level;
100-
self.total_xp += xp;
100+
self.total_xp = self.total_xp.saturating_add(xp);
101101
self.level = XPCalculator::level_from_xp(self.total_xp);
102102
self.level > old_level
103103
}

src/main.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,17 @@ fn handle_key_event(key: KeyEvent, state: &AppState) -> Option<Message> {
219219
ui::Screen::MainMenu => handle_menu_keys(key, state),
220220
ui::Screen::Task => handle_task_keys(key, state),
221221
ui::Screen::Results => handle_results_keys(key),
222-
ui::Screen::Profile | ui::Screen::Statistics => handle_profile_stats_keys(key),
222+
ui::Screen::Profile | ui::Screen::Statistics => handle_profile_stats_keys(key, state),
223223
}
224224
}
225225

226226
/// Handle keyboard events on profile and statistics screens
227-
fn handle_profile_stats_keys(key: KeyEvent) -> Option<Message> {
227+
fn handle_profile_stats_keys(key: KeyEvent, state: &AppState) -> Option<Message> {
228228
match key.code {
229-
KeyCode::Esc | KeyCode::Char('q') => Some(Message::BackToMenu),
229+
KeyCode::Esc | KeyCode::Char('m') => Some(Message::BackToMenu),
230+
KeyCode::Char('q') => Some(Message::QuitApp),
231+
KeyCode::Char('s') if state.screen == ui::Screen::Profile => Some(Message::ShowStatistics),
232+
KeyCode::Char('p') if state.screen == ui::Screen::Statistics => Some(Message::ShowProfile),
230233
_ => None,
231234
}
232235
}
@@ -240,7 +243,10 @@ fn handle_menu_keys(key: KeyEvent, state: &AppState) -> Option<Message> {
240243
KeyCode::Enter => Some(Message::MenuSelect),
241244
// Quick jump with number keys (1-9)
242245
KeyCode::Char(c) if c.is_ascii_digit() => {
243-
let digit = c.to_digit(10).unwrap() as usize;
246+
let digit = c
247+
.to_digit(10)
248+
.expect("char is validated as ascii_digit by guard condition")
249+
as usize;
244250
if digit >= 1 && digit <= state.scenarios.len() {
245251
// Jump to scenario (digit - 1 because scenarios are 0-indexed)
246252
Some(Message::StartScenario(digit - 1))
@@ -363,6 +369,7 @@ fn handle_results_keys(key: KeyEvent) -> Option<Message> {
363369
KeyCode::Char('q') => Some(Message::QuitApp),
364370
KeyCode::Char('r') => Some(Message::RetryScenario),
365371
KeyCode::Char('m') => Some(Message::BackToMenu),
372+
KeyCode::Char('p') => Some(Message::ShowProfile),
366373
_ => None,
367374
}
368375
}

src/ui/render/mod.rs

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ mod editor;
88
mod helpers;
99
mod menu;
1010
mod popups;
11+
mod profile;
1112
mod results;
13+
mod statistics;
1214
mod task;
1315

1416
#[cfg(test)]
@@ -31,53 +33,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState) {
3133
Screen::MainMenu => menu::render_main_menu(frame, state),
3234
Screen::Task => task::render_task_screen(frame, state),
3335
Screen::Results => results::render_results_screen(frame, state),
34-
Screen::Profile => render_profile_screen_placeholder(frame, state),
35-
Screen::Statistics => render_statistics_screen_placeholder(frame, state),
36+
Screen::Profile => profile::render_profile_screen(frame, state),
37+
Screen::Statistics => statistics::render_statistics_screen(frame, state),
3638
}
3739
}
38-
39-
/// Placeholder for profile screen
40-
// TODO: Iteration 4 - Implement full profile screen with achievements, stats, level progress
41-
fn render_profile_screen_placeholder(frame: &mut Frame, _state: &mut AppState) {
42-
use ratatui::{
43-
layout::Alignment,
44-
style::{Color, Modifier, Style},
45-
widgets::{Block, Borders, Paragraph},
46-
};
47-
48-
let area = frame.area();
49-
let placeholder =
50-
Paragraph::new("Profile Screen - Coming Soon!\n\nPress Esc to return to menu")
51-
.style(
52-
Style::default()
53-
.fg(Color::Cyan)
54-
.add_modifier(Modifier::BOLD),
55-
)
56-
.alignment(Alignment::Center)
57-
.block(Block::default().title(" Profile ").borders(Borders::ALL));
58-
59-
frame.render_widget(placeholder, area);
60-
}
61-
62-
/// Placeholder for statistics screen
63-
// TODO: Iteration 4 - Implement statistics with mastery breakdown, activity graph, weak commands
64-
fn render_statistics_screen_placeholder(frame: &mut Frame, _state: &mut AppState) {
65-
use ratatui::{
66-
layout::Alignment,
67-
style::{Color, Modifier, Style},
68-
widgets::{Block, Borders, Paragraph},
69-
};
70-
71-
let area = frame.area();
72-
let placeholder =
73-
Paragraph::new("Statistics Screen - Coming Soon!\n\nPress Esc to return to menu")
74-
.style(
75-
Style::default()
76-
.fg(Color::Cyan)
77-
.add_modifier(Modifier::BOLD),
78-
)
79-
.alignment(Alignment::Center)
80-
.block(Block::default().title(" Statistics ").borders(Borders::ALL));
81-
82-
frame.render_widget(placeholder, area);
83-
}

0 commit comments

Comments
 (0)