1use maud::{html, Markup, PreEscaped};
3
4#[derive(Clone, Debug)]
5pub struct Column {
6 pub key: String,
7 pub label: String,
8 pub sortable: bool,
9}
10
11#[derive(Clone, Debug)]
12pub struct Props {
13 pub id: String,
14 pub columns: Vec<Column>,
15 pub rows: Vec<Vec<String>>,
16 pub page_size: usize,
17 pub searchable: bool,
18 pub search_placeholder: String,
19}
20
21impl Default for Props {
22 fn default() -> Self {
23 Self {
24 id: "data-table".to_string(),
25 columns: vec![],
26 rows: vec![],
27 page_size: 5,
28 searchable: false,
29 search_placeholder: "Filter...".to_string(),
30 }
31 }
32}
33
34fn escape_for_attr(s: &str) -> String {
36 s.replace('\\', "\\\\")
37 .replace('"', """)
38 .replace('<', "<")
39 .replace('>', ">")
40 .replace('&', "&")
41}
42
43pub fn render(props: Props) -> Markup {
44 let total = props.rows.len();
45 let page_size = if props.page_size == 0 { 5 } else { props.page_size };
46 let end = std::cmp::min(page_size, total);
47 let start = if total == 0 { 0 } else { 1 };
48 let has_next = end < total;
49
50 html! {
51 div.mui-data-table data-mui="data-table" id=(props.id) data-page-size=(page_size) {
52 @if props.searchable {
53 div.mui-data-table__toolbar {
54 input type="text" class="mui-input mui-data-table__search"
55 placeholder=(props.search_placeholder);
56 }
57 }
58 div.mui-data-table__wrapper {
59 table.mui-table.mui-table--hoverable {
60 thead {
61 tr {
62 @for col in &props.columns {
63 th.mui-table__th.mui-data-table__th
64 data-key=(col.key)
65 data-sortable=(col.sortable) {
66 (col.label)
67 @if col.sortable {
68 span.mui-data-table__sort-icon {
69 (PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>"#))
70 }
71 }
72 }
73 }
74 }
75 }
76 tbody.mui-data-table__body {
77 @for (i, row) in props.rows.iter().enumerate() {
78 @let json_row = format!(
79 "[{}]",
80 row.iter()
81 .map(|c| format!("\"{}\"", escape_for_attr(c)))
82 .collect::<Vec<_>>()
83 .join(",")
84 );
85 tr.mui-table__row
86 data-row-data=(json_row)
87 hidden[i >= page_size] {
88 @for cell in row {
89 td.mui-table__td { (cell) }
90 }
91 }
92 }
93 }
94 }
95 }
96 div.mui-data-table__footer {
97 span.mui-data-table__info {
98 (format!("Showing {}-{} of {}", start, end, total))
99 }
100 div.mui-data-table__pagination {
101 button type="button" class="mui-data-table__page-btn" data-action="prev" disabled {
102 "Previous"
103 }
104 button type="button" class="mui-data-table__page-btn" data-action="next" disabled[!has_next] {
105 "Next"
106 }
107 }
108 }
109 }
110 }
111}
112
113pub fn showcase() -> Markup {
114 let columns = vec![
115 Column { key: "invoice".to_string(), label: "Invoice".to_string(), sortable: true },
116 Column { key: "status".to_string(), label: "Status".to_string(), sortable: true },
117 Column { key: "method".to_string(), label: "Method".to_string(), sortable: true },
118 Column { key: "amount".to_string(), label: "Amount".to_string(), sortable: true },
119 ];
120
121 let rows = vec![
122 vec!["INV001".to_string(), "Paid".to_string(), "Credit Card".to_string(), "$250.00".to_string()],
123 vec!["INV002".to_string(), "Pending".to_string(), "PayPal".to_string(), "$150.00".to_string()],
124 vec!["INV003".to_string(), "Unpaid".to_string(), "Bank Transfer".to_string(), "$350.00".to_string()],
125 vec!["INV004".to_string(), "Paid".to_string(), "Credit Card".to_string(), "$450.00".to_string()],
126 vec!["INV005".to_string(), "Paid".to_string(), "PayPal".to_string(), "$550.00".to_string()],
127 vec!["INV006".to_string(), "Pending".to_string(), "Bank Transfer".to_string(), "$200.00".to_string()],
128 vec!["INV007".to_string(), "Paid".to_string(), "Credit Card".to_string(), "$300.00".to_string()],
129 vec!["INV008".to_string(), "Unpaid".to_string(), "PayPal".to_string(), "$400.00".to_string()],
130 vec!["INV009".to_string(), "Paid".to_string(), "Bank Transfer".to_string(), "$500.00".to_string()],
131 vec!["INV010".to_string(), "Pending".to_string(), "Credit Card".to_string(), "$275.00".to_string()],
132 ];
133
134 html! {
135 div.mui-showcase__grid {
136 div {
137 p.mui-showcase__caption { "Searchable, sortable, paginated (5 per page)" }
138 (render(Props {
139 id: "invoice-table".to_string(),
140 columns,
141 rows,
142 page_size: 5,
143 searchable: true,
144 search_placeholder: "Filter invoices...".to_string(),
145 }))
146 }
147 }
148 }
149}