From 01871f4214acf3c0e512a152e99f05a3dac08085 Mon Sep 17 00:00:00 2001 From: DavePk04 <Dave.Pikop.Pokam@ulb.be> Date: Sat, 29 Mar 2025 16:02:58 +0100 Subject: [PATCH 1/5] Added 'My profile' item within the tutor header dropdown menu --- frontend/src/lang/en.json | 3 +++ frontend/src/lang/fr.json | 3 +++ frontend/src/routes/Header.svelte | 34 +++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index d3f6b7a0..348357fc 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -8,6 +8,9 @@ "users": "Users", "studies": "Studies" }, + "tutor": { + "profile": "My profile" + }, "availability": "Availability", "language": "Language", "register": "Sign up", diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 1cade0cc..e873b8c7 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -13,6 +13,9 @@ "users": "Utilisateurs", "sessions": "Sessions", "studies": "Études" + }, + "tutor": { + "profile": "Mon profil" } }, "chatbox": { diff --git a/frontend/src/routes/Header.svelte b/frontend/src/routes/Header.svelte index 876faf2f..87a19947 100644 --- a/frontend/src/routes/Header.svelte +++ b/frontend/src/routes/Header.svelte @@ -51,7 +51,7 @@ </div> <div class="navbar-end hidden sm:flex"> <ul class="menu menu-horizontal p-0 flex items-center"> - {#if user} + {#if user?.type === 2} <li> <details> <summary class="px-3"> @@ -71,7 +71,34 @@ </ul> </details> </li> - {#if user?.type === 0} + {/if} + {#if user?.type === 1} + <li> + <details> + <summary class="px-3"> + <img + src={`https://gravatar.com/avatar/${user.emailHash}?d=identicon`} + alt={''} + class="rounded-full border text-sm size-8 border-neutral-400" + /> + {user.nickname} + </summary> + <ul class="menu menu-sm dropdown-content absolute right-0"> + <li> + <a data-sveltekit-reload href="/logout" class="whitespace-nowrap"> + {$t('header.signout')} + </a> + </li> + <li> + <a data-sveltekit-reload href="/tutor/profile" class="whitespace-nowrap"> + {$t('header.tutor.profile')} + </a> + </li> + </ul> + </details> + </li> + {/if} + {#if user?.type === 0} <li> <details> <summary class="p-3"> @@ -96,8 +123,7 @@ </ul> </details> </li> - {/if} - {:else} + {:else if !user} <li> <a class="btn btn-sm my-auto" -- GitLab From 3bbdda10559f2006a22c5683d0eaabda291fc5fe Mon Sep 17 00:00:00 2001 From: DavePk04 <Dave.Pikop.Pokam@ulb.be> Date: Sat, 29 Mar 2025 16:04:43 +0100 Subject: [PATCH 2/5] --wip-- --- frontend/src/routes/Header.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/routes/Header.svelte b/frontend/src/routes/Header.svelte index 87a19947..f8282ce3 100644 --- a/frontend/src/routes/Header.svelte +++ b/frontend/src/routes/Header.svelte @@ -120,6 +120,11 @@ {$t('header.admin.studies')} </a> </li> + <li> + <a data-sveltekit-reload href="/logout" class="whitespace-nowrap"> + {$t('header.signout')} + </a> + </li> </ul> </details> </li> -- GitLab From f543d7cadbeb09e3bb325900020527537c3f3311 Mon Sep 17 00:00:00 2001 From: DavePk04 <Dave.Pikop.Pokam@ulb.be> Date: Sat, 29 Mar 2025 16:53:48 +0100 Subject: [PATCH 3/5] UI: created tutor profile page --- frontend/src/lang/en.json | 9 +- frontend/src/lang/fr.json | 11 +- frontend/src/routes/Header.svelte | 58 +++---- .../src/routes/tutor/profile/+page.svelte | 142 ++++++++++++++++++ frontend/src/routes/tutor/profile/+page.ts | 0 5 files changed, 186 insertions(+), 34 deletions(-) create mode 100644 frontend/src/routes/tutor/profile/+page.svelte create mode 100644 frontend/src/routes/tutor/profile/+page.ts diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 348357fc..929a2b0c 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -9,7 +9,11 @@ "studies": "Studies" }, "tutor": { - "profile": "My profile" + "profile": "My profile", + "update": "Update", + "selectGender": "Select your gender", + "bio": "Biography", + "availabilities": "Availabilities" }, "availability": "Availability", "language": "Language", @@ -52,7 +56,8 @@ "noCurrentOrFutureSessions": "No session in progress or planned", "pastSessions": "Completed sessions", "plannedSessions": "Scheduled sessions", - "sessionAdded": "You have been added to a session with {users}" + "sessionAdded": "You have been added to a session with {users}", + "birthdate": "Birthdate" }, "signup": { "title": "Register", diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index e873b8c7..47c3c157 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -13,9 +13,13 @@ "users": "Utilisateurs", "sessions": "Sessions", "studies": "Études" - }, + }, "tutor": { - "profile": "Mon profil" + "profile": "Mon profil", + "update": "Confirmer", + "selectGender": "Sélectionnez votre genre", + "bio": "Biographie", + "availabilities": "Disponibilités" } }, "chatbox": { @@ -52,7 +56,8 @@ "actions": "Actions", "date": "Date", "participants": "Participants", - "sessionAdded": "Vous avez été ajouté à une session avec {users}" + "sessionAdded": "Vous avez été ajouté à une session avec {users}", + "birthdate": "Date de naissance" }, "login": { "email": "E-mail", diff --git a/frontend/src/routes/Header.svelte b/frontend/src/routes/Header.svelte index f8282ce3..97086bcd 100644 --- a/frontend/src/routes/Header.svelte +++ b/frontend/src/routes/Header.svelte @@ -99,35 +99,35 @@ </li> {/if} {#if user?.type === 0} - <li> - <details> - <summary class="p-3"> - <Icon src={Cog6Tooth} class="h-5 w-5" /> - </summary> - <ul class="menu menu-sm dropdown-content absolute right-0 z-10"> - <li> - <a data-sveltekit-reload href="/admin/users"> - {$t('header.admin.users')} - </a> - </li> - <li> - <a data-sveltekit-reload href="/admin/sessions"> - {$t('header.admin.sessions')} - </a> - </li> - <li> - <a data-sveltekit-reload href="/admin/studies"> - {$t('header.admin.studies')} - </a> - </li> - <li> - <a data-sveltekit-reload href="/logout" class="whitespace-nowrap"> - {$t('header.signout')} - </a> - </li> - </ul> - </details> - </li> + <li> + <details> + <summary class="p-3"> + <Icon src={Cog6Tooth} class="h-5 w-5" /> + </summary> + <ul class="menu menu-sm dropdown-content absolute right-0 z-10"> + <li> + <a data-sveltekit-reload href="/admin/users"> + {$t('header.admin.users')} + </a> + </li> + <li> + <a data-sveltekit-reload href="/admin/sessions"> + {$t('header.admin.sessions')} + </a> + </li> + <li> + <a data-sveltekit-reload href="/admin/studies"> + {$t('header.admin.studies')} + </a> + </li> + <li> + <a data-sveltekit-reload href="/logout" class="whitespace-nowrap"> + {$t('header.signout')} + </a> + </li> + </ul> + </details> + </li> {:else if !user} <li> <a diff --git a/frontend/src/routes/tutor/profile/+page.svelte b/frontend/src/routes/tutor/profile/+page.svelte new file mode 100644 index 00000000..3825c59c --- /dev/null +++ b/frontend/src/routes/tutor/profile/+page.svelte @@ -0,0 +1,142 @@ +<script lang="ts"> + import { t } from '$lib/services/i18n'; + import type { PageData } from './$types'; + + let { data }: { data: PageData } = $props(); + + const formatBirthdate = (dateStr: string | undefined): string => { + if (!dateStr) return ''; + const isoDate = dateStr.split('T')[0]; + if (/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return isoDate; + const [day, month, year] = dateStr.split('/'); + if (year?.length === 4) { + return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + } + + return ''; + }; + + let email = data.user?.email || ''; + let nickname = data.user?.nickname || ''; + let birthdate = formatBirthdate( + data.user?.birthdate ? data.user.birthdate.toString() : undefined + ); + let gender = data.user?.gender || ''; + let bio = data.user?.bio || ''; + let availabilities = data.user?.availabilities + ? JSON.stringify(data.user.availabilities, null, 2) + : ''; + + function updateProfile() { + let parsedAvailabilities; + try { + parsedAvailabilities = availabilities ? JSON.parse(availabilities) : []; + } catch (error) { + alert($t('errors.invalidAvailabilities')); + return; + } + + console.log({ + email, + nickname, + birthdate, + gender, + bio, + availabilities: parsedAvailabilities + }); + alert($t('profile.updatedSuccessfully')); + } +</script> + +<h1 class="text-2xl font-bold text-center my-5">{$t('header.tutor.profile')}</h1> + +<div class="max-w-lg mx-auto bg-white shadow-md rounded-lg p-6"> + <form on:submit|preventDefault={updateProfile}> + <div class="mb-4"> + <label for="email" class="block text-sm font-medium text-gray-700">{$t('home.email')}</label> + <input + type="email" + id="email" + bind:value={email} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + /> + </div> + + <div class="mb-4"> + <label for="nickname" class="block text-sm font-medium text-gray-700" + >{$t('home.nickname')}</label + > + <input + type="text" + id="nickname" + bind:value={nickname} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + /> + </div> + + <div class="mb-4"> + <label for="birthdate" class="block text-sm font-medium text-gray-700" + >{$t('home.birthdate')}</label + > + <input + type="date" + id="birthdate" + bind:value={birthdate} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + /> + </div> + + <div class="mb-4"> + <label for="gender" class="block text-sm font-medium text-gray-700" + >{$t('users.gender')}</label + > + <select + id="gender" + bind:value={gender} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + > + <option value="" disabled>{$t('header.tutor.selectGender')}</option> + <option value="male">{$t('users.genders.male')}</option> + <option value="female">{$t('users.genders.female')}</option> + <option value="other">{$t('users.genders.other')}</option> + </select> + </div> + + <div class="mb-4"> + <label for="bio" class="block text-sm font-medium text-gray-700">{$t('register.bio')}</label> + <textarea + id="bio" + rows="4" + bind:value={bio} + placeholder={$t('header.tutor.bio')} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + ></textarea> + </div> + + <div class="mb-4"> + <label for="availabilities" class="block text-sm font-medium text-gray-700" + >{$t('register.availabilities')}</label + > + <textarea + id="availabilities" + rows="4" + bind:value={availabilities} + placeholder={$t('header.tutor.availabilities')} + class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + required + ></textarea> + </div> + + <button + type="submit" + class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-md shadow-md focus:outline-none focus:ring focus:ring-indigo-200" + > + {$t('header.tutor.update')} + </button> + </form> +</div> diff --git a/frontend/src/routes/tutor/profile/+page.ts b/frontend/src/routes/tutor/profile/+page.ts new file mode 100644 index 00000000..e69de29b -- GitLab From 010353d0784110967b19ce24fa8549abccd6dc01 Mon Sep 17 00:00:00 2001 From: DavePk04 <Dave.Pikop.Pokam@ulb.be> Date: Sat, 29 Mar 2025 22:38:34 +0100 Subject: [PATCH 4/5] Connect form submission to PATCH user API endpoint --- frontend/src/lang/en.json | 3 + frontend/src/lang/fr.json | 3 + .../src/routes/tutor/profile/+page.svelte | 68 +++++++++++-------- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 929a2b0c..5816857a 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -11,6 +11,9 @@ "tutor": { "profile": "My profile", "update": "Update", + "updatedSuccessfully": "Profile updated successfully", + "updateError": "Error updating profile", + "userNotFound": "User not found", "selectGender": "Select your gender", "bio": "Biography", "availabilities": "Availabilities" diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 47c3c157..9e6b4465 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -16,6 +16,9 @@ }, "tutor": { "profile": "Mon profil", + "updatedSuccessfully": "Profil mis à jour avec succès", + "updateError": "Erreur lors de la mise à jour du profil", + "userNotFound": "Utilisateur non trouvé", "update": "Confirmer", "selectGender": "Sélectionnez votre genre", "bio": "Biographie", diff --git a/frontend/src/routes/tutor/profile/+page.svelte b/frontend/src/routes/tutor/profile/+page.svelte index 3825c59c..5ba2a3d7 100644 --- a/frontend/src/routes/tutor/profile/+page.svelte +++ b/frontend/src/routes/tutor/profile/+page.svelte @@ -1,50 +1,63 @@ <script lang="ts"> import { t } from '$lib/services/i18n'; + import { patchUserAPI } from '$lib/api/users'; import type { PageData } from './$types'; let { data }: { data: PageData } = $props(); - const formatBirthdate = (dateStr: string | undefined): string => { + const formatBirthdate = (dateStr: string | Date | undefined): string => { if (!dateStr) return ''; - const isoDate = dateStr.split('T')[0]; - if (/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return isoDate; - const [day, month, year] = dateStr.split('/'); - if (year?.length === 4) { - return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + const date = new Date(dateStr); + if (!isNaN(date.getTime())) { + return date.toISOString().split('T')[0]; + } + if (typeof dateStr === 'string') { + const [day, month, year] = dateStr.split('/'); + if (year?.length === 4) { + return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + } } - return ''; }; let email = data.user?.email || ''; let nickname = data.user?.nickname || ''; - let birthdate = formatBirthdate( - data.user?.birthdate ? data.user.birthdate.toString() : undefined - ); + let birthdate = formatBirthdate(data.user?.birthdate ?? undefined); let gender = data.user?.gender || ''; let bio = data.user?.bio || ''; let availabilities = data.user?.availabilities ? JSON.stringify(data.user.availabilities, null, 2) : ''; - function updateProfile() { - let parsedAvailabilities; + async function updateProfile() { try { - parsedAvailabilities = availabilities ? JSON.parse(availabilities) : []; + const parsedAvailabilities = availabilities ? JSON.parse(availabilities) : []; + console.log('birth:', new Date(birthdate).toISOString()); + const updateData = { + email, + nickname, + birthdate, + gender, + bio, + availabilities: parsedAvailabilities + }; + + let success = false; + if (data.user) { + success = await patchUserAPI(fetch, data.user.id, updateData); + } else { + throw new Error($t('header.tutor.userNotFound')); + } + console.log('Update success:', success); + if (success) { + alert($t('header.tutor.updatedSuccessfully')); + } else { + alert($t('header.tutor.updateError')); + } } catch (error) { - alert($t('errors.invalidAvailabilities')); - return; + console.error('Update failed:', error); + alert(error instanceof Error ? error.message : $t('errors.updateFailed')); } - - console.log({ - email, - nickname, - birthdate, - gender, - bio, - availabilities: parsedAvailabilities - }); - alert($t('profile.updatedSuccessfully')); } </script> @@ -132,10 +145,7 @@ ></textarea> </div> - <button - type="submit" - class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-md shadow-md focus:outline-none focus:ring focus:ring-indigo-200" - > + <button type="submit" class="button"> {$t('header.tutor.update')} </button> </form> -- GitLab From b93472698b42499d1f164d1edeb279a2f8714ed1 Mon Sep 17 00:00:00 2001 From: DavePk04 <Dave.Pikop.Pokam@ulb.be> Date: Sat, 29 Mar 2025 22:52:58 +0100 Subject: [PATCH 5/5] feat(profile): Enhance profile form with DaisyUI styling and fix data handling --- .../src/routes/tutor/profile/+page.svelte | 143 +++++++++--------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/frontend/src/routes/tutor/profile/+page.svelte b/frontend/src/routes/tutor/profile/+page.svelte index 5ba2a3d7..a1285fbe 100644 --- a/frontend/src/routes/tutor/profile/+page.svelte +++ b/frontend/src/routes/tutor/profile/+page.svelte @@ -8,14 +8,10 @@ const formatBirthdate = (dateStr: string | Date | undefined): string => { if (!dateStr) return ''; const date = new Date(dateStr); - if (!isNaN(date.getTime())) { - return date.toISOString().split('T')[0]; - } + if (!isNaN(date.getTime())) return date.toISOString().split('T')[0]; if (typeof dateStr === 'string') { const [day, month, year] = dateStr.split('/'); - if (year?.length === 4) { - return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; - } + if (year?.length === 4) return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; } return ''; }; @@ -32,7 +28,6 @@ async function updateProfile() { try { const parsedAvailabilities = availabilities ? JSON.parse(availabilities) : []; - console.log('birth:', new Date(birthdate).toISOString()); const updateData = { email, nickname, @@ -48,7 +43,6 @@ } else { throw new Error($t('header.tutor.userNotFound')); } - console.log('Update success:', success); if (success) { alert($t('header.tutor.updatedSuccessfully')); } else { @@ -61,91 +55,102 @@ } </script> -<h1 class="text-2xl font-bold text-center my-5">{$t('header.tutor.profile')}</h1> +<h1 class="text-3xl font-bold text-center mb-8 text-primary"> + {$t('header.tutor.profile')} +</h1> -<div class="max-w-lg mx-auto bg-white shadow-md rounded-lg p-6"> - <form on:submit|preventDefault={updateProfile}> - <div class="mb-4"> - <label for="email" class="block text-sm font-medium text-gray-700">{$t('home.email')}</label> - <input - type="email" - id="email" - bind:value={email} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - required - /> - </div> +<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto p-8"> + <form on:submit|preventDefault={updateProfile} class="space-y-4"> + <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div class="form-control"> + <label class="label" for="email"> + <span class="label-text">{$t('home.email')}</span> + </label> + <input + type="email" + id="email" + bind:value={email} + class="input input-bordered focus:input-primary" + required + /> + </div> - <div class="mb-4"> - <label for="nickname" class="block text-sm font-medium text-gray-700" - >{$t('home.nickname')}</label - > - <input - type="text" - id="nickname" - bind:value={nickname} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - required - /> - </div> + <div class="form-control"> + <label class="label" for="nickname"> + <span class="label-text">{$t('home.nickname')}</span> + </label> + <input + type="text" + id="nickname" + bind:value={nickname} + class="input input-bordered focus:input-primary" + required + /> + </div> - <div class="mb-4"> - <label for="birthdate" class="block text-sm font-medium text-gray-700" - >{$t('home.birthdate')}</label - > - <input - type="date" - id="birthdate" - bind:value={birthdate} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - required - /> - </div> + <div class="form-control"> + <label class="label" for="birthdate"> + <span class="label-text">{$t('home.birthdate')}</span> + </label> + <input + type="date" + id="birthdate" + bind:value={birthdate} + class="input input-bordered focus:input-primary" + required + /> + </div> - <div class="mb-4"> - <label for="gender" class="block text-sm font-medium text-gray-700" - >{$t('users.gender')}</label - > - <select - id="gender" - bind:value={gender} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - required - > - <option value="" disabled>{$t('header.tutor.selectGender')}</option> - <option value="male">{$t('users.genders.male')}</option> - <option value="female">{$t('users.genders.female')}</option> - <option value="other">{$t('users.genders.other')}</option> - </select> + <div class="form-control"> + <label class="label" for="gender"> + <span class="label-text">{$t('users.gender')}</span> + </label> + <select + id="gender" + bind:value={gender} + class="select select-bordered focus:select-primary" + required + > + <option value="" disabled>{$t('header.tutor.selectGender')}</option> + <option value="male">{$t('users.genders.male')}</option> + <option value="female">{$t('users.genders.female')}</option> + <option value="other">{$t('users.genders.other')}</option> + </select> + </div> </div> - <div class="mb-4"> - <label for="bio" class="block text-sm font-medium text-gray-700">{$t('register.bio')}</label> + <div class="form-control"> + <label class="label" for="bio"> + <span class="label-text">{$t('register.bio')}</span> + </label> <textarea id="bio" rows="4" bind:value={bio} placeholder={$t('header.tutor.bio')} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + class="textarea textarea-bordered focus:textarea-primary h-24" required ></textarea> </div> - <div class="mb-4"> - <label for="availabilities" class="block text-sm font-medium text-gray-700" - >{$t('register.availabilities')}</label - > + <div class="form-control"> + <label class="label" for="availabilities"> + <span class="label-text"> + {$t('register.availabilities')} + <span class="text-xs text-info ml-2">(JSON format)</span> + </span> + </label> <textarea id="availabilities" rows="4" bind:value={availabilities} placeholder={$t('header.tutor.availabilities')} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + class="textarea textarea-bordered focus:textarea-primary font-mono h-32" required ></textarea> </div> - <button type="submit" class="button"> + <button type="submit" class="btn btn-primary w-full mt-6 text-lg"> {$t('header.tutor.update')} </button> </form> -- GitLab