From f10fdc4df656b2f5ef44c95c29d8f75c051ea01d Mon Sep 17 00:00:00 2001
From: Brieuc Dubois <git@bhasher.com>
Date: Sun, 8 Sep 2024 17:07:02 +0300
Subject: [PATCH] Implement tutor registration #65

---
 backend/app/main.py                           |   3 +-
 frontend/src/lang/fr.json                     |  29 +-
 frontend/src/lib/api/auth.ts                  |   6 +-
 frontend/src/routes/+layout.server.ts         |   2 +-
 frontend/src/routes/tutor/+layout.server.ts   |   5 +-
 .../src/routes/tutor/register/+page.svelte    | 506 ++++++++++++++++++
 .../src/routes/tutor/timeslots/+page.svelte   |  13 +-
 7 files changed, 557 insertions(+), 7 deletions(-)
 create mode 100644 frontend/src/routes/tutor/register/+page.svelte

diff --git a/backend/app/main.py b/backend/app/main.py
index a474e0ee..86f14fa7 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -110,13 +110,14 @@ def register(
     email: Annotated[str, Form()],
     password: Annotated[str, Form()],
     nickname: Annotated[str, Form()],
+    tutor: Annotated[bool, Form()],
     db: Session = Depends(get_db),
 ):
     db_user = crud.get_user_by_email(db, email=email)
     if db_user:
         raise HTTPException(status_code=400, detail="User already registered")
 
-    user_data = schemas.UserCreate(email=email, password=password, nickname=nickname)
+    user_data = schemas.UserCreate(email=email, password=password, nickname=nickname, type=models.UserType.TUTOR.value if tutor else models.UserType.STUDENT.value)
 
     user = crud.create_user(db=db, user=user_data)
 
diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json
index 8bffa154..682189b6 100644
--- a/frontend/src/lang/fr.json
+++ b/frontend/src/lang/fr.json
@@ -74,7 +74,8 @@
 			"test": "Tests",
 			"timeslots": "Tuteur",
 			"start": "Commencer",
-			"continue": "Continuer"
+			"continue": "Continuer",
+			"availabilities": "Disponibilitées"
 		},
 		"error": {
 			"emptyFields": "Veuillez remplir tous les champs",
@@ -111,6 +112,32 @@
 			},
 			"ok": "J'accepte de participer à l'étude telle que décrite ci-dessus."
 		},
+		"consentTutor": {
+			"title": "Document d’information et consentement éclairé",
+			"intro": "Vous êtes invité·e à participer à une étude scientifique. L'objectif de cette étude est de comprendre comment les tuteurs et les apprenants de langue étrangère interagissent lors de sessions de tutorat en ligne. Les données collectées seront utilisées pour améliorer les outils de tutorat en ligne et pour mieux comprendre les processus cognitifs de part et d'autre.",
+			"participation": "Qu'implique votre participation ?",
+			"participationD": "Si vous acceptez de participer, vous serez invité·e à participer à des sessions de tutorat en ligne avec un tuteur de langue étrangère. Vous serez également invité à remplir des questionnaires avant et après les sessions de tutorat. Les sessions de tutorat seront enregistrées pour analyse ultérieure.</p><p>Nous vous demandons de prévoir de réaliser un minimum de <strong>8 sessions d'une heure</strong> de tutorat (donc 8 heures au total), au cours d'une période de 1 à 3 mois. Vous pouvez bien sûr en réaliser plus si vous le souhaitez. Vous pouvez cependant arrêter de participer à l'étude à tout moment.",
+			"privacy": "Comment seront traitées et conservées vos données ?",
+			"privacyD": "Les données collectées (par exemple, les transcriptions des conversations, les résultats de tests, les mesures de frappe, les informations sur les participants comme l'age ou le genre) seront traitées de manière confidentielle et anonyme. Elles seront conservées après leur anonymisation intégrale et ne pourront être utilisées qu'à des fins scientifiques ou pédagogiques. Elles pourront éventuellement être partagées avec d'autres chercheurs ou enseignants, mais toujours dans ce cadre strictement de recherche ou d'enseignement.",
+			"rights": "Quels sont vos droits ? Participation volontaire et retrait éventuel",
+			"rightsD": "Votre participation à cette étude est volontaire. Vous pouvez à tout moment décider de ne plus participer à l'étude sans avoir à vous justifier. Vous pouvez également demander à ce que vos données soient supprimées à tout moment. Si vous avez des questions ou des préoccupations concernant cette étude, vous pouvez contacter le responsable de l'étude, Serge Bibauw, à l'adresse suivante :",
+			"studyData": {
+				"title": "Informations sur l'étude",
+				"study": "Titre de l'étude",
+				"studyD": "Étude longitudinale du tutorat en langue étrangère",
+				"project": "Projet de recherche",
+				"projectD": "AI Tutoring in Language Learning (FNRS CDR 2024-2026)",
+				"person": "Responsable de l'étude",
+				"personD": "Serge Bibauw",
+				"university": "Université",
+				"universityD": "Université catholique de Louvain (UCLouvain)",
+				"address": "Adresse",
+				"addressD": "Place Cardinal Mercier 14, 1348 Louvain-la-Neuve",
+				"email": "E-mail",
+				"emailD": "serge.bibauw@uclouvain.be"
+			},
+			"ok": "J'accepte de participer à l'étude telle que décrite ci-dessus."
+		},
 		"welcome": "Bienvenue sur LanguageLab ! Avant de commencer, veuillez remplir les informations suivantes. Cela nous permettra de mieux vous connaitre et d'adapter l'expérience à vos besoins.",
 		"homeLanguage": "Langue première",
 		"homeLanguage.note": "Langue maternelle ou langue principale du foyer",
diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts
index 4a59d59c..5d3ba02d 100644
--- a/frontend/src/lib/api/auth.ts
+++ b/frontend/src/lib/api/auth.ts
@@ -34,7 +34,8 @@ export async function loginAPI(email: string, password: string): Promise<string>
 export async function registerAPI(
 	email: string,
 	password: string,
-	nickname: string
+	nickname: string,
+	tutor: boolean = false
 ): Promise<string> {
 	return axiosPublicInstance
 		.post(
@@ -43,7 +44,8 @@ export async function registerAPI(
 				email,
 				username: email,
 				password,
-				nickname
+				nickname,
+				tutor
 			},
 			{
 				headers: {
diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts
index 0c0a1405..a621bd9c 100644
--- a/frontend/src/routes/+layout.server.ts
+++ b/frontend/src/routes/+layout.server.ts
@@ -1,6 +1,6 @@
 import { type ServerLoad, redirect } from '@sveltejs/kit';
 
-const publicly_allowed = ['/login', '/register', '/tests', '/surveys'];
+const publicly_allowed = ['/login', '/register', '/tests', '/surveys', '/tutor/register'];
 
 const isPublic = (path: string) => {
 	for (const allowed of publicly_allowed) {
diff --git a/frontend/src/routes/tutor/+layout.server.ts b/frontend/src/routes/tutor/+layout.server.ts
index b5386de7..a03ab77e 100644
--- a/frontend/src/routes/tutor/+layout.server.ts
+++ b/frontend/src/routes/tutor/+layout.server.ts
@@ -1,7 +1,10 @@
 import { type ServerLoad, error, redirect } from '@sveltejs/kit';
 
-export const load: ServerLoad = async ({ locals }) => {
+export const load: ServerLoad = async ({ locals, url }) => {
 	if (locals.user == null || locals.user == undefined) {
+		if (url.pathname.startsWith('/tutor/register')) {
+			return {};
+		}
 		redirect(307, '/login');
 	}
 
diff --git a/frontend/src/routes/tutor/register/+page.svelte b/frontend/src/routes/tutor/register/+page.svelte
new file mode 100644
index 00000000..1f841a11
--- /dev/null
+++ b/frontend/src/routes/tutor/register/+page.svelte
@@ -0,0 +1,506 @@
+<script lang="ts">
+	import { loginAPI, registerAPI } from '$lib/api/auth';
+	import config from '$lib/config';
+	import { locale, t } from '$lib/services/i18n';
+	import { user } from '$lib/types/user';
+	import { toastAlert, toastWarning } from '$lib/utils/toasts';
+	import { onMount } from 'svelte';
+	import { get } from 'svelte/store';
+	import Timeslots from '$lib/components/users/timeslots.svelte';
+	import User, { users } from '$lib/types/user';
+	import {
+		getUsersAPI,
+		patchUserAPI,
+		createUserContactAPI,
+		getUserContactsAPI
+	} from '$lib/api/users';
+	import { Icon, Envelope, Key, UserCircle, Calendar, QuestionMarkCircle } from 'svelte-hero-icons';
+	import Typingtest from '$lib/components/tests/typingtest.svelte';
+
+	let current_step = 0;
+
+	$: message = '';
+
+	onMount(async () => {
+		const u = get(user);
+
+		if (u == null) {
+			current_step = 1;
+			return;
+		}
+		User.parseAll(await getUsersAPI());
+
+		if (!u.home_language || !u.target_language || !u.birthdate || !u.gender) {
+			current_step = 3;
+			return;
+		}
+
+		const contacts = User.parseAll(await getUserContactsAPI(u.id));
+
+		if (contacts.length == 0) {
+			current_step = 4;
+			return;
+		}
+
+		current_step = 5;
+	});
+
+	let nickname = '';
+	let email = '';
+	let password = '';
+	let confirmPassword = '';
+
+	let ui_language: string = $locale;
+	let home_language: string;
+	let birthdate: string;
+	let gender: string;
+	let calcom_link = '';
+
+	let timeslots = 0n;
+	$: filteredUsers = $users.filter((user) => {
+		if (user.availability === 0n) return false;
+		if (timeslots === 0n) return true;
+
+		return user.availability & timeslots;
+	});
+
+	async function onRegister() {
+		if (nickname == '' || email == '' || password == '' || confirmPassword == '') {
+			message = $t('register.error.emptyFields');
+			return;
+		}
+		if (password.length < 8) {
+			message = $t('register.error.passwordRules');
+			return;
+		}
+		if (password != confirmPassword) {
+			message = $t('register.error.differentPasswords');
+			return;
+		}
+		const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
+		if (!emailRegex.test(email)) {
+			message = $t('register.error.emailRules');
+			return;
+		}
+		message = '';
+
+		const registerRes = await registerAPI(email, password, nickname, true);
+
+		if (registerRes !== 'OK') {
+			message = registerRes;
+			return;
+		}
+
+		const loginRes = await loginAPI(email, password);
+
+		if (loginRes !== 'OK') {
+			toastAlert('Failed to login: ' + loginRes);
+			document.location.href = '/login';
+			return;
+		}
+
+		document.location.href = '/tutor/register';
+
+		message = 'OK';
+	}
+
+	async function onData() {
+		const user_id = get(user)?.id;
+
+		if (!user_id) {
+			toastAlert('Failed to get current user ID');
+			return;
+		}
+
+		if (!ui_language || !home_language || !birthdate || !gender) {
+			message = $t('register.error.emptyFields');
+			return;
+		}
+
+		const res = await patchUserAPI(user_id, {
+			ui_language,
+			home_language,
+			birthdate,
+			gender
+		});
+
+		if (!res) {
+			message = $t('register.error.metadata');
+			return;
+		}
+
+		current_step++;
+	}
+
+	async function onAvailabilities() {
+		if (!calcom_link || !calcom_link.startsWith('https://cal.com/')) {
+			toastWarning($t('timeslots.calcomWarning'));
+			return;
+		}
+
+		const res = $user?.setAvailability(timeslots, calcom_link);
+
+		if (!res) return;
+
+		current_step++;
+	}
+
+	async function onTyping() {
+		current_step++;
+	}
+</script>
+
+<div class="header mx-auto my-5">
+	<ul class="steps text-xs">
+		<li class="step" class:step-primary={current_step >= 1}>
+			{$t('register.tab.consent')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 2}>
+			{$t('register.tab.signup')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 3}>
+			{$t('register.tab.information')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 4}>
+			{$t('register.tab.availabilities')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 5} data-content="?">
+			{$t('register.tab.continue')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 6} data-content="">
+			{$t('register.tab.test')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 7} data-content="★">
+			{$t('register.tab.start')}
+		</li>
+	</ul>
+</div>
+
+<div class="max-w-screen-md mx-auto p-5">
+	{#if message}
+		<div class="alert alert-error text-content text-base-100 py-2 mb-4">
+			{message}
+		</div>
+	{/if}
+	{#if current_step == 1}
+		<div class="join join-vertical w-full">
+			<div class="join-item">
+				<h2 class="text-xl font-bold text-center">{$t('register.consentTutor.title')}</h2>
+				<p class="m-5">{@html $t('register.consentTutor.intro')}</p>
+			</div>
+			<div class="collapse collapse-arrow join-item border border-base-300">
+				<input type="radio" name="consent-accordion" checked="checked" />
+				<div class="collapse-title font-medium">{$t('register.consentTutor.participation')}</div>
+				<div class="collapse-content">
+					<p>{@html $t('register.consentTutor.participationD')}</p>
+				</div>
+			</div>
+			<div class="collapse collapse-arrow join-item border border-base-300">
+				<input type="radio" name="consent-accordion" />
+				<div class="collapse-title font-medium">{$t('register.consentTutor.privacy')}</div>
+				<div class="collapse-content"><p>{@html $t('register.consentTutor.privacyD')}</p></div>
+			</div>
+			<div class="collapse collapse-arrow join-item border border-base-300">
+				<input type="radio" name="consent-accordion" />
+				<div class="collapse-title font-medium">{$t('register.consentTutor.rights')}</div>
+				<div class="collapse-content">
+					<p>
+						{$t('register.consentTutor.rightsD')}
+						<a
+							class="link link-primary"
+							href="mailto:{$t('register.consentTutor.studyData.emailD')}"
+							>{$t('register.consentTutor.studyData.emailD')}</a
+						>.
+					</p>
+				</div>
+			</div>
+			<div class="collapse collapse-arrow join-item border border-base-300">
+				<input type="radio" name="consent-accordion" />
+				<div class="collapse-title font-medium">{$t('register.consentTutor.studyData.title')}</div>
+				<div class="collapse-content">
+					<dl class="text-sm">
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.study')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								{$t('register.consentTutor.studyData.studyD')}
+							</dd>
+						</div>
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.project')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								{$t('register.consentTutor.studyData.projectD')}
+							</dd>
+						</div>
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.university')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								{$t('register.consentTutor.studyData.universityD')}
+							</dd>
+						</div>
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.address')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								{$t('register.consentTutor.studyData.addressD')}
+							</dd>
+						</div>
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.person')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								{$t('register.consentTutor.studyData.personD')}
+							</dd>
+						</div>
+						<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+							<dt class="font-medium">{$t('register.consentTutor.studyData.email')}</dt>
+							<dd class="text-gray-700 sm:col-span-2">
+								<a href="mailto:{$t('register.consentTutor.studyData.emailD')}" class="link"
+									>{$t('register.consentTutor.studyData.emailD')}</a
+								>
+							</dd>
+						</div>
+					</dl>
+				</div>
+			</div>
+		</div>
+		<div class="form-control">
+			<button class="button mt-4" on:click={() => current_step++}>
+				{$t('register.consentTutor.ok')}
+			</button>
+		</div>
+	{:else if current_step == 2}
+		<div class="space-y-2">
+			<label for="email" class="form-control">
+				<div class="label">
+					<span class="label-text">{$t('register.email')}</span>
+					<span class="label-text-alt">{$t('register.email.note')}</span>
+				</div>
+				<div class="input flex items-center">
+					<Icon src={Envelope} class="w-4 mr-2 opacity-70" solid />
+					<input
+						type="text"
+						class="grow"
+						bind:value={email}
+						placeholder={$t('register.email.ph')}
+					/>
+				</div>
+			</label>
+			<label for="nickname" class="form-control">
+				<div class="label">
+					<span class="label-text">{$t('register.nickname')}</span>
+					<span class="label-text-alt">{$t('register.nickname.note')}</span>
+				</div>
+				<div class="input flex items-center">
+					<Icon src={UserCircle} class="w-4 mr-2 opacity-70" solid />
+					<input
+						type="text"
+						class="grow"
+						bind:value={nickname}
+						placeholder={$t('register.nickname.ph')}
+					/>
+				</div>
+			</label>
+			<label for="password" class="form-control">
+				<div class="label">
+					<span class="label-text">{$t('register.password')}</span>
+					<span class="label-text-alt">{$t('register.password.note')}</span>
+				</div>
+				<div class="input flex items-center">
+					<Icon src={Key} class="w-4 mr-2 opacity-70" solid />
+					<input
+						type="password"
+						class="grow"
+						bind:value={password}
+						placeholder={$t('register.password')}
+					/>
+				</div>
+			</label>
+			<label for="confirmPassword" class="form-control">
+				<div class="input flex items-center">
+					<Icon src={Key} class="w-4 mr-2 opacity-70" solid />
+					<input
+						type="password"
+						class="grow"
+						bind:value={confirmPassword}
+						placeholder={$t('register.confirmPassword')}
+					/>
+				</div>
+			</label>
+			<div class="form-control">
+				<button class="button mt-2" on:click={onRegister}>{$t('register.signup')}</button>
+			</div>
+		</div>
+	{:else if current_step == 3}
+		<div class="space-y-2">
+			<div class="p-5 text-sm text-prose">
+				{@html $t('register.welcome')}
+			</div>
+			<div class="form-control">
+				<label for="homeLanguage" class="label">
+					<span class="label-text">{$t('register.homeLanguage')}</span>
+					<span class="label-text-alt">{$t('register.homeLanguage.note')}</span>
+				</label>
+				<select
+					class="select select-bordered"
+					id="homeLanguage"
+					name="homeLanguage"
+					required
+					bind:value={home_language}
+				>
+					<option disabled selected value="">{$t('register.homeLanguage')}</option>
+					{#each Object.entries(config.PRIMARY_LANGUAGE) as [code, name]}
+						<option value={code}>{name}</option>
+					{/each}
+				</select>
+			</div>
+			<div class="form-control">
+				<label for="birthyear" class="label">
+					<span class="label-text">{$t('register.birthyear')}</span>
+				</label>
+				<select
+					class="select select-bordered"
+					id="birthyear"
+					name="birthyear"
+					required
+					bind:value={birthdate}
+				>
+					<option disabled selected value="">{$t('register.birthyear')}</option>
+					{#each Array.from({ length: 82 }, (_, i) => i + 1931).reverse() as year}
+						<option value={year}>{year}</option>
+					{/each}
+				</select>
+			</div>
+			<div class="form-control space-y-1">
+				<label for="gender" class="label">
+					<span class="label-text">{$t('register.gender')}</span>
+					<span class="label-text-alt">{$t('register.gender.note')}</span>
+				</label>
+				<div class="label justify-normal gap-2 py-0">
+					<input
+						type="radio"
+						class="radio"
+						id="male"
+						name="gender"
+						value="male"
+						on:change={() => (gender = 'male')}
+					/>
+					<label for="male" class="label-text cursor-pointer">
+						{$t('register.genders.male')}
+					</label>
+				</div>
+				<div class="label justify-normal gap-2 py-0">
+					<input
+						type="radio"
+						class="radio"
+						id="female"
+						name="gender"
+						value="female"
+						on:change={() => (gender = 'female')}
+					/>
+					<label for="female" class="label-text cursor-pointer">
+						{$t('register.genders.female')}
+					</label>
+				</div>
+				<div class="label justify-normal gap-2 py-0">
+					<input
+						type="radio"
+						class="radio"
+						id="other"
+						name="gender"
+						value="other"
+						on:change={() => (gender = 'other')}
+					/>
+					<label for="other" class="label-text cursor-pointer">
+						{$t('register.genders.other')}
+					</label>
+				</div>
+				<div class="label justify-normal gap-2 py-0">
+					<input
+						type="radio"
+						class="radio"
+						id="na"
+						name="gender"
+						value="na"
+						on:change={() => (gender = 'na')}
+					/>
+					<label for="na" class="label-text cursor-pointer">
+						{$t('register.genders.na')}
+					</label>
+				</div>
+			</div>
+			<div class="form-control">
+				<button class="button mt-4" on:click={onData}>{$t('button.submit')}</button>
+			</div>
+		</div>
+	{:else if current_step == 4}
+		<!--{#if get(user)}-->
+		<h2 class="my-4 text-xl">{$t('timeslots.setAvailabilities')}</h2>
+		<Timeslots bind:timeslots />
+		<div class="form-control mt-4">
+			<label class="label" for="calcom">
+				<span class="label-text">
+					{$t('timeslots.calcom')}
+					<a
+						href="https://forge.uclouvain.be/sbibauw/languagelab/-/blob/93897d67f63ec81ebbe13b10035e4cd5a3a09071/docs/cal.com.md"
+						target="_blank"
+					>
+						<Icon
+							src={QuestionMarkCircle}
+							class="w-5 h-5 cursor-pointer inline"
+							title="Documentation"
+							solid
+						/>
+					</a>
+				</span>
+			</label>
+			<div class="input flex items-center">
+				<Icon src={Calendar} class="w-5 h-5 mr-2 opacity-70" solid />
+				<input
+					type="text"
+					id="calcom"
+					class="grow"
+					placeholder="https://cal.com/username/tutoring"
+					bind:value={calcom_link}
+				/>
+			</div>
+		</div>
+		<div class="form-control">
+			<button class="button mt-4" on:click={onAvailabilities}>{$t('button.submit')}</button>
+		</div>
+	{:else if current_step == 5}
+		<div class="text-center">
+			<p class="text-center">
+				{@html $t('register.continue')}
+			</p>
+			<button class="button mt-4 w-full" on:click={() => (current_step = 6)}>
+				{$t('register.continueButton')}
+			</button>
+			<button class="button mt-4 w-full" on:click={() => (document.location.href = '/')}>
+				{$t('register.startFastButton')}
+			</button>
+		</div>
+	{:else if current_step == 6}
+		<Typingtest onFinish={onTyping} />
+	{:else if current_step == 7}
+		<div class="text-center">
+			<p class="text-center">
+				{@html $t('register.start')}
+			</p>
+			<button class="button mt-4 m-auto" on:click={() => (document.location.href = '/')}>
+				{$t('register.startButton')}
+			</button>
+		</div>
+	{/if}
+</div>
+
+<style lang="postcss">
+	/* input:not([type='radio']) {
+		@apply w-full;
+	} */
+
+	.label-text-alt {
+		@apply opacity-50 ml-8;
+	}
+
+	.steps {
+		@apply text-base-300;
+	}
+</style>
diff --git a/frontend/src/routes/tutor/timeslots/+page.svelte b/frontend/src/routes/tutor/timeslots/+page.svelte
index 22bf6cda..0b0f3d2b 100644
--- a/frontend/src/routes/tutor/timeslots/+page.svelte
+++ b/frontend/src/routes/tutor/timeslots/+page.svelte
@@ -4,7 +4,7 @@
 	import Timeslots from '$lib/components/users/timeslots.svelte';
 	import { user } from '$lib/types/user';
 	import { toastWarning } from '$lib/utils/toasts';
-	import { Icon, Calendar } from 'svelte-hero-icons';
+	import { Icon, Calendar, QuestionMarkCircle } from 'svelte-hero-icons';
 
 	$: lastTimeslots = 0n;
 	$: timeslots = 0n;
@@ -48,6 +48,17 @@
 			<label class="label" for="calcom">
 				<span class="label-text">
 					{$t('timeslots.calcom')}
+					<a
+						href="https://forge.uclouvain.be/sbibauw/languagelab/-/blob/93897d67f63ec81ebbe13b10035e4cd5a3a09071/docs/cal.com.md"
+						target="_blank"
+					>
+						<Icon
+							src={QuestionMarkCircle}
+							class="w-5 h-5 cursor-pointer inline"
+							title="Documentation"
+							solid
+						/>
+					</a>
 				</span>
 			</label>
 			<div class="input flex items-center">
-- 
GitLab