diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json
index 92ddba90763cd13f837f83c43f9de39beb18f589..a029f519eb6a156d40f1557eb649d336bb6991c2 100644
--- a/frontend/src/lang/fr.json
+++ b/frontend/src/lang/fr.json
@@ -14,7 +14,8 @@
"sessions": "Sessions",
"studies": "Études",
"tasks": "Tâches"
- }
+ },
+ "profile": "Mon profil"
},
"chatbox": {
"placeholder": "Écrivez votre message ici...",
@@ -429,6 +430,10 @@
"startTask": "Commencer cette tâche",
"achieveTask": "Tâche achevée"
},
+ "profile": {
+ "title": "Mon profile",
+ "save": "Sauvegarder les modifications"
+ },
"button": {
"create": "Créer",
"submit": "Envoyer",
diff --git a/frontend/src/lib/types/user.ts b/frontend/src/lib/types/user.ts
index 906d98667918f45fd8a268b76446186c84d30baf..0acb93b5a9e65569a4c4e977b52b2a5cc074b9cc 100644
--- a/frontend/src/lib/types/user.ts
+++ b/frontend/src/lib/types/user.ts
@@ -28,7 +28,7 @@ export default class User {
private _ui_language: string | null;
private _home_language: string | null;
private _target_language: string | null;
- private _birthdate: Date | null;
+ private _birthdate: string | null;
private _gender: string | null;
private _bio: string | null;
private _calcom_link: string | null;
@@ -51,7 +51,7 @@ export default class User {
ui_language: string | null,
home_language: string | null,
target_language: string | null,
- birthdate: Date | null,
+ birthdate: string | null,
gender: string | null,
calcom_link: string | null,
studies_id: number[] | null,
@@ -130,10 +130,17 @@ export default class User {
return this._target_language;
}
- get birthdate(): Date | null {
+ get birthdate(): string | null {
return this._birthdate;
}
+ get birthdateAsDay(): string | null {
+ if (this._birthdate) {
+ return this._birthdate.slice(0, 10); // Format as YYYY-MM-DD
+ }
+ return null;
+ }
+
get gender(): string | null {
return this._gender;
}
diff --git a/frontend/src/routes/Header.svelte b/frontend/src/routes/Header.svelte
index dbb172773b765caaeaec429c1998c2ce5b2e6bfa..79f04b7c1005aa12df5040f350acfadd393f1304 100644
--- a/frontend/src/routes/Header.svelte
+++ b/frontend/src/routes/Header.svelte
@@ -50,7 +50,7 @@
</h1>
</div>
<div class="navbar-end hidden sm:flex">
- <ul class="menu menu-horizontal p-0 flex items-center">
+ <ul class="menu menu-horizontal p-0 flex items-center z-10">
{#if user}
<li>
<details>
@@ -63,6 +63,19 @@
{user.nickname}
</summary>
<ul class="menu menu-sm dropdown-content absolute right-0">
+ {#if user?.type < 2}
+ <li>
+ <a
+ class="btn btn-sm my-auto"
+ data-sveltekit-reload
+ href="/tutor/profile?redirect={encodeURIComponent(
+ $page.url.pathname + $page.url.search
+ )}"
+ >
+ {$t('header.profile')}
+ </a>
+ </li>
+ {/if}
<li>
<a data-sveltekit-reload href="/logout" class="whitespace-nowrap">
{$t('header.signout')}
diff --git a/frontend/src/routes/tutor/+layout.server.ts b/frontend/src/routes/tutor/+layout.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ddf2023916330cb19354b8333ee001f0a6f1c306
--- /dev/null
+++ b/frontend/src/routes/tutor/+layout.server.ts
@@ -0,0 +1,11 @@
+import { type ServerLoad, error, redirect } from '@sveltejs/kit';
+
+export const load: ServerLoad = async ({ locals }) => {
+ if (locals.user == null || locals.user == undefined) {
+ redirect(303, '/login');
+ }
+
+ if (locals.user == null || locals.user == undefined || locals.user.type > 1) {
+ error(403, 'Forbidden');
+ }
+};
diff --git a/frontend/src/routes/tutor/profile/+page.server.ts b/frontend/src/routes/tutor/profile/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..821322e9427656ab239af47a78d5644d0ef72a8a
--- /dev/null
+++ b/frontend/src/routes/tutor/profile/+page.server.ts
@@ -0,0 +1,57 @@
+import { fail, redirect, type Actions } from '@sveltejs/kit';
+import { patchUserAPI } from '$lib/api/users';
+import { safeRedirectAuto } from '$lib/utils/security';
+
+export const actions: Actions = {
+ default: async ({ request, fetch, locals, url }) => {
+ if (!locals.user) {
+ return fail(401, { message: 'Unauthorized' });
+ }
+ const formData = await request.formData();
+ const nickname = formData.get('nickname') as string;
+ const email = formData.get('email') as string;
+ const gender = formData.get('gender') as string;
+ const birthdate = formData.get('birthdate') as string;
+ const bio = formData.get('bio') as string;
+ const availabilitiesRaw = formData.getAll('availability[]') as string[];
+
+ let availabilities: { day: string; start: string; end: string }[] = [];
+ for (const availability of availabilitiesRaw) {
+ const [day, start, end] = availability.split('-', 3);
+
+ if (!day || !start || !end) continue;
+
+ availabilities.push({
+ day: day.trim(),
+ start: start.trim(),
+ end: end.trim()
+ });
+ }
+
+ console.log('Updating profile with data:', {
+ nickname,
+ email,
+ gender,
+ birthdate,
+ bio,
+ availabilities
+ });
+
+ const data: any = {
+ nickname,
+ email,
+ gender,
+ birthdate,
+ bio,
+ availabilities
+ };
+
+ const ok = await patchUserAPI(fetch, locals.user.id, data);
+
+ if (!ok) {
+ return fail(400, { message: 'Failed to update profile.' });
+ }
+
+ return safeRedirectAuto(url);
+ }
+};
diff --git a/frontend/src/routes/tutor/profile/+page.svelte b/frontend/src/routes/tutor/profile/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..f586cba06724f91893319c5c5a7f7c74850ab468
--- /dev/null
+++ b/frontend/src/routes/tutor/profile/+page.svelte
@@ -0,0 +1,193 @@
+<script lang="ts">
+ import { t } from '$lib/services/i18n';
+ import type { PageData } from './$types';
+ import autosize from 'svelte-autosize';
+
+ let { data, form }: { data: PageData; form: FormData } = $props();
+ let user = $state(data.user);
+
+ let isLoading = false;
+ let message = $state(form?.message || '');
+
+ const MAX_BIO_LENGTH = 100;
+
+ let bio = $state(user?.bio || '');
+
+ let remainingCharacters = $derived(MAX_BIO_LENGTH - bio.length);
+
+ type Availability = {
+ day: string;
+ start: string;
+ end: string;
+ };
+
+ const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
+
+ let availabilities: Availability[] = $state(
+ user?.availabilities ? [...user?.availabilities] : []
+ );
+ let selectedWeekday = $state('');
+ let selectedTimeStart = $state('');
+ let selectedTimeEnd = $state('');
+
+ function addAvailability() {
+ if (!selectedWeekday || !selectedTimeStart || !selectedTimeEnd) {
+ message = 'All fields must be selected.';
+ return;
+ }
+ const startHour = parseInt(selectedTimeStart.split(':')[0]);
+ const endHour = parseInt(selectedTimeEnd.split(':')[0]);
+ if (startHour >= endHour) {
+ message = 'End time must be later than start time.';
+ return;
+ }
+ availabilities = [
+ ...availabilities,
+ { day: selectedWeekday, start: selectedTimeStart, end: selectedTimeEnd }
+ ];
+ selectedWeekday = '';
+ selectedTimeStart = '';
+ selectedTimeEnd = '';
+ message = '';
+ }
+
+ function removeAvailability(index: number) {
+ availabilities = availabilities.filter((_, i) => i !== index);
+ }
+</script>
+
+<div class="w-full max-w-4xl mx-auto p-6 bg-white rounded-lg shadow mt-8">
+ <h1 class="text-2xl font-bold mb-6">{$t('profile.title')}</h1>
+ {#if message}
+ <div class="alert alert-error mb-4">{message}</div>
+ {/if}
+ <form class="space-y-6" method="POST">
+ <div>
+ <label class="block text-sm font-medium mb-1" for="nickname">
+ {$t('register.nickname')}
+ </label>
+ <input
+ id="nickname"
+ name="nickname"
+ type="text"
+ class="input input-bordered w-full"
+ value={user?.nickname}
+ required
+ />
+ </div>
+ <div>
+ <label class="block text-sm font-medium mb-1" for="email">
+ {$t('register.email')}
+ </label>
+ <input
+ id="email"
+ name="email"
+ type="email"
+ class="input input-bordered w-full"
+ value={user?.email}
+ required
+ />
+ </div>
+ <div>
+ <label class="block text-sm font-medium mb-1" for="gender">
+ {$t('register.gender')}
+ </label>
+ <select
+ id="gender"
+ name="gender"
+ class="select select-bordered w-full"
+ value={user?.gender}
+ required
+ >
+ <option value="" disabled>{$t('register.gender')}</option>
+ <option value="male">{$t('register.genders.male')}</option>
+ <option value="female">{$t('register.genders.female')}</option>
+ <option value="other">{$t('register.genders.other')}</option>
+ <option value="na">{$t('register.genders.na')}</option>
+ </select>
+ </div>
+ <div>
+ <label class="block text-sm font-medium mb-1" for="birthdate">
+ {$t('register.birthyear')}
+ </label>
+ <input
+ id="birthdate"
+ name="birthdate"
+ type="date"
+ class="input input-bordered w-full"
+ value={user?.birthdateAsDay}
+ />
+ required />
+ </div>
+ <div>
+ <label class="block text-sm font-medium mb-1" for="bio">
+ {$t('register.bio')}
+ <span class="text-xs text-gray-400 ml-2">{remainingCharacters} / {MAX_BIO_LENGTH}</span>
+ </label>
+ <textarea
+ use:autosize
+ id="bio"
+ name="bio"
+ class="textarea textarea-bordered w-full"
+ bind:value={bio}
+ maxlength={MAX_BIO_LENGTH}
+ required
+ ></textarea>
+ </div>
+ <div>
+ <h2 class="text-lg font-semibold">
+ {$t('register.availabilities')}
+ </h2>
+ <div class="flex flex-row gap-2 mb-2 w-full">
+ <select class="select select-bordered flex-grow" bind:value={selectedWeekday}>
+ <option value="" disabled>{$t('register.weekday')}</option>
+ {#each days as dayKey}
+ <option value={dayKey}>{$t(`utils.days.${dayKey}`)}</option>
+ {/each}
+ </select>
+ <select class="select select-bordered flex-grow" bind:value={selectedTimeStart}>
+ <option value="" disabled>{$t('register.startTime')}</option>
+ {#each Array.from({ length: 24 }, (_, i) => `${i}:00`) as time}
+ <option value={time}>{time}</option>
+ {/each}
+ </select>
+ <select class="select select-bordered flex-grow" bind:value={selectedTimeEnd}>
+ <option value="" disabled>{$t('register.endTime')}</option>
+ {#each Array.from({ length: 24 }, (_, i) => `${i}:00`) as time}
+ <option value={time}>{time}</option>
+ {/each}
+ </select>
+ <button type="button" class="btn btn-primary flex-grow" onclick={addAvailability}>
+ {$t('register.addAvailability')}
+ </button>
+ </div>
+ {#if availabilities.length > 0}
+ <ul class="list-disc pl-6 py-2">
+ {#each availabilities as { day, start, end }, index}
+ <li class="flex justify-between items-center">
+ <input type="hidden" name="availability[]" value={`${day}-${start}-${end}`} />
+
+ <span>{$t(`utils.days.${day}`)}: {start} - {end}</span>
+ <button
+ type="button"
+ class="text-red-500 ml-4"
+ onclick={() => removeAvailability(index)}
+ >
+ {$t('register.remove')}
+ </button>
+ </li>
+ {/each}
+ </ul>
+ {:else}
+ <p class="text-gray-500 text-sm py-2">
+ {$t('register.noAvailabilities')}
+ </p>
+ {/if}
+ </div>
+ <div>
+ <button type="submit" class="btn btn-primary w-full mt-4" disabled={isLoading}>
+ {isLoading ? $t('profile.saving') : $t('profile.save')}
+ </button>
+ </div>
+ </form>
+</div>