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..fa3c6049cc3f72b5d713d76e38414afdcc7ac34e 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -225,10 +225,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": "", @@ -302,6 +302,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", 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..5e791ed0af52ce89bbe3ff6397374f2d80f3c212 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -72,19 +72,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 +108,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 { + return ( + ('0' + date.getDate()).slice(-2) + + '/' + + ('0' + (date.getMonth() + 1)).slice(-2) + + '/' + + date.getFullYear() + + ' ' + + ('0' + date.getHours()).slice(-2) + + ':' + + ('0' + date.getMinutes()).slice(-2) + ); +} + +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..07ddf577a3215bc6db96fca3a918dc31adba6c85 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,13 +1,15 @@ <script lang="ts"> import Session from '$lib/types/session'; import { onMount } from 'svelte'; - import { displayTime } from '$lib/utils/date'; + import { displayShortTime, displayTime, displayTimeSince } from '$lib/utils/date'; import { AcademicCap, Sparkles, Icon, User as UserIcon, - MagnifyingGlass + MagnifyingGlass, + ArrowRight, + ArrowRightCircle } from 'svelte-hero-icons'; import { t } from '$lib/services/i18n'; import User, { user } from '$lib/types/user'; @@ -34,7 +36,9 @@ 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 +107,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 +119,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() { @@ -136,105 +144,72 @@ </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-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" - class:bg-gray-200={c.id === contact?.id} - on:click={() => selectContact(c)} - role="button" - aria-label={c.nickname} - 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"> - {#if c.type == 0} - <Icon src={Sparkles} class="mask mask-squircle" /> - {:else if c.type == 1} - <Icon src={AcademicCap} /> - {:else} - <Icon src={UserIcon} /> - {/if} - </div> - <div class="flex items-center text-lg"> - {c.nickname} - </div> - </li> - {/each} + <div class="flex-col flex p-4 lg:w-[64rem] lg:mx-auto"> + {#if contact} + <div> + <button on:click|preventDefault={createSession} class="button float-start mr-2"> + {$t('home.createSession')} + </button> + <button + 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> - <button - class="h-20 w-full flex justify-center items-center text-lg border-gray-200 border-t hover:bg-gray-200" - on:click={() => (modalNew = true)} + <div + class="border p-4 mt-4 rounded-xl shadow-[0_0_6px_0_rgba(0,14,156,.2)] overflow-y-scroll no-scrollbar" > - + - </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"> - {$t('home.createSession')} - </button> - <div class="size-4 float-end"></div> - <button - class="button float-end" - 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> + <table class="divide-y divide-neutral-300 text-center w-full"> + <thead> + <tr> + <th scope="col" class="text-left">Date</th> + <th scope="col">Status</th> + <th scope="col">Participants</th> + <th scope="col"># messages</th> + <th scope="col"></th> + </tr> + </thead> + <tbody class="divide-y divide-neutral-200"> {#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)} + <tr> + <td class="py-2 text-left"> + <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(2024, 10, 13, 3) && s.start_time <= new Date()} + <span class="bg-purple-200 rounded-lg px-2 py-1">En ligne</span> + {:else if s.start_time <= new Date() && s.end_time >= new Date()} + <span class="bg-green-200 rounded-lg px-2 py-1">En cours</span> + {:else if s.start_time > new Date()} + <span class="bg-orange-200 rounded-lg px-2 py-1">Programmée</span> + {:else} + <span class="bg-red-200 rounded-lg px-2 py-1">Terminée</span> + {/if} + </td> + <td class="py-2">{s.otherUsersList(5)}</td> + <td class="py-2">4 messages</td> + <td class="py-2"> + <a href="/session?id={s.id}"> + <Icon + src={ArrowRightCircle} + size="32" + class="text-accent float-end hover:text-white hover:bg-accent hover:cursor-pointer rounded-full" + /> </a> - </li> - {/if} + </td> + </tr> {/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}`} - > - {displayTime(s.start_time)} - {displayTime(s.end_time)} - </a> - </li> - {/if} - {/each} - </ul> - </div> - {/if} - </div> + </tbody> + </table> + </div> + {/if} </div> {/if}