1use crate::schema::*;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum UiTemplate {
12 Registration,
14 Login,
16 UserProfile,
18 Settings,
20 ConfirmDelete,
22 StatusDashboard,
24 DataTable,
26 SuccessMessage,
28 ErrorMessage,
30 Loading,
32}
33
34impl UiTemplate {
35 pub fn all_names() -> &'static [&'static str] {
37 &[
38 "registration",
39 "login",
40 "user_profile",
41 "settings",
42 "confirm_delete",
43 "status_dashboard",
44 "data_table",
45 "success_message",
46 "error_message",
47 "loading",
48 ]
49 }
50
51 pub fn from_name(name: &str) -> Option<Self> {
53 match name.to_lowercase().as_str() {
54 "registration" | "register" | "signup" => Some(Self::Registration),
55 "login" | "signin" => Some(Self::Login),
56 "user_profile" | "profile" => Some(Self::UserProfile),
57 "settings" | "preferences" => Some(Self::Settings),
58 "confirm_delete" | "delete_confirm" => Some(Self::ConfirmDelete),
59 "status_dashboard" | "dashboard" | "status" => Some(Self::StatusDashboard),
60 "data_table" | "table" => Some(Self::DataTable),
61 "success_message" | "success" => Some(Self::SuccessMessage),
62 "error_message" | "error" => Some(Self::ErrorMessage),
63 "loading" | "spinner" => Some(Self::Loading),
64 _ => None,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct TemplateData {
72 pub title: Option<String>,
74 pub description: Option<String>,
76 pub user: Option<UserData>,
78 pub data: HashMap<String, String>,
80 pub stats: Vec<StatItem>,
82 pub columns: Vec<TableColumn>,
84 pub rows: Vec<HashMap<String, serde_json::Value>>,
86 pub message: Option<String>,
88 pub theme: Option<Theme>,
90}
91
92#[derive(Debug, Clone)]
94pub struct UserData {
95 pub name: String,
96 pub email: String,
97 pub avatar_url: Option<String>,
98 pub role: Option<String>,
99}
100
101#[derive(Debug, Clone)]
103pub struct StatItem {
104 pub label: String,
105 pub value: String,
106 pub status: Option<String>,
107}
108
109pub fn render_template(template: UiTemplate, data: TemplateData) -> UiResponse {
111 let components = match template {
112 UiTemplate::Registration => registration_template(&data),
113 UiTemplate::Login => login_template(&data),
114 UiTemplate::UserProfile => user_profile_template(&data),
115 UiTemplate::Settings => settings_template(&data),
116 UiTemplate::ConfirmDelete => confirm_delete_template(&data),
117 UiTemplate::StatusDashboard => status_dashboard_template(&data),
118 UiTemplate::DataTable => data_table_template(&data),
119 UiTemplate::SuccessMessage => success_message_template(&data),
120 UiTemplate::ErrorMessage => error_message_template(&data),
121 UiTemplate::Loading => loading_template(&data),
122 };
123
124 let mut response = UiResponse::new(components);
125 if let Some(theme) = data.theme {
126 response = response.with_theme(theme);
127 }
128 response
129}
130
131fn registration_template(data: &TemplateData) -> Vec<Component> {
134 vec![Component::Card(Card {
135 id: Some("registration-card".to_string()),
136 title: Some(data.title.clone().unwrap_or_else(|| "Create Account".to_string())),
137 description: data
138 .description
139 .clone()
140 .or_else(|| Some("Enter your details to register".to_string())),
141 content: vec![
142 Component::TextInput(TextInput {
143 id: Some("name".to_string()),
144 name: "name".to_string(),
145 label: "Full Name".to_string(),
146 placeholder: Some("Enter your name".to_string()),
147 input_type: "text".to_string(),
148 required: true,
149 default_value: None,
150 error: None,
151 min_length: Some(2),
152 max_length: Some(100),
153 }),
154 Component::TextInput(TextInput {
155 id: Some("email".to_string()),
156 name: "email".to_string(),
157 label: "Email".to_string(),
158 placeholder: Some("you@example.com".to_string()),
159 input_type: "email".to_string(),
160 required: true,
161 default_value: None,
162 error: None,
163 min_length: None,
164 max_length: None,
165 }),
166 Component::TextInput(TextInput {
167 id: Some("password".to_string()),
168 name: "password".to_string(),
169 label: "Password".to_string(),
170 placeholder: Some("Choose a strong password".to_string()),
171 input_type: "password".to_string(),
172 required: true,
173 default_value: None,
174 error: None,
175 min_length: Some(8),
176 max_length: None,
177 }),
178 ],
179 footer: Some(vec![Component::Button(Button {
180 id: Some("submit".to_string()),
181 label: "Create Account".to_string(),
182 action_id: "register_submit".to_string(),
183 variant: ButtonVariant::Primary,
184 disabled: false,
185 icon: None,
186 })]),
187 })]
188}
189
190fn login_template(data: &TemplateData) -> Vec<Component> {
191 vec![Component::Card(Card {
192 id: Some("login-card".to_string()),
193 title: Some(data.title.clone().unwrap_or_else(|| "Welcome Back".to_string())),
194 description: data
195 .description
196 .clone()
197 .or_else(|| Some("Sign in to your account".to_string())),
198 content: vec![
199 Component::TextInput(TextInput {
200 id: Some("email".to_string()),
201 name: "email".to_string(),
202 label: "Email".to_string(),
203 placeholder: Some("you@example.com".to_string()),
204 input_type: "email".to_string(),
205 required: true,
206 default_value: None,
207 error: None,
208 min_length: None,
209 max_length: None,
210 }),
211 Component::TextInput(TextInput {
212 id: Some("password".to_string()),
213 name: "password".to_string(),
214 label: "Password".to_string(),
215 placeholder: Some("Enter your password".to_string()),
216 input_type: "password".to_string(),
217 required: true,
218 default_value: None,
219 error: None,
220 min_length: None,
221 max_length: None,
222 }),
223 ],
224 footer: Some(vec![Component::Button(Button {
225 id: Some("submit".to_string()),
226 label: "Sign In".to_string(),
227 action_id: "login_submit".to_string(),
228 variant: ButtonVariant::Primary,
229 disabled: false,
230 icon: None,
231 })]),
232 })]
233}
234
235fn user_profile_template(data: &TemplateData) -> Vec<Component> {
236 let user = data.user.as_ref();
237 let name = user.map(|u| u.name.clone()).unwrap_or_else(|| "User".to_string());
238 let email = user.map(|u| u.email.clone()).unwrap_or_else(|| "user@example.com".to_string());
239 let role = user.and_then(|u| u.role.clone()).unwrap_or_else(|| "Member".to_string());
240
241 vec![Component::Card(Card {
242 id: Some("profile-card".to_string()),
243 title: Some(data.title.clone().unwrap_or_else(|| "User Profile".to_string())),
244 description: None,
245 content: vec![
246 Component::Text(Text {
247 id: None,
248 content: format!("**{}**", name),
249 variant: TextVariant::H3,
250 }),
251 Component::Badge(Badge { id: None, label: role, variant: BadgeVariant::Info }),
252 Component::Divider(Divider { id: None }),
253 Component::KeyValue(KeyValue {
254 id: None,
255 pairs: vec![KeyValuePair { key: "Email".to_string(), value: email }],
256 }),
257 ],
258 footer: Some(vec![Component::Button(Button {
259 id: Some("edit".to_string()),
260 label: "Edit Profile".to_string(),
261 action_id: "edit_profile".to_string(),
262 variant: ButtonVariant::Secondary,
263 disabled: false,
264 icon: None,
265 })]),
266 })]
267}
268
269fn settings_template(data: &TemplateData) -> Vec<Component> {
270 vec![Component::Card(Card {
271 id: Some("settings-card".to_string()),
272 title: Some(data.title.clone().unwrap_or_else(|| "Settings".to_string())),
273 description: data
274 .description
275 .clone()
276 .or_else(|| Some("Manage your preferences".to_string())),
277 content: vec![
278 Component::Switch(Switch {
279 id: Some("notifications".to_string()),
280 name: "notifications".to_string(),
281 label: "Email Notifications".to_string(),
282 default_checked: true,
283 }),
284 Component::Switch(Switch {
285 id: Some("dark_mode".to_string()),
286 name: "dark_mode".to_string(),
287 label: "Dark Mode".to_string(),
288 default_checked: false,
289 }),
290 Component::Select(Select {
291 id: Some("language".to_string()),
292 name: "language".to_string(),
293 label: "Language".to_string(),
294 options: vec![
295 SelectOption { value: "en".to_string(), label: "English".to_string() },
296 SelectOption { value: "es".to_string(), label: "Spanish".to_string() },
297 SelectOption { value: "fr".to_string(), label: "French".to_string() },
298 ],
299 required: false,
300 error: None,
301 }),
302 ],
303 footer: Some(vec![Component::Button(Button {
304 id: Some("save".to_string()),
305 label: "Save Settings".to_string(),
306 action_id: "save_settings".to_string(),
307 variant: ButtonVariant::Primary,
308 disabled: false,
309 icon: None,
310 })]),
311 })]
312}
313
314fn confirm_delete_template(data: &TemplateData) -> Vec<Component> {
315 vec![Component::Modal(Modal {
316 id: Some("confirm-delete-modal".to_string()),
317 title: data.title.clone().unwrap_or_else(|| "Confirm Deletion".to_string()),
318 content: vec![Component::Alert(Alert {
319 id: None,
320 title: "Warning".to_string(),
321 description: Some(data.message.clone().unwrap_or_else(|| {
322 "This action cannot be undone. All data will be permanently deleted.".to_string()
323 })),
324 variant: AlertVariant::Warning,
325 })],
326 footer: Some(vec![
327 Component::Button(Button {
328 id: Some("cancel".to_string()),
329 label: "Cancel".to_string(),
330 action_id: "cancel_delete".to_string(),
331 variant: ButtonVariant::Secondary,
332 disabled: false,
333 icon: None,
334 }),
335 Component::Button(Button {
336 id: Some("confirm".to_string()),
337 label: "Delete".to_string(),
338 action_id: "confirm_delete".to_string(),
339 variant: ButtonVariant::Danger,
340 disabled: false,
341 icon: None,
342 }),
343 ]),
344 size: ModalSize::Small,
345 closable: true,
346 })]
347}
348
349fn status_dashboard_template(data: &TemplateData) -> Vec<Component> {
350 let stats = if data.stats.is_empty() {
351 vec![
352 StatItem {
353 label: "CPU".to_string(),
354 value: "45%".to_string(),
355 status: Some("ok".to_string()),
356 },
357 StatItem {
358 label: "Memory".to_string(),
359 value: "78%".to_string(),
360 status: Some("warning".to_string()),
361 },
362 StatItem {
363 label: "Disk".to_string(),
364 value: "32%".to_string(),
365 status: Some("ok".to_string()),
366 },
367 ]
368 } else {
369 data.stats.clone()
370 };
371
372 vec![
373 Component::Text(Text {
374 id: None,
375 content: data.title.clone().unwrap_or_else(|| "System Status".to_string()),
376 variant: TextVariant::H2,
377 }),
378 Component::Grid(Grid {
379 id: None,
380 columns: stats.len().min(4) as u8,
381 gap: 4,
382 children: stats
383 .iter()
384 .map(|stat| {
385 let status_variant = match stat.status.as_deref() {
386 Some("ok") | Some("success") => BadgeVariant::Success,
387 Some("warning") => BadgeVariant::Warning,
388 Some("error") | Some("critical") => BadgeVariant::Error,
389 _ => BadgeVariant::Default,
390 };
391 Component::Card(Card {
392 id: None,
393 title: None,
394 description: None,
395 content: vec![
396 Component::Text(Text {
397 id: None,
398 content: stat.label.clone(),
399 variant: TextVariant::Caption,
400 }),
401 Component::Text(Text {
402 id: None,
403 content: stat.value.clone(),
404 variant: TextVariant::H3,
405 }),
406 Component::Badge(Badge {
407 id: None,
408 label: stat.status.clone().unwrap_or_else(|| "ok".to_string()),
409 variant: status_variant,
410 }),
411 ],
412 footer: None,
413 })
414 })
415 .collect(),
416 }),
417 ]
418}
419
420fn data_table_template(data: &TemplateData) -> Vec<Component> {
421 let columns = if data.columns.is_empty() {
422 vec![
423 TableColumn {
424 header: "ID".to_string(),
425 accessor_key: "id".to_string(),
426 sortable: true,
427 },
428 TableColumn {
429 header: "Name".to_string(),
430 accessor_key: "name".to_string(),
431 sortable: true,
432 },
433 TableColumn {
434 header: "Status".to_string(),
435 accessor_key: "status".to_string(),
436 sortable: false,
437 },
438 ]
439 } else {
440 data.columns.clone()
441 };
442
443 vec![
444 Component::Text(Text {
445 id: None,
446 content: data.title.clone().unwrap_or_else(|| "Data".to_string()),
447 variant: TextVariant::H2,
448 }),
449 Component::Table(Table {
450 id: Some("data-table".to_string()),
451 columns,
452 data: data.rows.clone(),
453 sortable: true,
454 page_size: Some(10),
455 striped: true,
456 }),
457 ]
458}
459
460fn success_message_template(data: &TemplateData) -> Vec<Component> {
461 vec![Component::Alert(Alert {
462 id: Some("success-alert".to_string()),
463 title: data.title.clone().unwrap_or_else(|| "Success!".to_string()),
464 description: data
465 .message
466 .clone()
467 .or_else(|| Some("Operation completed successfully.".to_string())),
468 variant: AlertVariant::Success,
469 })]
470}
471
472fn error_message_template(data: &TemplateData) -> Vec<Component> {
473 vec![Component::Alert(Alert {
474 id: Some("error-alert".to_string()),
475 title: data.title.clone().unwrap_or_else(|| "Error".to_string()),
476 description: data
477 .message
478 .clone()
479 .or_else(|| Some("Something went wrong. Please try again.".to_string())),
480 variant: AlertVariant::Error,
481 })]
482}
483
484fn loading_template(data: &TemplateData) -> Vec<Component> {
485 vec![Component::Spinner(Spinner {
486 id: Some("loading-spinner".to_string()),
487 size: SpinnerSize::Large,
488 label: data.message.clone().or_else(|| Some("Loading...".to_string())),
489 })]
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_registration_template() {
498 let response = render_template(UiTemplate::Registration, TemplateData::default());
499 assert_eq!(response.components.len(), 1);
500 }
501
502 #[test]
503 fn test_template_from_name() {
504 assert_eq!(UiTemplate::from_name("registration"), Some(UiTemplate::Registration));
505 assert_eq!(UiTemplate::from_name("signup"), Some(UiTemplate::Registration));
506 assert_eq!(UiTemplate::from_name("login"), Some(UiTemplate::Login));
507 assert_eq!(UiTemplate::from_name("unknown"), None);
508 }
509}