qorzen_oxide/ui/pages/
profile.rs

1// src/ui/pages/profile.rs - User profile management page
2
3use dioxus::prelude::*;
4
5use crate::ui::{
6    pages::PageWrapper,
7    state::{use_app_dispatch, use_app_state},
8};
9
10/// Profile page component
11#[component]
12pub fn Profile() -> Element {
13    let app_state = use_app_state();
14    let _dispatch = use_app_dispatch();
15
16    // Clone user data to avoid borrowing issues
17    let current_user = app_state.current_user.clone();
18
19    // Form state
20    let mut display_name = use_signal(String::new);
21    let mut email = use_signal(String::new);
22    let mut bio = use_signal(String::new);
23    let mut department = use_signal(String::new);
24    let mut title = use_signal(String::new);
25    let mut phone = use_signal(String::new);
26    let mut saving = use_signal(|| false);
27    let mut save_message = use_signal(|| None::<String>);
28
29    // Initialize form with current user data
30    use_effect({
31        let current_user = current_user.clone();
32        move || {
33            if let Some(user) = &current_user {
34                display_name.set(user.profile.display_name.clone());
35                email.set(user.email.clone());
36                bio.set(user.profile.bio.clone().unwrap_or_default());
37                department.set(user.profile.department.clone().unwrap_or_default());
38                title.set(user.profile.title.clone().unwrap_or_default());
39                phone.set(user.profile.contact_info.phone.clone().unwrap_or_default());
40            }
41        }
42    });
43
44    let handle_save = {
45        // let dispatch = dispatch.clone();
46        move |_| {
47            save_message.set(None);
48            saving.set(true);
49
50            // Simulate save operation
51            spawn({
52                // let dispatch = dispatch.clone();
53                async move {
54                    #[cfg(not(target_arch = "wasm32"))]
55                    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
56                    #[cfg(target_arch = "wasm32")]
57                    gloo_timers::future::TimeoutFuture::new(1000).await;
58
59                    // In a real app, this would update the user via API
60                    save_message.set(Some("Profile updated successfully!".to_string()));
61                    saving.set(false);
62                }
63            });
64        }
65    };
66
67    rsx! {
68        PageWrapper {
69            title: "Profile".to_string(),
70            subtitle: Some("Manage your account settings and personal information".to_string()),
71
72            div {
73                class: "space-y-6",
74
75                ProfileOverviewCard { user: current_user.clone() }
76                EditProfileForm {
77                    display_name: display_name,
78                    email: email,
79                    bio: bio,
80                    department: department,
81                    title: title,
82                    phone: phone,
83                    saving: saving,
84                    save_message: save_message,
85                    on_save: handle_save
86                }
87                SecuritySection {}
88                PreferencesSection {}
89            }
90        }
91    }
92}
93
94/// Profile overview card component
95#[component]
96fn ProfileOverviewCard(user: Option<crate::ui::state::User>) -> Element {
97    let profile_header = if let Some(user) = &user {
98        let avatar_section = if let Some(avatar_url) = &user.profile.avatar_url {
99            rsx! {
100                img {
101                    class: "h-20 w-20 rounded-full border-4 border-white",
102                    src: "{avatar_url}",
103                    alt: "{user.profile.display_name}"
104                }
105            }
106        } else {
107            let initial = user.profile.display_name.chars().next().unwrap_or('U');
108            rsx! {
109                div {
110                    class: "h-20 w-20 rounded-full bg-white bg-opacity-20 flex items-center justify-center border-4 border-white",
111                    span {
112                        class: "text-3xl font-bold text-white",
113                        "{initial}"
114                    }
115                }
116            }
117        };
118
119        let user_title = if let Some(title) = &user.profile.title {
120            rsx! {
121                p {
122                    class: "text-blue-100",
123                    "{title}"
124                }
125            }
126        } else {
127            rsx! {}
128        };
129
130        rsx! {
131            div {
132                class: "flex items-center",
133                div {
134                    class: "flex-shrink-0",
135                    {avatar_section}
136                }
137                div {
138                    class: "ml-6",
139                    h1 {
140                        class: "text-2xl font-bold text-white",
141                        "{user.profile.display_name}"
142                    }
143                    p {
144                        class: "text-blue-100",
145                        "{user.email}"
146                    }
147                    {user_title}
148                    p {
149                        class: "text-blue-200 text-sm mt-2",
150                        "Member since {user.created_at.format(\"%B %Y\")}"
151                    }
152                }
153            }
154        }
155    } else {
156        rsx! {
157            div {
158                class: "text-center",
159                p {
160                    class: "text-white text-lg",
161                    "No user data available"
162                }
163            }
164        }
165    };
166
167    rsx! {
168        div {
169            class: "bg-white shadow rounded-lg overflow-hidden",
170            div {
171                class: "bg-gradient-to-r from-blue-500 to-purple-600 px-6 py-8",
172                {profile_header}
173            }
174        }
175    }
176}
177
178/// Edit profile form component
179#[component]
180fn EditProfileForm(
181    display_name: Signal<String>,
182    email: Signal<String>,
183    bio: Signal<String>,
184    department: Signal<String>,
185    title: Signal<String>,
186    phone: Signal<String>,
187    saving: Signal<bool>,
188    save_message: Signal<Option<String>>,
189    on_save: Callback<Event<FormData>>,
190) -> Element {
191    let success_message = if let Some(message) = save_message() {
192        rsx! {
193            div {
194                class: "mb-6 rounded-md bg-green-50 p-4",
195                div {
196                    class: "flex",
197                    div {
198                        class: "flex-shrink-0",
199                        svg {
200                            class: "h-5 w-5 text-green-400",
201                            xmlns: "http://www.w3.org/2000/svg",
202                            view_box: "0 0 20 20",
203                            fill: "currentColor",
204                            path {
205                                fill_rule: "evenodd",
206                                d: "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",
207                                clip_rule: "evenodd"
208                            }
209                        }
210                    }
211                    div {
212                        class: "ml-3",
213                        p {
214                            class: "text-sm font-medium text-green-800",
215                            "{message}"
216                        }
217                    }
218                }
219            }
220        }
221    } else {
222        rsx! {}
223    };
224
225    let form_fields = rsx! {
226        div {
227            class: "grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2",
228
229            // Display name
230            div {
231                class: "sm:col-span-1",
232                label {
233                    r#for: "display_name",
234                    class: "block text-sm font-medium text-gray-700",
235                    "Display Name"
236                }
237                div {
238                    class: "mt-1",
239                    input {
240                        r#type: "text",
241                        name: "display_name",
242                        id: "display_name",
243                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
244                        value: "{display_name}",
245                        oninput: move |e| display_name.set(e.value())
246                    }
247                }
248            }
249
250            // Email
251            div {
252                class: "sm:col-span-1",
253                label {
254                    r#for: "email",
255                    class: "block text-sm font-medium text-gray-700",
256                    "Email Address"
257                }
258                div {
259                    class: "mt-1",
260                    input {
261                        r#type: "email",
262                        name: "email",
263                        id: "email",
264                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
265                        value: "{email}",
266                        oninput: move |e| email.set(e.value())
267                    }
268                }
269            }
270
271            // Department
272            div {
273                class: "sm:col-span-1",
274                label {
275                    r#for: "department",
276                    class: "block text-sm font-medium text-gray-700",
277                    "Department"
278                }
279                div {
280                    class: "mt-1",
281                    input {
282                        r#type: "text",
283                        name: "department",
284                        id: "department",
285                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
286                        value: "{department}",
287                        oninput: move |e| department.set(e.value())
288                    }
289                }
290            }
291
292            // Job title
293            div {
294                class: "sm:col-span-1",
295                label {
296                    r#for: "title",
297                    class: "block text-sm font-medium text-gray-700",
298                    "Job Title"
299                }
300                div {
301                    class: "mt-1",
302                    input {
303                        r#type: "text",
304                        name: "title",
305                        id: "title",
306                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
307                        value: "{title}",
308                        oninput: move |e| title.set(e.value())
309                    }
310                }
311            }
312
313            // Phone
314            div {
315                class: "sm:col-span-1",
316                label {
317                    r#for: "phone",
318                    class: "block text-sm font-medium text-gray-700",
319                    "Phone Number"
320                }
321                div {
322                    class: "mt-1",
323                    input {
324                        r#type: "tel",
325                        name: "phone",
326                        id: "phone",
327                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
328                        value: "{phone}",
329                        oninput: move |e| phone.set(e.value())
330                    }
331                }
332            }
333
334            // Bio
335            div {
336                class: "sm:col-span-2",
337                label {
338                    r#for: "bio",
339                    class: "block text-sm font-medium text-gray-700",
340                    "Bio"
341                }
342                div {
343                    class: "mt-1",
344                    textarea {
345                        id: "bio",
346                        name: "bio",
347                        rows: "3",
348                        class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md",
349                        placeholder: "Tell us about yourself...",
350                        value: "{bio}",
351                        oninput: move |e| bio.set(e.value())
352                    }
353                }
354                p {
355                    class: "mt-2 text-sm text-gray-500",
356                    "Brief description for your profile."
357                }
358            }
359        }
360    };
361
362    let action_buttons = rsx! {
363        div {
364            class: "pt-6 border-t border-gray-200 flex justify-end space-x-3",
365            button {
366                r#type: "button",
367                class: "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
368                "Cancel"
369            }
370            SaveButton { saving: saving }
371        }
372    };
373
374    rsx! {
375        div {
376            class: "bg-white shadow rounded-lg",
377            div {
378                class: "px-4 py-5 sm:px-6 border-b border-gray-200",
379                h3 {
380                    class: "text-lg leading-6 font-medium text-gray-900",
381                    "Personal Information"
382                }
383                p {
384                    class: "mt-1 max-w-2xl text-sm text-gray-500",
385                    "Update your personal details and contact information."
386                }
387            }
388
389            form {
390                class: "px-4 py-5 sm:p-6",
391                onsubmit: on_save,
392
393                {success_message}
394                {form_fields}
395                {action_buttons}
396            }
397        }
398    }
399}
400
401/// Save button component
402#[component]
403fn SaveButton(saving: Signal<bool>) -> Element {
404    if saving() {
405        rsx! {
406            button {
407                r#type: "submit",
408                class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50",
409                disabled: true,
410                span {
411                    class: "flex items-center",
412                    svg {
413                        class: "animate-spin -ml-1 mr-2 h-4 w-4",
414                        xmlns: "http://www.w3.org/2000/svg",
415                        fill: "none",
416                        view_box: "0 0 24 24",
417                        circle {
418                            class: "opacity-25",
419                            cx: "12",
420                            cy: "12",
421                            r: "10",
422                            stroke: "currentColor",
423                            stroke_width: "4"
424                        }
425                        path {
426                            class: "opacity-75",
427                            fill: "currentColor",
428                            d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
429                        }
430                    }
431                    "Saving..."
432                }
433            }
434        }
435    } else {
436        rsx! {
437            button {
438                r#type: "submit",
439                class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
440                "Save Changes"
441            }
442        }
443    }
444}
445
446/// Security settings section
447#[component]
448fn SecuritySection() -> Element {
449    let security_items = rsx! {
450        div {
451            class: "space-y-6",
452
453            // Change password
454            div {
455                class: "flex items-center justify-between",
456                div {
457                    h4 {
458                        class: "text-sm font-medium text-gray-900",
459                        "Password"
460                    }
461                    p {
462                        class: "text-sm text-gray-500",
463                        "Last updated 3 months ago"
464                    }
465                }
466                button {
467                    r#type: "button",
468                    class: "bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
469                    "Change Password"
470                }
471            }
472
473            // Two-factor authentication
474            div {
475                class: "flex items-center justify-between",
476                div {
477                    h4 {
478                        class: "text-sm font-medium text-gray-900",
479                        "Two-factor Authentication"
480                    }
481                    p {
482                        class: "text-sm text-gray-500",
483                        "Add an extra layer of security to your account"
484                    }
485                }
486                button {
487                    r#type: "button",
488                    class: "bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
489                    "Enable 2FA"
490                }
491            }
492
493            // Sessions
494            div {
495                class: "flex items-center justify-between",
496                div {
497                    h4 {
498                        class: "text-sm font-medium text-gray-900",
499                        "Active Sessions"
500                    }
501                    p {
502                        class: "text-sm text-gray-500",
503                        "Manage your active sessions on other devices"
504                    }
505                }
506                button {
507                    r#type: "button",
508                    class: "bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
509                    "View Sessions"
510                }
511            }
512        }
513    };
514
515    rsx! {
516        div {
517            class: "bg-white shadow rounded-lg",
518            div {
519                class: "px-4 py-5 sm:px-6 border-b border-gray-200",
520                h3 {
521                    class: "text-lg leading-6 font-medium text-gray-900",
522                    "Security"
523                }
524                p {
525                    class: "mt-1 max-w-2xl text-sm text-gray-500",
526                    "Manage your account security settings."
527                }
528            }
529            div {
530                class: "px-4 py-5 sm:p-6",
531                {security_items}
532            }
533        }
534    }
535}
536
537/// Preferences section
538#[component]
539fn PreferencesSection() -> Element {
540    let mut theme = use_signal(|| "light".to_string());
541    let mut language = use_signal(|| "en".to_string());
542    let notifications = use_signal(|| true);
543
544    let preferences_items = rsx! {
545        div {
546            class: "space-y-6",
547
548            // Theme selection
549            div {
550                label {
551                    class: "block text-sm font-medium text-gray-700",
552                    "Theme"
553                }
554                div {
555                    class: "mt-1",
556                    select {
557                        class: "block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md",
558                        value: "{theme}",
559                        onchange: move |e| theme.set(e.value()),
560                        option { value: "light", "Light" }
561                        option { value: "dark", "Dark" }
562                        option { value: "auto", "Auto (System)" }
563                    }
564                }
565            }
566
567            // Language selection
568            div {
569                label {
570                    class: "block text-sm font-medium text-gray-700",
571                    "Language"
572                }
573                div {
574                    class: "mt-1",
575                    select {
576                        class: "block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md",
577                        value: "{language}",
578                        onchange: move |e| language.set(e.value()),
579                        option { value: "en", "English" }
580                        option { value: "es", "Español" }
581                        option { value: "fr", "Français" }
582                        option { value: "de", "Deutsch" }
583                    }
584                }
585            }
586
587            // Notifications toggle
588            NotificationToggle { notifications: notifications }
589        }
590    };
591
592    rsx! {
593        div {
594            class: "bg-white shadow rounded-lg",
595            div {
596                class: "px-4 py-5 sm:px-6 border-b border-gray-200",
597                h3 {
598                    class: "text-lg leading-6 font-medium text-gray-900",
599                    "Preferences"
600                }
601                p {
602                    class: "mt-1 max-w-2xl text-sm text-gray-500",
603                    "Customize your application experience."
604                }
605            }
606            div {
607                class: "px-4 py-5 sm:p-6",
608                {preferences_items}
609            }
610        }
611    }
612}
613
614/// Notification toggle component
615#[component]
616fn NotificationToggle(notifications: Signal<bool>) -> Element {
617    let toggle_class = if notifications() {
618        "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 bg-blue-600"
619    } else {
620        "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 bg-gray-200"
621    };
622
623    let toggle_dot_class = if notifications() {
624        "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-5"
625    } else {
626        "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"
627    };
628
629    rsx! {
630        div {
631            class: "flex items-center justify-between",
632            div {
633                h4 {
634                    class: "text-sm font-medium text-gray-900",
635                    "Email Notifications"
636                }
637                p {
638                    class: "text-sm text-gray-500",
639                    "Receive email updates about your account activity"
640                }
641            }
642            button {
643                r#type: "button",
644                class: "{toggle_class}",
645                onclick: move |_| notifications.set(!notifications()),
646                span {
647                    class: "{toggle_dot_class}"
648                }
649            }
650        }
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use dioxus::prelude::*;
658
659    #[test]
660    fn test_profile_component_creation() {
661        let _profile = rsx! { Profile {} };
662    }
663
664    #[test]
665    fn test_security_section_creation() {
666        let _security = rsx! { SecuritySection {} };
667    }
668
669    #[test]
670    fn test_preferences_section_creation() {
671        let _preferences = rsx! { PreferencesSection {} };
672    }
673}