diff --git a/backend/app/crud.py b/backend/app/crud.py index 3736f8043f9802fb4569c52526237137853cfc2b..a750cb73cfd54ef68d88be33e8a839634d3480b9 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -77,12 +77,20 @@ def create_user_survey_weekly(db: Session, user_id: int, survey: schemas.SurveyC def get_contact_sessions(db: Session, user_id: int, contact_id: int): - return ( + sessions = ( db.query(models.Session) .filter(models.Session.users.any(models.User.id == user_id)) .filter(models.Session.users.any(models.User.id == contact_id)) .all() ) + for session in sessions: + session.length = ( + db.query(models.Message) + .filter(models.Message.session_id == session.id) + .count() + ) + + return sessions def create_session(db: Session, user: schemas.User): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 85e434b8ea32a3a4449243cecec13c93e086bde2..0fe6f39df8bdd4a91ed38b8bafd7e1de92fc0ff7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.1", "dependencies": { + "@sveltekit-i18n/parser-icu": "^1.0.8", "dayjs": "^1.11.13", "emoji-picker-element": "^1.23.0", "linkify-html": "^4.1.3", @@ -655,7 +656,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/intl-localematcher": "0.5.4", @@ -666,7 +666,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -676,7 +675,6 @@ "version": "2.7.8", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz", "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", @@ -688,7 +686,6 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz", "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", @@ -699,7 +696,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -1439,6 +1435,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@sveltekit-i18n/parser-icu": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltekit-i18n/parser-icu/-/parser-icu-1.0.8.tgz", + "integrity": "sha512-/LnvE1EJv+higIxB5cWIV+9neiOe+CfC7VKhpv9mnU35NcZO3yOhEZ8y6F8nHHkMYIABLcqr15yk4hSvmRGWDw==", + "license": "MIT", + "dependencies": { + "intl-messageformat": "^10.1.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -3326,7 +3331,6 @@ "version": "10.5.14", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz", "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", @@ -5327,7 +5331,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type": { diff --git a/frontend/package.json b/frontend/package.json index 786f5fa7ccd23255df9f5bd304874c900772a113..6de874ce125fb5773d34583104489f0bfd83b41e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ }, "type": "module", "dependencies": { + "@sveltekit-i18n/parser-icu": "^1.0.8", "dayjs": "^1.11.13", "emoji-picker-element": "^1.23.0", "linkify-html": "^4.1.3", diff --git a/frontend/src/app.css b/frontend/src/app.css index fac86420b4cd42400cfc5626927624898acf5f2f..124f156456817ec77b5827a6d553136d341070f8 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -26,3 +26,12 @@ .input { @apply border-2 border-gray-300 rounded-md p-2; } + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index c2bd5c0e34b94fce939fcb4e07e0148f31fb9fc9..fbd331035fb46e5e4ef341d22b73195309f4b496 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -22,10 +22,7 @@ "history": "Historique" }, "home": { - "date": "Date", - "participants": "Participants", "email": "E-mail", - "actions": "Actions", "add": "Ajouter", "deleteSessionConfirm": "Êtes-vous sûr de vouloir supprimer cette session ? Cette action est irréversible.", "createSession": "Session immédiate", @@ -42,7 +39,9 @@ "pastSessions": "Sessions terminées", "newContact": "Ajouter un contact", "bookingSuccessful": "Session réservée avec succès", - "bookingFailed": "Erreur lors de la réservation de la session" + "bookingFailed": "Erreur lors de la réservation de la session", + "noCurrentOrFutureSessions": "Aucune session en cours ou planifiée", + "noSessions": "Aucune session" }, "login": { "email": "E-mail", @@ -225,10 +224,10 @@ "title": "Questionnaire hebdomadaire", "description": "Au cours des 7 derniers jours...", "questions": [ - "Combien d'heures de <span class='font-bold'>cours</span> de {TARGET_LANGUAGE} avez vous suivies ?", - "Combien d'heures avez-vous <span class='font-bold'>regardé des vidéos</span> en {TARGET_LANGUAGE} (films, séries, Youtube...) ou <span class='font-bold'>écouté des contenus</span> en {TARGET_LANGUAGE} (podcasts, radio, cours universitaires...) ?", - "Combien d'heures avez-vous <span class='font-bold'>lu des textes</span> en {TARGET_LANGUAGE} (livre, journal, BD, sites web...) ?", - "Combien d'heures avez-vous <span class='font-bold'>parlé</span> en {TARGET_LANGUAGE} (discussions avec amis, famille, collègues...) ?" + "Combien d'heures de <span class='font-bold'>cours</span> de {lang} avez vous suivies ?", + "Combien d'heures avez-vous <span class='font-bold'>regardé des vidéos</span> en {lang} (films, séries, Youtube...) ou <span class='font-bold'>écouté des contenus</span> en {lang} (podcasts, radio, cours universitaires...) ?", + "Combien d'heures avez-vous <span class='font-bold'>lu des textes</span> en {lang} (livre, journal, BD, sites web...) ?", + "Combien d'heures avez-vous <span class='font-bold'>parlé</span> en {lang} (discussions avec amis, famille, collègues...) ?" ], "answers": { "placeholder": "", @@ -286,6 +285,20 @@ "november": "novembre", "december": "décembre" }, + "shortMonth": { + "january": "janv.", + "february": "févr.", + "march": "mars", + "april": "avr.", + "may": "mai", + "june": "juin", + "july": "juil.", + "august": "août", + "september": "sept.", + "october": "oct.", + "november": "nov.", + "december": "déc." + }, "days": { "monday": "Lundi", "tuesday": "Mardi", @@ -302,6 +315,15 @@ "5": "Samedi", "6": "Dimanche" }, + "past": { + "year": "Il y a {n, plural, one {# an} other {# ans}}", + "month": "Il y a {n, plural, one {# mois} other {# mois}}", + "day": "Il y a {n, plural, one {# jour} other {# jours}}", + "hour": "Il y a {n, plural, one {# heure} other {# heures}}", + "today": "Aujourd'hui", + "yesterday": "Hier", + "justNow": "Il y a moins d'une heure" + }, "language": { "fr": "Français", "en": "Anglais", @@ -315,7 +337,13 @@ "words": { "date": "Date", "messages": "Messages", - "actions": "Actions" + "actions": "Actions", + "participants": "Participants", + "status": "Statut", + "date": "Date", + "programed": "Programmée", + "inProgress": "En cours", + "finished": "Terminée" } }, "inputs": { diff --git a/frontend/src/lib/components/users/weeklySurvey.svelte b/frontend/src/lib/components/users/weeklySurvey.svelte index 76790f854fb6e5355d45e659e0992fea374280cb..8143e783b884b73140fb9f8f69cddec53a50a731 100644 --- a/frontend/src/lib/components/users/weeklySurvey.svelte +++ b/frontend/src/lib/components/users/weeklySurvey.svelte @@ -49,10 +49,9 @@ <label class="form-control w-full"> <div class="label"> <span class="label-text" - >{@html $t('session.modal.weekly.questions.' + i).replaceAll( - '{TARGET_LANGUAGE}', - $t('utils.language.' + $user?.target_language).toLowerCase() - )}</span + >{@html $t('session.modal.weekly.questions.' + i, { + lang: $t('utils.language.' + $user?.target_language).toLowerCase() + })}</span > </div> <select id={'questions-' + i} class="select select-bordered"> diff --git a/frontend/src/lib/services/i18n.ts b/frontend/src/lib/services/i18n.ts index 7158bc8f2cfec1a9903fb6a587f35923922edc05..f5e350b0c5275e541f19b33437769056a054b68b 100644 --- a/frontend/src/lib/services/i18n.ts +++ b/frontend/src/lib/services/i18n.ts @@ -1,6 +1,12 @@ -import i18n, { type Config } from 'sveltekit-i18n'; +import i18n from '@sveltekit-i18n/base'; +import parser from '@sveltekit-i18n/parser-icu'; + +import type { Config } from '@sveltekit-i18n/parser-icu'; const config: Config = { + parser: parser({ + ignoreTag: true + }), loaders: [ { locale: 'en', diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts index c8de2aa14b74b6fdfdabaa5bba3e601591e5edeb..c276e2d2c6aa0bd8de3073db92eadffc851b9d0d 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -32,6 +32,37 @@ export function getFullMonth(id: number): string { } } +export function getShortMonth(id: number): string { + switch (id) { + case 0: + return get(t)('utils.shortMonth.january'); + case 1: + return get(t)('utils.shortMonth.february'); + case 2: + return get(t)('utils.shortMonth.march'); + case 3: + return get(t)('utils.shortMonth.april'); + case 4: + return get(t)('utils.shortMonth.may'); + case 5: + return get(t)('utils.shortMonth.june'); + case 6: + return get(t)('utils.shortMonth.july'); + case 7: + return get(t)('utils.shortMonth.august'); + case 8: + return get(t)('utils.shortMonth.september'); + case 9: + return get(t)('utils.shortMonth.october'); + case 10: + return get(t)('utils.shortMonth.november'); + case 11: + return get(t)('utils.shortMonth.december'); + default: + return '??'; + } +} + export function displayDate(date: Date): string { if (date === null) return ''; @@ -72,19 +103,21 @@ export function displayTime(date: Date): string { now.getFullYear() === date.getFullYear() && now.getMonth() === date.getMonth() ) { - return hours + ':' + minutes; + return ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2); } const day = date.getDate().toString(); const month = getFullMonth(date.getMonth()); if (now.getFullYear() === date.getFullYear()) { - return day + ' ' + month + ' ' + hours + ':' + minutes; + return day + ' ' + month + ' ' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2); } const year = date.getFullYear().toString(); - return day + ' ' + month + ' ' + year + ' ' + hours + ':' + minutes; + return ( + day + ' ' + month + ' ' + year + ' ' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2) + ); } export function displayDuration(start: Date, end: Date): string | null { @@ -106,3 +139,39 @@ export function parseToLocalDate(dateStr: string): Date { export function formatToUTCDate(date: Date): string { return date.toISOString().split('Z')[0]; } + +export function displayShortTime(date: Date): string { + const now = new Date(); + + return ( + ('0' + date.getDate()).slice(-2) + + ' ' + + getShortMonth(date.getMonth()) + + (date.getFullYear() != now.getFullYear() ? ' ' + date.getFullYear() : '') + + ' ' + + ('0' + date.getHours()).slice(-2) + + 'h' + ); +} + +export function displayTimeSince(date: Date): string { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + if (years > 0) { + return get(t)('utils.past.year', { n: years }); + } else if (months > 0) { + return get(t)('utils.past.month', { n: months }); + } else if (days === 1) { + return get(t)('utils.past.yesterday'); + } else if (days > 0) { + return get(t)('utils.past.day', { n: days }); + } else { + return get(t)('utils.past.today'); + } +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 5acdde5d92dccd0f59531a4e641de84da689efb3..ed9f6fef645c15796ec431ddc8aaa58ffe989371 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,13 +1,14 @@ <script lang="ts"> import Session from '$lib/types/session'; import { onMount } from 'svelte'; - import { displayTime } from '$lib/utils/date'; + import { displayShortTime, displayTimeSince } from '$lib/utils/date'; import { AcademicCap, Sparkles, Icon, User as UserIcon, - MagnifyingGlass + MagnifyingGlass, + ArrowRightCircle } from 'svelte-hero-icons'; import { t } from '$lib/services/i18n'; import User, { user } from '$lib/types/user'; @@ -27,14 +28,19 @@ let modalNew = false; let nickname = ''; + let showTerminatedSessions = false; + async function selectContact(c: User | null) { + showTerminatedSessions = false; contact = c; if (!contact) { contactSessions = []; return; } - contactSessions = Session.parseAll(await getUserContactSessionsAPI($user!.id, contact.id)); + contactSessions = Session.parseAll(await getUserContactSessionsAPI($user!.id, contact.id)).sort( + (a, b) => b.start_time.getTime() - a.start_time.getTime() + ); } onMount(async () => { @@ -103,7 +109,9 @@ return; } toastSuccess(get(t)('home.bookingSuccessful')); - contactSessions = Session.parseAll(await getUserContactSessionsAPI($user!.id, contact.id)); + contactSessions = Session.parseAll( + await getUserContactSessionsAPI($user!.id, contact.id) + ).sort((a, b) => b.start_time.getTime() - a.start_time.getTime()); } }); }); @@ -113,7 +121,9 @@ let session = await Session.create(); if (!session) return; await session.addUser(contact); - contactSessions = [...contactSessions, session]; + contactSessions = [...contactSessions, session].sort( + (a, b) => b.start_time.getTime() - a.start_time.getTime() + ); } async function searchNickname() { @@ -130,18 +140,13 @@ } </script> -<svelte:head> - <script> - </script> -</svelte:head> - {#if ready} - <div class="h-full w-full flex"> - <ul class="h-full [width:_clamp(200px,25%,500px)] overflow-y-scroll border-r-2 flex flex-col"> + <div class="flex-row h-full flex py-4 flex-grow overflow-y-hidden"> + <div class="flex flex-col border shadow-[0_0_6px_0_rgba(0,14,156,.2)] min-w-72 rounded-r-xl"> <div class="flex-grow"> {#each contacts as c (c.id)} - <li - class="h-20 flex border-gray-300 border-b-2 hover:bg-gray-200 hover:cursor-pointer" + <div + class="h-24 flex border-gray-300 border-b-2 hover:bg-gray-200 hover:cursor-pointer p-4" class:bg-gray-200={c.id === contact?.id} on:click={() => selectContact(c)} role="button" @@ -149,19 +154,19 @@ tabindex="0" on:keydown={(e) => e.key === 'Enter' && selectContact(c)} > - <div class="w-16 ml-2 mr-4 p-4 avatar bg-gray-300 mask-squircle mask"> + <div class="w-16 ml-2 mr-4 p-4 bg-gray-300 rounded-2xl"> {#if c.type == 0} <Icon src={Sparkles} class="mask mask-squircle" /> {:else if c.type == 1} - <Icon src={AcademicCap} /> + <Icon src={AcademicCap} class="" /> {:else} <Icon src={UserIcon} /> {/if} </div> - <div class="flex items-center text-lg"> + <div class="text-lg font-bold capitalize flex items-center"> {c.nickname} </div> - </li> + </div> {/each} </div> <button @@ -170,80 +175,125 @@ > + </button> - </ul> - <div class="flex-grow flex-col flex"> - {#if contact} - <div class="p-4 pr-8"> - <button on:click|preventDefault={createSession} class="button float-end"> + </div> + {#if contact} + <div class="flex flex-col xl:mx-auto xl:w-[60rem] m-4"> + <div> + <button on:click|preventDefault={createSession} class="button float-start mr-2"> {$t('home.createSession')} </button> - <div class="size-4 float-end"></div> <button - class="button float-end" + class="button float-start" class:btn-disabled={!contact || !contact.calcom_link} data-cal-link={`${contact.calcom_link}?email=${$user?.email}&name=${$user?.nickname}`} > {$t('home.bookSession')} </button> </div> - <div class="flex-grow p-2"> - <h2 class="text-xl my-4 font-bold">{$t('home.currentSessions')}</h2> - <ul> - {#each contactSessions as s (s.id)} - {#if s.start_time <= new Date() && s.end_time >= new Date()} - <li> - <a - class="block p-4 m-1 mx-4 rounded-md w-[calc(100%-32px)] border-2 hover:bg-gray-200 text-center" - href={`/session?id=${s.id}`} - > - {displayTime(s.start_time)} - {displayTime(s.end_time)} - </a> - </li> - {/if} - {/each} - </ul> - <h2 class="text-xl my-4 font-bold">{$t('home.plannedSessions')}</h2> - <ul> - {#each contactSessions as s (s.id)} - {#if s.start_time > new Date()} - <li> - <a - class="block p-4 m-1 mx-4 rounded-md w-[calc(100%-32px)] border-2 hover:bg-gray-200 text-center" - href={`/session?id=${s.id}`} - > - {displayTime(s.start_time)} - {displayTime(s.end_time)} - </a> - </li> - {/if} - {/each} - </ul> - <h2 class="text-xl my-4 font-bold">{$t('home.pastSessions')}</h2> - <ul> - {#each contactSessions as s (s.id)} - {#if s.end_time < new Date()} - <li> - <a - class="block p-4 m-1 mx-4 rounded-md w-[calc(100%-32px)] border-2 hover:bg-gray-200 text-center" - href={`/session?id=${s.id}`} + <div + class="border p-4 mt-4 rounded-xl shadow-[0_0_6px_0_rgba(0,14,156,.2)] overflow-y-scroll no-scrollbar" + > + <table class="divide-y divide-neutral-300 text-center w-full table-fixed"> + <thead> + <tr> + <th scope="col" class="text-left">{$t('utils.words.date')}</th> + <th scope="col">{$t('utils.words.status')}</th> + <th scope="col"># {$t('utils.words.messages').toLowerCase()}</th> + <th scope="col">{$t('utils.words.actions')}</th> + </tr> + </thead> + <tbody class="divide-y divide-neutral-200"> + {#if contactSessions.length === 0} + <tr> + <td colspan="4" class="py-5 text-gray-500">{$t('home.noSessions')}</td> + </tr> + {:else} + {#if !showTerminatedSessions && contactSessions.filter((s) => s.end_time >= new Date()).length === 0} + <tr> + <td colspan="4" class="py-5 text-gray-500" + >{$t('home.noCurrentOrFutureSessions')}</td + > + </tr> + {/if} + {#each contactSessions as s (s.id)} + {#if showTerminatedSessions || s.end_time >= new Date()} + <tr> + <td class="py-2 text-left space-y-1"> + <div> + {displayShortTime(s.start_time)} + </div> + <div class="text-sm italic text-gray-600"> + {displayTimeSince(s.start_time)} + </div> + </td> + <td class="py-2"> + {#if s.start_time <= new Date() && s.end_time >= new Date()} + <span class="bg-green-200 rounded-lg px-2 py-1" + >{$t('utils.words.inProgress')}</span + > + {:else if s.start_time > new Date()} + <span class="bg-orange-200 rounded-lg px-2 py-1" + >{$t('utils.words.programed')}</span + > + {:else} + <span class="bg-red-200 rounded-lg px-2 py-1" + >{$t('utils.words.finished')}</span + > + {/if} + </td> + <td class="py-2">{s.length} {$t('utils.words.messages').toLowerCase()}</td> + <td class="py-2"> + <a href="/session?id={s.id}" class="group"> + <Icon + src={ArrowRightCircle} + size="32" + class="text-accent mx-auto group-hover:text-white group-hover:bg-accent rounded-full" + /> + </a> + </td> + </tr> + {/if} + {/each} + <tr> + <td + class="py-2 hover:cursor-pointer" + colspan="4" + on:click={() => (showTerminatedSessions = !showTerminatedSessions)} > - {displayTime(s.start_time)} - {displayTime(s.end_time)} - </a> - </li> + <button aria-label={showTerminatedSessions ? 'Hide' : 'Show'}> + <svg + class="size-3 ms-3" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 10 6" + class:rotate-180={showTerminatedSessions} + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="m1 1 4 4 4-4" + /> + </svg> + </button> + </td> + </tr> {/if} - {/each} - </ul> + </tbody> + </table> </div> - {/if} - </div> + </div> + {/if} </div> {/if} <dialog - class="modal" + class="modal bg-black bg-opacity-50" open={modalNew} on:close={() => (modalNew = false)} - on:keydown={(e) => e.key === 'Escape' && (modalNew = false)} - tabindex="0" + tabindex="-1" > <div class="modal-box"> <h2 class="text-xl font-bold mb-4">{$t('home.newContact')}</h2> @@ -253,6 +303,7 @@ placeholder={$t('home.email')} bind:value={nickname} class="input flex-grow mr-2" + on:keydown={(e) => e.key === 'Escape' && (modalNew = false)} /> <button class="button w-16" on:click={searchNickname}> <Icon src={MagnifyingGlass} />