Skip to main content

max / makenotwork

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