diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index d3f6b7a090b33a383d22e42ae50f6000cf3a1a7a..5816857a89f35af2a352be3c81ac779fa5bbeb17 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -8,6 +8,16 @@ "users": "Users", "studies": "Studies" }, + "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" + }, "availability": "Availability", "language": "Language", "register": "Sign up", @@ -49,7 +59,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 1cade0ccb2b5c789800e88ddf29d6b70a3c0ab38..9e6b44659da57b0bf18096a24b27b924df24f7bc 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -13,6 +13,16 @@ "users": "Utilisateurs", "sessions": "Sessions", "studies": "Études" + }, + "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", + "availabilities": "Disponibilités" } }, "chatbox": { @@ -49,7 +59,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 876faf2f741150f97c9aaa4e5e0bb02f82717092..97086bcdb59697cbccbeb44d626b7152a18ac21c 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,33 +71,64 @@ </ul> </details> </li> - {#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> - </ul> - </details> - </li> - {/if} - {:else} + {/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"> + <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 class="btn btn-sm my-auto" diff --git a/frontend/src/routes/tutor/profile/+page.svelte b/frontend/src/routes/tutor/profile/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a1285fbecd3f2707056e21dd17c9c96ddf0912fe --- /dev/null +++ b/frontend/src/routes/tutor/profile/+page.svelte @@ -0,0 +1,157 @@ +<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 | Date | undefined): string => { + if (!dateStr) return ''; + 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 ?? undefined); + let gender = data.user?.gender || ''; + let bio = data.user?.bio || ''; + let availabilities = data.user?.availabilities + ? JSON.stringify(data.user.availabilities, null, 2) + : ''; + + async function updateProfile() { + try { + const parsedAvailabilities = availabilities ? JSON.parse(availabilities) : []; + 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')); + } + if (success) { + alert($t('header.tutor.updatedSuccessfully')); + } else { + alert($t('header.tutor.updateError')); + } + } catch (error) { + console.error('Update failed:', error); + alert(error instanceof Error ? error.message : $t('errors.updateFailed')); + } + } +</script> + +<h1 class="text-3xl font-bold text-center mb-8 text-primary"> + {$t('header.tutor.profile')} +</h1> + +<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="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="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="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="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="textarea textarea-bordered focus:textarea-primary h-24" + required + ></textarea> + </div> + + <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="textarea textarea-bordered focus:textarea-primary font-mono h-32" + required + ></textarea> + </div> + + <button type="submit" class="btn btn-primary w-full mt-6 text-lg"> + {$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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391