use crate::icons;
use crate::theme::Theme;
use arct_core::{UserStats, Difficulty};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
Frame,
};
pub struct ProgressPanel;
impl ProgressPanel {
pub fn new() -> Self {
Self
}
pub fn render(&self, frame: &mut Frame, theme: &Theme, stats: &UserStats) {
let area = Self::centered_rect(70, 60, frame.size());
frame.render_widget(Clear, area);
let block = Block::default()
.title(format!(" {}Your Progress Dashboard ", icons::target().content))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_style(theme.style_border_focused())
.style(theme.style_block());
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(8), Constraint::Length(10), Constraint::Min(3), Constraint::Length(2), ])
.split(inner);
let overview_items = vec![
ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled("", theme.style_accent()),
Span::raw(" Current Streak: "),
Span::styled(
format!("{} days", stats.current_streak),
theme.style_accent().add_modifier(ratatui::style::Modifier::BOLD),
),
Span::raw(" (Best: "),
Span::styled(
format!("{}", stats.longest_streak),
theme.style_dim(),
),
Span::raw(" days)"),
])),
ListItem::new(Line::from("")),
ListItem::new(Line::from(vec![
Span::raw(" "),
icons::lesson(),
Span::raw(" Lessons Completed: "),
Span::styled(
format!("{}", stats.lessons_completed.len()),
theme.style_accent(),
),
])),
ListItem::new(Line::from(vec![
Span::raw(" "),
icons::shell(),
Span::raw(" Commands Mastered: "),
Span::styled(
format!("{}", stats.commands_used.len()),
theme.style_accent(),
),
])),
ListItem::new(Line::from(vec![
Span::raw(" "),
icons::celebration(),
Span::raw(" Achievements Unlocked: "),
Span::styled(
format!("{}", stats.achievements.total_unlocked()),
theme.style_accent(),
),
Span::raw(" ("),
Span::styled(
format!("{} points", stats.achievements.total_points()),
theme.style_dim(),
),
Span::raw(")"),
])),
ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled(icons::TIMER, theme.style_accent()),
Span::raw(" Time Invested: "),
Span::styled(
format_time(stats.total_time_seconds),
theme.style_dim(),
),
])),
];
let overview_list = List::new(overview_items);
frame.render_widget(overview_list, chunks[0]);
let difficulty_section = Self::render_difficulty_progress(theme, stats);
frame.render_widget(difficulty_section, chunks[1]);
let command_text = if stats.commands_used.is_empty() {
vec![
Line::from(""),
Line::from(vec![
Span::styled(" No commands used yet. ", theme.style_dim()),
Span::styled("Start practicing to see your progress!", theme.style_normal()),
]),
]
} else {
let mut lines = vec![
Line::from(vec![
Span::styled(" Most Used Commands:", theme.style_header()),
]),
Line::from(""),
];
let mut commands: Vec<_> = stats.commands_used.iter().collect();
commands.sort();
for cmd in commands.iter().take(5) {
lines.push(Line::from(vec![
Span::raw(" "),
icons::shell(),
Span::styled(cmd.to_string(), theme.style_accent()),
]));
}
if commands.len() > 5 {
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("... and {} more", commands.len() - 5),
theme.style_dim(),
),
]));
}
lines
};
let commands_para = Paragraph::new(command_text);
frame.render_widget(commands_para, chunks[2]);
let controls = Paragraph::new(vec![Line::from(vec![
Span::styled("Esc", theme.style_accent()),
Span::raw(" or "),
Span::styled("p", theme.style_accent()),
Span::raw(" to close"),
])])
.alignment(Alignment::Center);
frame.render_widget(controls, chunks[3]);
}
fn render_difficulty_progress(theme: &Theme, stats: &UserStats) -> Paragraph<'static> {
let total_per_difficulty = 10;
let difficulties = vec![
(Difficulty::Beginner, "Beginner"),
(Difficulty::Intermediate, "Intermediate"),
(Difficulty::Advanced, "Advanced"),
];
let mut lines = vec![
Line::from(vec![
Span::styled(" Progress by Difficulty:", theme.style_header()),
]),
Line::from(""),
];
for (difficulty, name) in difficulties {
let completed = stats
.lessons_by_difficulty
.get(&difficulty)
.copied()
.unwrap_or(0);
let percentage = (completed as f64 / total_per_difficulty as f64 * 100.0).min(100.0);
let bar_width = 30;
let filled = (percentage / 100.0 * bar_width as f64) as usize;
let empty = bar_width - filled;
let bar_filled = "█".repeat(filled);
let bar_empty = "░".repeat(empty);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(format!("{:12}", name), theme.style_normal()),
Span::raw(" ["),
Span::styled(bar_filled, theme.style_accent()),
Span::styled(bar_empty, theme.style_dim()),
Span::raw("] "),
Span::styled(
format!("{}/{} ({:.0}%)", completed, total_per_difficulty, percentage),
theme.style_dim(),
),
]));
}
Paragraph::new(lines)
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
}
impl Default for ProgressPanel {
fn default() -> Self {
Self::new()
}
}
fn format_time(seconds: u64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
}