Skip to main content

max / makenotwork

5.1 KB · 163 lines History Blame Raw
1 //! Subscription tier list screen — read-only view of project tiers.
2
3 use makeover_tui::makeover_layout::{Column, Priority, Width};
4 use makeover_tui::table::{self, Cell, Sizing, TableStyle};
5 use ratatui::Frame;
6 use ratatui::layout::{Constraint, Layout};
7 use ratatui::style::{Modifier, Style};
8 use ratatui::text::{Line, Span};
9 use ratatui::widgets::{Block, Borders, Paragraph, TableState};
10
11 use crate::format;
12
13 use super::App;
14
15 /// The subscription tier columns. A tier is a name and a price, so both are
16 /// essential; the description is the first thing to go.
17 const TIER_COLUMNS: [Column<'static>; 4] = [
18 Column {
19 name: "Name",
20 width: Width::Fill,
21 priority: Priority::Essential,
22 sortable: false,
23 sorted: None,
24 },
25 Column {
26 name: "Price",
27 width: Width::Fixed,
28 priority: Priority::Essential,
29 sortable: false,
30 sorted: None,
31 },
32 Column {
33 name: "Status",
34 width: Width::Fixed,
35 priority: Priority::Secondary,
36 sortable: false,
37 sorted: None,
38 },
39 Column {
40 name: "Description",
41 width: Width::Fixed,
42 priority: Priority::Optional,
43 sortable: false,
44 sorted: None,
45 },
46 ];
47
48 /// The tracks the hand-written `Constraint`s carried. The description is
49 /// truncated to fit its 30, which is why it is a fixed track and not a fill.
50 const TIER_SIZING: Sizing<'static> = Sizing {
51 lengths: &[
52 ("Name", 16),
53 ("Price", 10),
54 ("Status", 10),
55 ("Description", 30),
56 ],
57 fallback: 10,
58 };
59
60 pub(crate) fn render(frame: &mut Frame, app: &App) {
61 let area = frame.area();
62
63 let project_title = app.tiers_project_title.as_deref().unwrap_or("Project");
64
65 let title = Line::from(vec![
66 Span::styled(
67 " Makenot.work ",
68 Style::default().add_modifier(Modifier::BOLD),
69 ),
70 Span::raw(" -- "),
71 Span::styled(project_title, Style::default().add_modifier(Modifier::BOLD)),
72 Span::raw(" -- "),
73 Span::styled(
74 "Subscription Tiers",
75 Style::default().add_modifier(Modifier::BOLD),
76 ),
77 Span::raw(" "),
78 ]);
79
80 let block = Block::default()
81 .title(title)
82 .borders(Borders::ALL)
83 .border_style(Style::default().fg(app.theme.line_border));
84
85 let inner = block.inner(area);
86 frame.render_widget(block, area);
87
88 let chunks = Layout::vertical([
89 Constraint::Length(1), // spacer
90 Constraint::Length(1), // section header
91 Constraint::Min(3), // list
92 Constraint::Length(1), // status line
93 Constraint::Length(1), // keybindings
94 ])
95 .split(inner);
96
97 let count = app.tiers.len();
98 let header = Paragraph::new(Line::from(vec![
99 Span::raw(" "),
100 Span::styled("Tiers", Style::default().add_modifier(Modifier::BOLD)),
101 if count == 0 {
102 Span::raw("")
103 } else {
104 Span::raw(format!(" ({count})"))
105 },
106 ]));
107 frame.render_widget(header, chunks[1]);
108
109 if app.loading {
110 let loading = Paragraph::new(" Loading...");
111 frame.render_widget(loading, chunks[2]);
112 } else if app.tiers.is_empty() {
113 let empty = Paragraph::new(" No subscription tiers for this project.");
114 frame.render_widget(empty, chunks[2]);
115 } else {
116 let area = super::indent(chunks[2], 2);
117 let rows: Vec<Vec<Cell>> = app
118 .tiers
119 .iter()
120 .map(|t| {
121 let desc = if t.description.len() > 30 {
122 format!("{}...", &t.description[..27])
123 } else {
124 t.description.clone()
125 };
126 vec![
127 Cell::new("Name", t.name.as_str()),
128 Cell::new("Price", format::format_price(t.price_cents, app.currency())),
129 Cell::new("Status", if t.is_active { "active" } else { "inactive" }),
130 Cell::new("Description", desc),
131 ]
132 })
133 .collect();
134
135 let style = TableStyle::from_theme(&app.theme);
136 let table = table::table(&TIER_COLUMNS, &rows, &TIER_SIZING, &style, area.width);
137 let mut state = TableState::default().with_selected(Some(app.selected_index));
138 frame.render_stateful_widget(table, area, &mut state);
139 }
140
141 if let Some(ref status) = app.tiers_status {
142 let style = if status.starts_with("Error") {
143 Style::default().fg(app.theme.status_danger)
144 } else {
145 Style::default().fg(app.theme.status_success)
146 };
147 let status_line = Paragraph::new(format!(" {status}")).style(style);
148 frame.render_widget(status_line, chunks[3]);
149 }
150
151 let key_spans = vec![
152 Span::raw(" "),
153 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
154 Span::raw(" Nav "),
155 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
156 Span::raw(" Back"),
157 ];
158
159 let keys =
160 Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted));
161 frame.render_widget(keys, chunks[4]);
162 }
163