Skip to main content

max / makenotwork

5.0 KB · 157 lines History Blame Raw
1 //! Promo code management screen — list, create, delete codes.
2
3 use ratatui::Frame;
4 use ratatui::layout::{Constraint, Layout};
5 use ratatui::style::{Color, Modifier, Style};
6 use ratatui::text::{Line, Span};
7 use ratatui::widgets::{Block, Borders, Paragraph, Row};
8
9 use super::App;
10 use super::widgets;
11
12 pub fn render(frame: &mut Frame, app: &App) {
13 let area = frame.area();
14
15 let title = Line::from(vec![
16 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
17 Span::raw(" -- "),
18 Span::styled("Promo Codes", Style::default().add_modifier(Modifier::BOLD)),
19 Span::raw(" "),
20 ]);
21
22 let block = Block::default()
23 .title(title)
24 .borders(Borders::ALL)
25 .border_style(Style::default().fg(Color::Gray));
26
27 let inner = block.inner(area);
28 frame.render_widget(block, area);
29
30 let chunks = Layout::vertical([
31 Constraint::Length(1), // spacer
32 Constraint::Length(1), // section header
33 Constraint::Min(3), // code list
34 Constraint::Length(1), // status line
35 Constraint::Length(1), // keybindings
36 ])
37 .split(inner);
38
39 // Section header
40 let count = app.promo_codes.len();
41 let header = Paragraph::new(Line::from(vec![
42 Span::raw(" "),
43 Span::styled("Codes", Style::default().add_modifier(Modifier::BOLD)),
44 if count == 0 {
45 Span::raw("")
46 } else {
47 Span::raw(format!(" ({})", count))
48 },
49 ]));
50 frame.render_widget(header, chunks[1]);
51
52 // Code list
53 if app.loading {
54 let loading = Paragraph::new(" Loading...");
55 frame.render_widget(loading, chunks[2]);
56 } else if app.promo_codes.is_empty() {
57 let empty = Paragraph::new(" No promo codes. Press [n] to create one.");
58 frame.render_widget(empty, chunks[2]);
59 } else {
60 render_code_table(frame, app, chunks[2]);
61 }
62
63 // Status line
64 if let Some(ref status) = app.promo_status {
65 let style = if status.starts_with("Error") {
66 Style::default().fg(Color::Red)
67 } else {
68 Style::default().fg(Color::Green)
69 };
70 let status_line = Paragraph::new(Line::from(vec![
71 Span::raw(" "),
72 Span::styled(status.as_str(), style),
73 ]));
74 frame.render_widget(status_line, chunks[3]);
75 }
76
77 // Keybindings
78 let editing = app.promo_editing_step.is_some();
79 let mut key_spans = vec![Span::raw(" ")];
80
81 if editing {
82 key_spans.extend([
83 Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)),
84 Span::raw(" Confirm "),
85 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
86 Span::raw(" Cancel"),
87 ]);
88 } else {
89 key_spans.extend([
90 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
91 Span::raw(" Nav "),
92 Span::styled("[n]", Style::default().add_modifier(Modifier::BOLD)),
93 Span::raw(" New "),
94 ]);
95 if !app.promo_codes.is_empty() {
96 key_spans.extend([
97 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
98 Span::raw(" Delete "),
99 ]);
100 }
101 key_spans.extend([
102 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
103 Span::raw(" Refresh "),
104 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
105 Span::raw(" Back"),
106 ]);
107 }
108
109 let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray));
110 frame.render_widget(keys, chunks[4]);
111 }
112
113 fn render_code_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
114 let rows: Vec<Row> = app
115 .promo_codes
116 .iter()
117 .enumerate()
118 .map(|(i, code)| {
119 let discount = format_discount(code.discount_type.as_deref(), code.discount_value);
120 let scope = code
121 .item_title
122 .as_deref()
123 .or(code.project_title.as_deref())
124 .unwrap_or("All items");
125 let uses = match code.max_uses {
126 Some(max) => format!("{}/{}", code.use_count, max),
127 None => code.use_count.to_string(),
128 };
129
130 Row::new(vec![
131 format!(" {}", code.code),
132 discount,
133 scope.to_string(),
134 uses,
135 ])
136 .style(widgets::selected_style(i, Some(app.selected_index)))
137 })
138 .collect();
139
140 let widths = [
141 Constraint::Min(16),
142 Constraint::Length(12),
143 Constraint::Length(20),
144 Constraint::Length(10),
145 ];
146
147 widgets::render_table(frame, area, &[" Code", "Discount", "Scope", "Uses"], &widths, rows);
148 }
149
150 fn format_discount(dtype: Option<&str>, value: Option<i32>) -> String {
151 match (dtype, value) {
152 (Some("percentage"), Some(v)) => format!("{}% off", v),
153 (Some("fixed"), Some(v)) => format!("${}.{:02} off", v / 100, v % 100),
154 _ => "Free".to_string(),
155 }
156 }
157