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(
137 data.title
138 .clone()
139 .unwrap_or_else(|| "Create Account".to_string()),
140 ),
141 description: data
142 .description
143 .clone()
144 .or_else(|| Some("Enter your details to register".to_string())),
145 content: vec![
146 Component::TextInput(TextInput {
147 id: Some("name".to_string()),
148 name: "name".to_string(),
149 label: "Full Name".to_string(),
150 placeholder: Some("Enter your name".to_string()),
151 input_type: "text".to_string(),
152 required: true,
153 default_value: None,
154 error: None,
155 min_length: Some(2),
156 max_length: Some(100),
157 }),
158 Component::TextInput(TextInput {
159 id: Some("email".to_string()),
160 name: "email".to_string(),
161 label: "Email".to_string(),
162 placeholder: Some("you@example.com".to_string()),
163 input_type: "email".to_string(),
164 required: true,
165 default_value: None,
166 error: None,
167 min_length: None,
168 max_length: None,
169 }),
170 Component::TextInput(TextInput {
171 id: Some("password".to_string()),
172 name: "password".to_string(),
173 label: "Password".to_string(),
174 placeholder: Some("Choose a strong password".to_string()),
175 input_type: "password".to_string(),
176 required: true,
177 default_value: None,
178 error: None,
179 min_length: Some(8),
180 max_length: None,
181 }),
182 ],
183 footer: Some(vec![Component::Button(Button {
184 id: Some("submit".to_string()),
185 label: "Create Account".to_string(),
186 action_id: "register_submit".to_string(),
187 variant: ButtonVariant::Primary,
188 disabled: false,
189 icon: None,
190 })]),
191 })]
192}
193
194fn login_template(data: &TemplateData) -> Vec<Component> {
195 vec![Component::Card(Card {
196 id: Some("login-card".to_string()),
197 title: Some(
198 data.title
199 .clone()
200 .unwrap_or_else(|| "Welcome Back".to_string()),
201 ),
202 description: data
203 .description
204 .clone()
205 .or_else(|| Some("Sign in to your account".to_string())),
206 content: vec![
207 Component::TextInput(TextInput {
208 id: Some("email".to_string()),
209 name: "email".to_string(),
210 label: "Email".to_string(),
211 placeholder: Some("you@example.com".to_string()),
212 input_type: "email".to_string(),
213 required: true,
214 default_value: None,
215 error: None,
216 min_length: None,
217 max_length: None,
218 }),
219 Component::TextInput(TextInput {
220 id: Some("password".to_string()),
221 name: "password".to_string(),
222 label: "Password".to_string(),
223 placeholder: Some("Enter your password".to_string()),
224 input_type: "password".to_string(),
225 required: true,
226 default_value: None,
227 error: None,
228 min_length: None,
229 max_length: None,
230 }),
231 ],
232 footer: Some(vec![Component::Button(Button {
233 id: Some("submit".to_string()),
234 label: "Sign In".to_string(),
235 action_id: "login_submit".to_string(),
236 variant: ButtonVariant::Primary,
237 disabled: false,
238 icon: None,
239 })]),
240 })]
241}
242
243fn user_profile_template(data: &TemplateData) -> Vec<Component> {
244 let user = data.user.as_ref();
245 let name = user
246 .map(|u| u.name.clone())
247 .unwrap_or_else(|| "User".to_string());
248 let email = user
249 .map(|u| u.email.clone())
250 .unwrap_or_else(|| "user@example.com".to_string());
251 let role = user
252 .and_then(|u| u.role.clone())
253 .unwrap_or_else(|| "Member".to_string());
254
255 vec![Component::Card(Card {
256 id: Some("profile-card".to_string()),
257 title: Some(
258 data.title
259 .clone()
260 .unwrap_or_else(|| "User Profile".to_string()),
261 ),
262 description: None,
263 content: vec![
264 Component::Text(Text {
265 id: None,
266 content: format!("**{}**", name),
267 variant: TextVariant::H3,
268 }),
269 Component::Badge(Badge {
270 id: None,
271 label: role,
272 variant: BadgeVariant::Info,
273 }),
274 Component::Divider(Divider { id: None }),
275 Component::KeyValue(KeyValue {
276 id: None,
277 pairs: vec![KeyValuePair {
278 key: "Email".to_string(),
279 value: email,
280 }],
281 }),
282 ],
283 footer: Some(vec![Component::Button(Button {
284 id: Some("edit".to_string()),
285 label: "Edit Profile".to_string(),
286 action_id: "edit_profile".to_string(),
287 variant: ButtonVariant::Secondary,
288 disabled: false,
289 icon: None,
290 })]),
291 })]
292}
293
294fn settings_template(data: &TemplateData) -> Vec<Component> {
295 vec![Component::Card(Card {
296 id: Some("settings-card".to_string()),
297 title: Some(data.title.clone().unwrap_or_else(|| "Settings".to_string())),
298 description: data
299 .description
300 .clone()
301 .or_else(|| Some("Manage your preferences".to_string())),
302 content: vec![
303 Component::Switch(Switch {
304 id: Some("notifications".to_string()),
305 name: "notifications".to_string(),
306 label: "Email Notifications".to_string(),
307 default_checked: true,
308 }),
309 Component::Switch(Switch {
310 id: Some("dark_mode".to_string()),
311 name: "dark_mode".to_string(),
312 label: "Dark Mode".to_string(),
313 default_checked: false,
314 }),
315 Component::Select(Select {
316 id: Some("language".to_string()),
317 name: "language".to_string(),
318 label: "Language".to_string(),
319 options: vec![
320 SelectOption {
321 value: "en".to_string(),
322 label: "English".to_string(),
323 },
324 SelectOption {
325 value: "es".to_string(),
326 label: "Spanish".to_string(),
327 },
328 SelectOption {
329 value: "fr".to_string(),
330 label: "French".to_string(),
331 },
332 ],
333 required: false,
334 error: None,
335 }),
336 ],
337 footer: Some(vec![Component::Button(Button {
338 id: Some("save".to_string()),
339 label: "Save Settings".to_string(),
340 action_id: "save_settings".to_string(),
341 variant: ButtonVariant::Primary,
342 disabled: false,
343 icon: None,
344 })]),
345 })]
346}
347
348fn confirm_delete_template(data: &TemplateData) -> Vec<Component> {
349 vec![Component::Modal(Modal {
350 id: Some("confirm-delete-modal".to_string()),
351 title: data
352 .title
353 .clone()
354 .unwrap_or_else(|| "Confirm Deletion".to_string()),
355 content: vec![Component::Alert(Alert {
356 id: None,
357 title: "Warning".to_string(),
358 description: Some(data.message.clone().unwrap_or_else(|| {
359 "This action cannot be undone. All data will be permanently deleted.".to_string()
360 })),
361 variant: AlertVariant::Warning,
362 })],
363 footer: Some(vec![
364 Component::Button(Button {
365 id: Some("cancel".to_string()),
366 label: "Cancel".to_string(),
367 action_id: "cancel_delete".to_string(),
368 variant: ButtonVariant::Secondary,
369 disabled: false,
370 icon: None,
371 }),
372 Component::Button(Button {
373 id: Some("confirm".to_string()),
374 label: "Delete".to_string(),
375 action_id: "confirm_delete".to_string(),
376 variant: ButtonVariant::Danger,
377 disabled: false,
378 icon: None,
379 }),
380 ]),
381 size: ModalSize::Small,
382 closable: true,
383 })]
384}
385
386fn status_dashboard_template(data: &TemplateData) -> Vec<Component> {
387 let stats = if data.stats.is_empty() {
388 vec![
389 StatItem {
390 label: "CPU".to_string(),
391 value: "45%".to_string(),
392 status: Some("ok".to_string()),
393 },
394 StatItem {
395 label: "Memory".to_string(),
396 value: "78%".to_string(),
397 status: Some("warning".to_string()),
398 },
399 StatItem {
400 label: "Disk".to_string(),
401 value: "32%".to_string(),
402 status: Some("ok".to_string()),
403 },
404 ]
405 } else {
406 data.stats.clone()
407 };
408
409 vec![
410 Component::Text(Text {
411 id: None,
412 content: data
413 .title
414 .clone()
415 .unwrap_or_else(|| "System Status".to_string()),
416 variant: TextVariant::H2,
417 }),
418 Component::Grid(Grid {
419 id: None,
420 columns: stats.len().min(4) as u8,
421 gap: 4,
422 children: stats
423 .iter()
424 .map(|stat| {
425 let status_variant = match stat.status.as_deref() {
426 Some("ok") | Some("success") => BadgeVariant::Success,
427 Some("warning") => BadgeVariant::Warning,
428 Some("error") | Some("critical") => BadgeVariant::Error,
429 _ => BadgeVariant::Default,
430 };
431 Component::Card(Card {
432 id: None,
433 title: None,
434 description: None,
435 content: vec![
436 Component::Text(Text {
437 id: None,
438 content: stat.label.clone(),
439 variant: TextVariant::Caption,
440 }),
441 Component::Text(Text {
442 id: None,
443 content: stat.value.clone(),
444 variant: TextVariant::H3,
445 }),
446 Component::Badge(Badge {
447 id: None,
448 label: stat.status.clone().unwrap_or_else(|| "ok".to_string()),
449 variant: status_variant,
450 }),
451 ],
452 footer: None,
453 })
454 })
455 .collect(),
456 }),
457 ]
458}
459
460fn data_table_template(data: &TemplateData) -> Vec<Component> {
461 let columns = if data.columns.is_empty() {
462 vec![
463 TableColumn {
464 header: "ID".to_string(),
465 accessor_key: "id".to_string(),
466 sortable: true,
467 },
468 TableColumn {
469 header: "Name".to_string(),
470 accessor_key: "name".to_string(),
471 sortable: true,
472 },
473 TableColumn {
474 header: "Status".to_string(),
475 accessor_key: "status".to_string(),
476 sortable: false,
477 },
478 ]
479 } else {
480 data.columns.clone()
481 };
482
483 vec![
484 Component::Text(Text {
485 id: None,
486 content: data.title.clone().unwrap_or_else(|| "Data".to_string()),
487 variant: TextVariant::H2,
488 }),
489 Component::Table(Table {
490 id: Some("data-table".to_string()),
491 columns,
492 data: data.rows.clone(),
493 sortable: true,
494 page_size: Some(10),
495 striped: true,
496 }),
497 ]
498}
499
500fn success_message_template(data: &TemplateData) -> Vec<Component> {
501 vec![Component::Alert(Alert {
502 id: Some("success-alert".to_string()),
503 title: data.title.clone().unwrap_or_else(|| "Success!".to_string()),
504 description: data
505 .message
506 .clone()
507 .or_else(|| Some("Operation completed successfully.".to_string())),
508 variant: AlertVariant::Success,
509 })]
510}
511
512fn error_message_template(data: &TemplateData) -> Vec<Component> {
513 vec![Component::Alert(Alert {
514 id: Some("error-alert".to_string()),
515 title: data.title.clone().unwrap_or_else(|| "Error".to_string()),
516 description: data
517 .message
518 .clone()
519 .or_else(|| Some("Something went wrong. Please try again.".to_string())),
520 variant: AlertVariant::Error,
521 })]
522}
523
524fn loading_template(data: &TemplateData) -> Vec<Component> {
525 vec![Component::Spinner(Spinner {
526 id: Some("loading-spinner".to_string()),
527 size: SpinnerSize::Large,
528 label: data
529 .message
530 .clone()
531 .or_else(|| Some("Loading...".to_string())),
532 })]
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_registration_template() {
541 let response = render_template(UiTemplate::Registration, TemplateData::default());
542 assert_eq!(response.components.len(), 1);
543 }
544
545 #[test]
546 fn test_template_from_name() {
547 assert_eq!(
548 UiTemplate::from_name("registration"),
549 Some(UiTemplate::Registration)
550 );
551 assert_eq!(
552 UiTemplate::from_name("signup"),
553 Some(UiTemplate::Registration)
554 );
555 assert_eq!(UiTemplate::from_name("login"), Some(UiTemplate::Login));
556 assert_eq!(UiTemplate::from_name("unknown"), None);
557 }
558}