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 data_source: None,
282 }),
283 ],
284 footer: Some(vec![Component::Button(Button {
285 id: Some("edit".to_string()),
286 label: "Edit Profile".to_string(),
287 action_id: "edit_profile".to_string(),
288 variant: ButtonVariant::Secondary,
289 disabled: false,
290 icon: None,
291 })]),
292 })]
293}
294
295fn settings_template(data: &TemplateData) -> Vec<Component> {
296 vec![Component::Card(Card {
297 id: Some("settings-card".to_string()),
298 title: Some(data.title.clone().unwrap_or_else(|| "Settings".to_string())),
299 description: data
300 .description
301 .clone()
302 .or_else(|| Some("Manage your preferences".to_string())),
303 content: vec![
304 Component::Switch(Switch {
305 id: Some("notifications".to_string()),
306 name: "notifications".to_string(),
307 label: "Email Notifications".to_string(),
308 default_checked: true,
309 }),
310 Component::Switch(Switch {
311 id: Some("dark_mode".to_string()),
312 name: "dark_mode".to_string(),
313 label: "Dark Mode".to_string(),
314 default_checked: false,
315 }),
316 Component::Select(Select {
317 id: Some("language".to_string()),
318 name: "language".to_string(),
319 label: "Language".to_string(),
320 options: vec![
321 SelectOption {
322 value: "en".to_string(),
323 label: "English".to_string(),
324 },
325 SelectOption {
326 value: "es".to_string(),
327 label: "Spanish".to_string(),
328 },
329 SelectOption {
330 value: "fr".to_string(),
331 label: "French".to_string(),
332 },
333 ],
334 required: false,
335 error: None,
336 }),
337 ],
338 footer: Some(vec![Component::Button(Button {
339 id: Some("save".to_string()),
340 label: "Save Settings".to_string(),
341 action_id: "save_settings".to_string(),
342 variant: ButtonVariant::Primary,
343 disabled: false,
344 icon: None,
345 })]),
346 })]
347}
348
349fn confirm_delete_template(data: &TemplateData) -> Vec<Component> {
350 vec![Component::Modal(Modal {
351 id: Some("confirm-delete-modal".to_string()),
352 title: data
353 .title
354 .clone()
355 .unwrap_or_else(|| "Confirm Deletion".to_string()),
356 content: vec![Component::Alert(Alert {
357 id: None,
358 title: "Warning".to_string(),
359 description: Some(data.message.clone().unwrap_or_else(|| {
360 "This action cannot be undone. All data will be permanently deleted.".to_string()
361 })),
362 variant: AlertVariant::Warning,
363 })],
364 footer: Some(vec![
365 Component::Button(Button {
366 id: Some("cancel".to_string()),
367 label: "Cancel".to_string(),
368 action_id: "cancel_delete".to_string(),
369 variant: ButtonVariant::Secondary,
370 disabled: false,
371 icon: None,
372 }),
373 Component::Button(Button {
374 id: Some("confirm".to_string()),
375 label: "Delete".to_string(),
376 action_id: "confirm_delete".to_string(),
377 variant: ButtonVariant::Danger,
378 disabled: false,
379 icon: None,
380 }),
381 ]),
382 size: ModalSize::Small,
383 closable: true,
384 })]
385}
386
387fn status_dashboard_template(data: &TemplateData) -> Vec<Component> {
388 let stats = if data.stats.is_empty() {
389 vec![
390 StatItem {
391 label: "CPU".to_string(),
392 value: "45%".to_string(),
393 status: Some("ok".to_string()),
394 },
395 StatItem {
396 label: "Memory".to_string(),
397 value: "78%".to_string(),
398 status: Some("warning".to_string()),
399 },
400 StatItem {
401 label: "Disk".to_string(),
402 value: "32%".to_string(),
403 status: Some("ok".to_string()),
404 },
405 ]
406 } else {
407 data.stats.clone()
408 };
409
410 vec![
411 Component::Text(Text {
412 id: None,
413 content: data
414 .title
415 .clone()
416 .unwrap_or_else(|| "System Status".to_string()),
417 variant: TextVariant::H2,
418 }),
419 Component::Grid(Grid {
420 id: None,
421 columns: stats.len().min(4) as u8,
422 gap: 4,
423 children: stats
424 .iter()
425 .map(|stat| {
426 let status_variant = match stat.status.as_deref() {
427 Some("ok") | Some("success") => BadgeVariant::Success,
428 Some("warning") => BadgeVariant::Warning,
429 Some("error") | Some("critical") => BadgeVariant::Error,
430 _ => BadgeVariant::Default,
431 };
432 Component::Card(Card {
433 id: None,
434 title: None,
435 description: None,
436 content: vec![
437 Component::Text(Text {
438 id: None,
439 content: stat.label.clone(),
440 variant: TextVariant::Caption,
441 }),
442 Component::Text(Text {
443 id: None,
444 content: stat.value.clone(),
445 variant: TextVariant::H3,
446 }),
447 Component::Badge(Badge {
448 id: None,
449 label: stat.status.clone().unwrap_or_else(|| "ok".to_string()),
450 variant: status_variant,
451 }),
452 ],
453 footer: None,
454 })
455 })
456 .collect(),
457 }),
458 ]
459}
460
461fn data_table_template(data: &TemplateData) -> Vec<Component> {
462 let columns = if data.columns.is_empty() {
463 vec![
464 TableColumn {
465 header: "ID".to_string(),
466 accessor_key: "id".to_string(),
467 sortable: true,
468 },
469 TableColumn {
470 header: "Name".to_string(),
471 accessor_key: "name".to_string(),
472 sortable: true,
473 },
474 TableColumn {
475 header: "Status".to_string(),
476 accessor_key: "status".to_string(),
477 sortable: false,
478 },
479 ]
480 } else {
481 data.columns.clone()
482 };
483
484 vec![
485 Component::Text(Text {
486 id: None,
487 content: data.title.clone().unwrap_or_else(|| "Data".to_string()),
488 variant: TextVariant::H2,
489 }),
490 Component::Table(Table {
491 id: Some("data-table".to_string()),
492 columns,
493 data: data.rows.clone(),
494 data_source: None,
495 sortable: true,
496 page_size: Some(10),
497 striped: true,
498 }),
499 ]
500}
501
502fn success_message_template(data: &TemplateData) -> Vec<Component> {
503 vec![Component::Alert(Alert {
504 id: Some("success-alert".to_string()),
505 title: data.title.clone().unwrap_or_else(|| "Success!".to_string()),
506 description: data
507 .message
508 .clone()
509 .or_else(|| Some("Operation completed successfully.".to_string())),
510 variant: AlertVariant::Success,
511 })]
512}
513
514fn error_message_template(data: &TemplateData) -> Vec<Component> {
515 vec![Component::Alert(Alert {
516 id: Some("error-alert".to_string()),
517 title: data.title.clone().unwrap_or_else(|| "Error".to_string()),
518 description: data
519 .message
520 .clone()
521 .or_else(|| Some("Something went wrong. Please try again.".to_string())),
522 variant: AlertVariant::Error,
523 })]
524}
525
526fn loading_template(data: &TemplateData) -> Vec<Component> {
527 vec![Component::Spinner(Spinner {
528 id: Some("loading-spinner".to_string()),
529 size: SpinnerSize::Large,
530 label: data
531 .message
532 .clone()
533 .or_else(|| Some("Loading...".to_string())),
534 })]
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn test_registration_template() {
543 let response = render_template(UiTemplate::Registration, TemplateData::default());
544 assert_eq!(response.components.len(), 1);
545 }
546
547 #[test]
548 fn test_template_from_name() {
549 assert_eq!(
550 UiTemplate::from_name("registration"),
551 Some(UiTemplate::Registration)
552 );
553 assert_eq!(
554 UiTemplate::from_name("signup"),
555 Some(UiTemplate::Registration)
556 );
557 assert_eq!(UiTemplate::from_name("login"), Some(UiTemplate::Login));
558 assert_eq!(UiTemplate::from_name("unknown"), None);
559 }
560}