1use dioxus::prelude::*;
4
5use crate::ui::{
6 pages::PageWrapper,
7 state::{use_app_dispatch, use_app_state},
8};
9
10#[component]
12pub fn Profile() -> Element {
13 let app_state = use_app_state();
14 let _dispatch = use_app_dispatch();
15
16 let current_user = app_state.current_user.clone();
18
19 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 use_effect({
31 let current_user = current_user.clone();
32 move || {
33 if let Some(user) = ¤t_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 move |_| {
47 save_message.set(None);
48 saving.set(true);
49
50 spawn({
52 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 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#[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#[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 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 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 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 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 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 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#[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#[component]
448fn SecuritySection() -> Element {
449 let security_items = rsx! {
450 div {
451 class: "space-y-6",
452
453 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 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 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#[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 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 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 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#[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}