diff --git a/backend/app/main.py b/backend/app/main.py index 3ccd84a7dae05f69231d2a92e4503dfb17931b99..779027522cbd9d75654256ae08b765c7f19b0a35 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -83,12 +83,11 @@ def health(): @authRouter.post("/login", status_code=200) def login( - email: Annotated[str, Form()], - password: Annotated[str, Form()], + login: schemas.LoginData, response: Response, db: Session = Depends(get_db), ): - db_user = crud.get_user_by_email_and_password(db, email, password) + db_user = crud.get_user_by_email_and_password(db, login.email, login.password) if db_user is None: raise HTTPException(status_code=401, detail="Incorrect email or password") @@ -112,21 +111,22 @@ def login( @authRouter.post("/register", status_code=status.HTTP_201_CREATED) def register( - email: Annotated[str, Form()], - password: Annotated[str, Form()], - nickname: Annotated[str, Form()], - tutor: Annotated[bool, Form()], + register: schemas.RegisterData, db: Session = Depends(get_db), ): - db_user = crud.get_user_by_email(db, email=email) + db_user = crud.get_user_by_email(db, email=register.email) if db_user: raise HTTPException(status_code=400, detail="User already registered") user_data = schemas.UserCreate( - email=email, - password=password, - nickname=nickname, - type=models.UserType.TUTOR.value if tutor else models.UserType.STUDENT.value, + email=register.email, + password=register.password, + nickname=register.nickname, + type=( + models.UserType.TUTOR.value + if register.is_tutor + else models.UserType.STUDENT.value + ), ) user = crud.create_user(db=db, user=user_data) @@ -206,9 +206,8 @@ def update_user( db: Session = Depends(get_db), current_user: schemas.User = Depends(get_jwt_user), ): - if ( - not check_user_level(current_user, models.UserType.ADMIN) - and current_user.id != user_id + if not check_user_level(current_user, models.UserType.ADMIN) and ( + current_user.id != user_id or user.type is not None ): raise HTTPException( status_code=401, detail="You do not have permission to update this user" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 464eec16dc3c2cf2369c30f7bf03278b7c36272e..caf180e679e9ffd24479fffc23ab9228aa39eca8 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -3,6 +3,18 @@ from pydantic import BaseModel, NaiveDatetime from models import UserType +class LoginData(BaseModel): + email: str + password: str + + +class RegisterData(BaseModel): + email: str + password: str + nickname: str + is_tutor: bool + + class User(BaseModel): id: int email: str diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a9519f482155d15532bce8aa0e79c926dbd56153..3a0ba508e603ea29e41710d3f0da07bdb9eebaac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,10 +12,11 @@ "@sveltekit-i18n/parser-icu": "^1.0.8", "dayjs": "^1.11.13", "emoji-picker-element": "^1.23.0", + "js-sha256": "^0.11.0", "linkify-html": "^4.1.3", "linkifyjs": "^4.1.3", "sanitize-html": "^2.13.1", - "svelte-gravatar": "^1.0.3", + "svelte-autosize": "^1.1.5", "svelte-i18n": "^4.0.1", "svelte-select": "^5.8.3" }, @@ -31,6 +32,7 @@ "@tsconfig/svelte": "^5.0.4", "@types/eslint": "^9.6.1", "@types/js-cookie": "^3.0.6", + "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/parser": "^8.14.0", "@zerodevx/svelte-toast": "^0.9.6", @@ -1426,6 +1428,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/autosize": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/autosize/-/autosize-4.0.3.tgz", + "integrity": "sha512-o0ZyU3ePp3+KRbhHsY4ogjc+ZQWgVN5h6j8BHW5RII4cFKi6PEKK9QPAcphJVkD0dGpyFnD3VRR0WMvHVjCv9w==", + "license": "MIT" + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1471,6 +1479,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", @@ -1871,6 +1889,12 @@ "postcss": "^8.1.0" } }, + "node_modules/autosize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-6.0.1.tgz", + "integrity": "sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==", + "license": "MIT" + }, "node_modules/axios": { "version": "1.7.9", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", @@ -2053,15 +2077,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -2190,15 +2205,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/css-selector-tokenizer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", @@ -3356,12 +3362,6 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -3494,6 +3494,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3719,17 +3725,6 @@ "semver": "bin/semver" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/memoizee": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", @@ -4899,6 +4894,19 @@ "node": ">=18" } }, + "node_modules/svelte-autosize": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/svelte-autosize/-/svelte-autosize-1.1.5.tgz", + "integrity": "sha512-whiND/GthFDG9Ansvil21qxmFhSMfuooVZPg40sbcLHYKR9srYhnfrP5qdw8MXHAm6DY9g5PawurOAWl34fK7g==", + "license": "MIT", + "dependencies": { + "@types/autosize": "^4.0.3", + "autosize": "*" + }, + "peerDependencies": { + "svelte": ">=3.0.0" + } + }, "node_modules/svelte-check": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz", @@ -4996,29 +5004,6 @@ "@floating-ui/dom": "^1.5.3" } }, - "node_modules/svelte-gravatar": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/svelte-gravatar/-/svelte-gravatar-1.0.3.tgz", - "integrity": "sha512-CNxIV2lAuiqwdaPrGAM/BFj5U1dNNQXzeyh+HVi/48BODFXoDy0L1CMqYyvM+aKiF4ideZUBwT0S9/C1BeL5oA==", - "license": "MIT", - "dependencies": { - "md5": "^2.2.1", - "svelte": "^3.16.0", - "svelte-waypoint": "^0.1.3" - }, - "peerDependencies": { - "svelte": "*" - } - }, - "node_modules/svelte-gravatar/node_modules/svelte": { - "version": "3.59.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", - "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/svelte-hero-icons": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/svelte-hero-icons/-/svelte-hero-icons-5.2.0.tgz", @@ -5125,12 +5110,6 @@ "svelte-floating-ui": "1.5.8" } }, - "node_modules/svelte-waypoint": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/svelte-waypoint/-/svelte-waypoint-0.1.4.tgz", - "integrity": "sha512-UEqoXZjJeKj2sWlAIsBOFjxjMn+KP8aFCc/zjdmZi1cCOE59z6T2C+I6ZaAf8EmNQqNzfZVB/Lci4Ci9spzXAw==", - "license": "MIT" - }, "node_modules/svelte/node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index c99f8cecdc3690f0f9a8ddee86148fd4c0cebd12..bb074c8ddc03fa17101e259c2ada4cd270d7a952 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,12 +3,13 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "VITE_API_URL=http://127.0.0.1:8000/api/v1 VITE_APP_URL=http://127.0.0.1:5173 VITE_WS_URL=ws://127.0.0.1:8000/api/v1/ws vite dev --host 127.0.0.1", + "dev": "VITE_API_URL=http://127.0.0.1:5173/api VITE_APP_URL=http://127.0.0.1:5173 VITE_WS_URL=ws://127.0.0.1:8000/api/v1/ws vite dev --host 127.0.0.1", "build": "vite build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check .", - "format": "prettier --write . --log-level warn" + "lint": "prettier --check . && tsc --noEmit", + "format": "prettier --write . --log-level warn", + "tsc": "tsc" }, "devDependencies": { "@rollup/plugin-commonjs": "^28.0.1", @@ -22,6 +23,7 @@ "@tsconfig/svelte": "^5.0.4", "@types/eslint": "^9.6.1", "@types/js-cookie": "^3.0.6", + "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/parser": "^8.14.0", "@zerodevx/svelte-toast": "^0.9.6", @@ -54,10 +56,11 @@ "@sveltekit-i18n/parser-icu": "^1.0.8", "dayjs": "^1.11.13", "emoji-picker-element": "^1.23.0", + "js-sha256": "^0.11.0", "linkify-html": "^4.1.3", "linkifyjs": "^4.1.3", "sanitize-html": "^2.13.1", - "svelte-gravatar": "^1.0.3", + "svelte-autosize": "^1.1.5", "svelte-i18n": "^4.0.1", "svelte-select": "^5.8.3" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 80254a00fa050a8ff36803d4abd5cce6ba87e370..fa2d4e6f24020c7755103b3a391f260dcf2bee40 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -19,19 +19,22 @@ importers: version: 1.11.13 emoji-picker-element: specifier: ^1.23.0 - version: 1.24.0 + version: 1.25.0 + js-sha256: + specifier: ^0.11.0 + version: 0.11.0 linkify-html: specifier: ^4.1.3 - version: 4.1.4(linkifyjs@4.1.4) + version: 4.2.0(linkifyjs@4.2.0) linkifyjs: specifier: ^4.1.3 - version: 4.1.4 + version: 4.2.0 sanitize-html: specifier: ^2.13.1 version: 2.13.1 - svelte-gravatar: - specifier: ^1.0.3 - version: 1.0.3(svelte@5.8.1) + svelte-autosize: + specifier: ^1.1.5 + version: 1.1.5(svelte@5.8.1) svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@5.8.1) @@ -41,31 +44,28 @@ importers: devDependencies: '@rollup/plugin-commonjs': specifier: ^28.0.1 - version: 28.0.1(rollup@4.27.3) + version: 28.0.1(rollup@4.28.1) '@rollup/plugin-json': specifier: ^6.1.0 - version: 6.1.0(rollup@4.27.3) - '@rollup/plugin-node-resolve': - specifier: ^15.3.0 - version: 15.3.0(rollup@4.27.3) + version: 6.1.0(rollup@4.28.1) '@sveltejs/adapter-auto': specifier: ^3.3.1 - version: 3.3.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0))) + version: 3.3.1(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1))) '@sveltejs/adapter-node': specifier: ^5.2.9 - version: 5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0))) + version: 5.2.9(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1))) '@sveltejs/adapter-static': specifier: ^3.0.6 - version: 3.0.6(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0))) + version: 3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1))) '@sveltejs/kit': specifier: ^2.8.0 - version: 2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) + version: 2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.0 - version: 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) + version: 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) '@tailwindcss/forms': specifier: ^0.5.9 - version: 0.5.9(tailwindcss@3.4.15) + version: 0.5.9(tailwindcss@3.4.16) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.4 @@ -75,12 +75,15 @@ importers: '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 + '@types/sanitize-html': + specifier: ^2.13.0 + version: 2.13.0 '@typescript-eslint/eslint-plugin': specifier: ^8.14.0 - version: 8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + version: 8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) '@typescript-eslint/parser': specifier: ^8.14.0 - version: 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + version: 8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) '@zerodevx/svelte-toast': specifier: ^0.9.6 version: 0.9.6(svelte@5.8.1) @@ -89,64 +92,64 @@ importers: version: 10.4.20(postcss@8.4.49) axios: specifier: ^1.7.7 - version: 1.7.7 + version: 1.7.9 axios-jwt: specifier: ^4.0.3 - version: 4.0.3(axios@1.7.7) + version: 4.0.3(axios@1.7.9) daisyui: specifier: ^4.12.14 version: 4.12.14(postcss@8.4.49) eslint: specifier: ^9.14.0 - version: 9.15.0(jiti@1.21.6) + version: 9.16.0(jiti@1.21.6) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.15.0(jiti@1.21.6)) + version: 9.1.0(eslint@9.16.0(jiti@1.21.6)) eslint-plugin-svelte: specifier: ^2.46.0 - version: 2.46.0(eslint@9.15.0(jiti@1.21.6))(svelte@5.8.1) + version: 2.46.1(eslint@9.16.0(jiti@1.21.6))(svelte@5.8.1) jwt-decode: specifier: ^4.0.0 version: 4.0.0 less: specifier: ^4.2.0 - version: 4.2.0 + version: 4.2.1 postcss: specifier: ^8.4.49 version: 8.4.49 prettier: specifier: ^3.3.3 - version: 3.3.3 + version: 3.4.2 prettier-plugin-svelte: specifier: ^3.2.8 - version: 3.2.8(prettier@3.3.3)(svelte@5.8.1) + version: 3.3.2(prettier@3.4.2)(svelte@5.8.1) svelte: specifier: ^5.1.15 version: 5.8.1 svelte-check: specifier: ^4.0.7 - version: 4.0.9(picomatch@4.0.2)(svelte@5.8.1)(typescript@5.6.3) + version: 4.1.1(picomatch@4.0.2)(svelte@5.8.1)(typescript@5.7.2) svelte-hero-icons: specifier: ^5.2.0 version: 5.2.0(svelte@5.8.1) svelte-preprocess: specifier: ^6.0.3 - version: 6.0.3(less@4.2.0)(postcss-load-config@4.0.2(postcss@8.4.49))(postcss@8.4.49)(svelte@5.8.1)(typescript@5.6.3) + version: 6.0.3(less@4.2.1)(postcss-load-config@4.0.2(postcss@8.4.49))(postcss@8.4.49)(svelte@5.8.1)(typescript@5.7.2) sveltekit-i18n: specifier: ^2.4.2 version: 2.4.2(svelte@5.8.1) tailwindcss: specifier: ^3.4.14 - version: 3.4.15 + version: 3.4.16 tslib: specifier: ^2.8.1 version: 2.8.1 typescript: specifier: ^5.6.3 - version: 5.6.3 + version: 5.7.2 vite: specifier: ^5.4.11 - version: 5.4.11(less@4.2.0) + version: 5.4.11(less@4.2.1) packages: @@ -444,28 +447,28 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.19.0': - resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.9.0': - resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.2.0': resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.15.0': - resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + '@eslint/js@9.16.0': + resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.3': - resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@floating-ui/core@1.6.8': @@ -589,93 +592,98 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.27.3': - resolution: {integrity: sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==} + '@rollup/rollup-android-arm-eabi@4.28.1': + resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.27.3': - resolution: {integrity: sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==} + '@rollup/rollup-android-arm64@4.28.1': + resolution: {integrity: sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.27.3': - resolution: {integrity: sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==} + '@rollup/rollup-darwin-arm64@4.28.1': + resolution: {integrity: sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.27.3': - resolution: {integrity: sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==} + '@rollup/rollup-darwin-x64@4.28.1': + resolution: {integrity: sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.27.3': - resolution: {integrity: sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==} + '@rollup/rollup-freebsd-arm64@4.28.1': + resolution: {integrity: sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.27.3': - resolution: {integrity: sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==} + '@rollup/rollup-freebsd-x64@4.28.1': + resolution: {integrity: sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.27.3': - resolution: {integrity: sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': + resolution: {integrity: sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.27.3': - resolution: {integrity: sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==} + '@rollup/rollup-linux-arm-musleabihf@4.28.1': + resolution: {integrity: sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.27.3': - resolution: {integrity: sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==} + '@rollup/rollup-linux-arm64-gnu@4.28.1': + resolution: {integrity: sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.27.3': - resolution: {integrity: sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==} + '@rollup/rollup-linux-arm64-musl@4.28.1': + resolution: {integrity: sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': - resolution: {integrity: sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==} + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': + resolution: {integrity: sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': + resolution: {integrity: sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.27.3': - resolution: {integrity: sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==} + '@rollup/rollup-linux-riscv64-gnu@4.28.1': + resolution: {integrity: sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.27.3': - resolution: {integrity: sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==} + '@rollup/rollup-linux-s390x-gnu@4.28.1': + resolution: {integrity: sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.27.3': - resolution: {integrity: sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==} + '@rollup/rollup-linux-x64-gnu@4.28.1': + resolution: {integrity: sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.27.3': - resolution: {integrity: sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==} + '@rollup/rollup-linux-x64-musl@4.28.1': + resolution: {integrity: sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.27.3': - resolution: {integrity: sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==} + '@rollup/rollup-win32-arm64-msvc@4.28.1': + resolution: {integrity: sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.27.3': - resolution: {integrity: sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==} + '@rollup/rollup-win32-ia32-msvc@4.28.1': + resolution: {integrity: sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.27.3': - resolution: {integrity: sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==} + '@rollup/rollup-win32-x64-msvc@4.28.1': + resolution: {integrity: sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==} cpu: [x64] os: [win32] @@ -697,14 +705,14 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.8.1': - resolution: {integrity: sha512-uuOfFwZ4xvnfPsiTB6a4H1ljjTUksGhWnYq5X/Y9z4x5+3uM2Md8q/YVeHL+7w+mygAwoEFdgKZ8YkUuk+VKww==} + '@sveltejs/kit@2.9.0': + resolution: {integrity: sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 + vite: ^5.0.3 || ^6.0.0 '@sveltejs/vite-plugin-svelte-inspector@3.0.1': resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} @@ -740,6 +748,9 @@ packages: '@tsconfig/svelte@5.0.4': resolution: {integrity: sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==} + '@types/autosize@4.0.3': + resolution: {integrity: sha512-o0ZyU3ePp3+KRbhHsY4ogjc+ZQWgVN5h6j8BHW5RII4cFKi6PEKK9QPAcphJVkD0dGpyFnD3VRR0WMvHVjCv9w==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -758,8 +769,11 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@typescript-eslint/eslint-plugin@8.15.0': - resolution: {integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==} + '@types/sanitize-html@2.13.0': + resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} + + '@typescript-eslint/eslint-plugin@8.17.0': + resolution: {integrity: sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -769,8 +783,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.15.0': - resolution: {integrity: sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==} + '@typescript-eslint/parser@8.17.0': + resolution: {integrity: sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -779,12 +793,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.15.0': - resolution: {integrity: sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==} + '@typescript-eslint/scope-manager@8.17.0': + resolution: {integrity: sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.15.0': - resolution: {integrity: sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==} + '@typescript-eslint/type-utils@8.17.0': + resolution: {integrity: sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -793,12 +807,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.15.0': - resolution: {integrity: sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==} + '@typescript-eslint/types@8.17.0': + resolution: {integrity: sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.15.0': - resolution: {integrity: sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==} + '@typescript-eslint/typescript-estree@8.17.0': + resolution: {integrity: sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -806,8 +820,8 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.15.0': - resolution: {integrity: sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==} + '@typescript-eslint/utils@8.17.0': + resolution: {integrity: sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -816,8 +830,8 @@ packages: typescript: optional: true - '@typescript-eslint/visitor-keys@8.15.0': - resolution: {integrity: sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==} + '@typescript-eslint/visitor-keys@8.17.0': + resolution: {integrity: sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@zerodevx/svelte-toast@0.9.6': @@ -886,6 +900,9 @@ packages: peerDependencies: postcss: ^8.1.0 + autosize@6.0.1: + resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==} + axios-jwt@4.0.3: resolution: {integrity: sha512-8y2lXSG3v/AKgrwT2fjwN0+GRjOVoHJk/dlbAoaUOI9+Ym9xbptkPZd7HS+QdPynzdcDykeiJjQUoruInF+shQ==} peerDependencies: @@ -895,8 +912,8 @@ packages: '@react-native-async-storage/async-storage': optional: true - axios@1.7.7: - resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -932,16 +949,13 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001680: - resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + caniuse-lite@1.0.30001687: + resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -986,9 +1000,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - css-selector-tokenizer@0.8.0: resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} @@ -1012,8 +1023,8 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1057,11 +1068,11 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.63: - resolution: {integrity: sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==} + electron-to-chromium@1.5.71: + resolution: {integrity: sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==} - emoji-picker-element@1.24.0: - resolution: {integrity: sha512-dIUJ6ZXult6lsnDxG0Y0YLUhZ49mzImpg9pZjPoYFRP9wzx65y5ypS5W8I8SOyIOp4rgKZXB0fma1i7NbRJj0A==} + emoji-picker-element@1.25.0: + resolution: {integrity: sha512-UcUMxqIuneLCsEJ5KpqTD1xaHZyUpg6Oa7uCVe5AMXXpsW3C2TNegbNLXj2/rlbyr6qVMf7lXTFyzvFEarOIUg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1121,8 +1132,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@2.46.0: - resolution: {integrity: sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==} + eslint-plugin-svelte@2.46.1: + resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 @@ -1147,8 +1158,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.15.0: - resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + eslint@9.16.0: + resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1344,9 +1355,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -1396,6 +1404,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + js-sha256@0.11.0: + resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1426,8 +1437,8 @@ packages: known-css-properties@0.35.0: resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} - less@4.2.0: - resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} + less@4.2.1: + resolution: {integrity: sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==} engines: {node: '>=6'} hasBin: true @@ -1439,20 +1450,20 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-html@4.1.4: - resolution: {integrity: sha512-6HA+YB++HrtPrSi+3gVEc0xdEGmqwLGErV0yagQta2bxfDOppjBHgadddpHWPR76LgB/Tiax22VfJtTqxkoijA==} + linkify-html@4.2.0: + resolution: {integrity: sha512-bVXuLiWmGwvlH95hq6q9DFGqTsQeFSGw/nHmvvjGMZv9T3GqkxuW2d2SOgk/a4DV2ajeS4c37EqlF16cjOj7GA==} peerDependencies: linkifyjs: ^4.0.0 - linkifyjs@4.1.4: - resolution: {integrity: sha512-0/NxkHNpiJ0k9VrYCkAn9OtU1eu8xEr1tCCpDtSsVRm/SF0xAak2Gzv3QimSfgUgqLBCDlfhMbu73XvaEHUTPQ==} + linkifyjs@4.2.0: + resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -1470,16 +1481,13 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - magic-string@0.30.13: - resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + magic-string@0.30.14: + resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} - md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} @@ -1538,8 +1546,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1706,14 +1714,14 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.2.8: - resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} + prettier-plugin-svelte@3.3.2: + resolution: {integrity: sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} hasBin: true @@ -1753,8 +1761,8 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.27.3: - resolution: {integrity: sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==} + rollup@4.28.1: + resolution: {integrity: sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1843,8 +1851,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.0.9: - resolution: {integrity: sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==} + svelte-autosize@1.1.5: + resolution: {integrity: sha512-whiND/GthFDG9Ansvil21qxmFhSMfuooVZPg40sbcLHYKR9srYhnfrP5qdw8MXHAm6DY9g5PawurOAWl34fK7g==} + peerDependencies: + svelte: '>=3.0.0' + + svelte-check@4.1.1: + resolution: {integrity: sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -1863,11 +1876,6 @@ packages: svelte-floating-ui@1.5.8: resolution: {integrity: sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==} - svelte-gravatar@1.0.3: - resolution: {integrity: sha512-CNxIV2lAuiqwdaPrGAM/BFj5U1dNNQXzeyh+HVi/48BODFXoDy0L1CMqYyvM+aKiF4ideZUBwT0S9/C1BeL5oA==} - peerDependencies: - svelte: '*' - svelte-hero-icons@5.2.0: resolution: {integrity: sha512-KpdMTL0bOnkxciEmDXvyVF/R5nrZ1x1uHCSt9gMrrbEd3g5HSIaaDChOutTOfeI+cZ3EJbb+OcBH/lBzJr1aEw==} engines: {node: '>=18.0.0'} @@ -1921,9 +1929,6 @@ packages: svelte-select@5.8.3: resolution: {integrity: sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA==} - svelte-waypoint@0.1.4: - resolution: {integrity: sha512-UEqoXZjJeKj2sWlAIsBOFjxjMn+KP8aFCc/zjdmZi1cCOE59z6T2C+I6ZaAf8EmNQqNzfZVB/Lci4Ci9spzXAw==} - svelte@5.8.1: resolution: {integrity: sha512-tqJY46Xoe+KiKvD4/guNlqpE+jco4IBcuaM6Ei9SEMETtsbLMfbak9XjTacqd6aGMmWXh7uFInfFTd4yES5r0A==} engines: {node: '>=18'} @@ -1933,8 +1938,8 @@ packages: peerDependencies: svelte: '>=3.49.0' - tailwindcss@3.4.15: - resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==} + tailwindcss@3.4.16: + resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==} engines: {node: '>=14.0.0'} hasBin: true @@ -1960,8 +1965,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - ts-api-utils@1.4.0: - resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' @@ -1979,8 +1984,8 @@ packages: type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true @@ -2215,27 +2220,29 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0(jiti@1.21.6))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.16.0(jiti@1.21.6))': dependencies: - eslint: 9.15.0(jiti@1.21.6) + eslint: 9.16.0(jiti@1.21.6) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.19.0': + '@eslint/config-array@0.19.1': dependencies: - '@eslint/object-schema': 2.1.4 - debug: 4.3.7 + '@eslint/object-schema': 2.1.5 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/core@0.9.0': {} + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -2246,11 +2253,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.15.0': {} + '@eslint/js@9.16.0': {} - '@eslint/object-schema@2.1.4': {} + '@eslint/object-schema@2.1.5': {} - '@eslint/plugin-kit@0.2.3': + '@eslint/plugin-kit@0.2.4': dependencies: levn: 0.4.1 @@ -2346,152 +2353,155 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@rollup/plugin-commonjs@28.0.1(rollup@4.27.3)': + '@rollup/plugin-commonjs@28.0.1(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.4.2(picomatch@4.0.2) is-reference: 1.2.1 - magic-string: 0.30.13 + magic-string: 0.30.14 picomatch: 4.0.2 optionalDependencies: - rollup: 4.27.3 + rollup: 4.28.1 - '@rollup/plugin-json@6.1.0(rollup@4.27.3)': + '@rollup/plugin-json@6.1.0(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) optionalDependencies: - rollup: 4.27.3 + rollup: 4.28.1 - '@rollup/plugin-node-resolve@15.3.0(rollup@4.27.3)': + '@rollup/plugin-node-resolve@15.3.0(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.8 optionalDependencies: - rollup: 4.27.3 + rollup: 4.28.1 - '@rollup/pluginutils@5.1.3(rollup@4.27.3)': + '@rollup/pluginutils@5.1.3(rollup@4.28.1)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.27.3 + rollup: 4.28.1 - '@rollup/rollup-android-arm-eabi@4.27.3': + '@rollup/rollup-android-arm-eabi@4.28.1': optional: true - '@rollup/rollup-android-arm64@4.27.3': + '@rollup/rollup-android-arm64@4.28.1': optional: true - '@rollup/rollup-darwin-arm64@4.27.3': + '@rollup/rollup-darwin-arm64@4.28.1': optional: true - '@rollup/rollup-darwin-x64@4.27.3': + '@rollup/rollup-darwin-x64@4.28.1': optional: true - '@rollup/rollup-freebsd-arm64@4.27.3': + '@rollup/rollup-freebsd-arm64@4.28.1': optional: true - '@rollup/rollup-freebsd-x64@4.27.3': + '@rollup/rollup-freebsd-x64@4.28.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.3': + '@rollup/rollup-linux-arm-musleabihf@4.28.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.3': + '@rollup/rollup-linux-arm64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.3': + '@rollup/rollup-linux-arm64-musl@4.28.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.3': + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.3': + '@rollup/rollup-linux-riscv64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.27.3': + '@rollup/rollup-linux-s390x-gnu@4.28.1': optional: true - '@rollup/rollup-linux-x64-musl@4.27.3': + '@rollup/rollup-linux-x64-gnu@4.28.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.27.3': + '@rollup/rollup-linux-x64-musl@4.28.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.27.3': + '@rollup/rollup-win32-arm64-msvc@4.28.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.27.3': + '@rollup/rollup-win32-ia32-msvc@4.28.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true '@steeze-ui/heroicons@2.4.2': {} - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))': dependencies: - '@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) + '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))': + '@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))': dependencies: - '@rollup/plugin-commonjs': 28.0.1(rollup@4.27.3) - '@rollup/plugin-json': 6.1.0(rollup@4.27.3) - '@rollup/plugin-node-resolve': 15.3.0(rollup@4.27.3) - '@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) - rollup: 4.27.3 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.28.1) + '@rollup/plugin-json': 6.1.0(rollup@4.28.1) + '@rollup/plugin-node-resolve': 15.3.0(rollup@4.28.1) + '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) + rollup: 4.28.1 - '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))': + '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))': dependencies: - '@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) + '@sveltejs/kit': 2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) - '@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0))': + '@sveltejs/kit@2.9.0(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) + '@sveltejs/vite-plugin-svelte': 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 esm-env: 1.2.1 import-meta-resolve: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.13 + magic-string: 0.30.14 mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.0 svelte: 5.8.1 tiny-glob: 0.2.9 - vite: 5.4.11(less@4.2.0) + vite: 5.4.11(less@4.2.1) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) - debug: 4.3.7 + '@sveltejs/vite-plugin-svelte': 4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) + debug: 4.4.0 svelte: 5.8.1 - vite: 5.4.11(less@4.2.0) + vite: 5.4.11(less@4.2.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0))': + '@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.0)))(svelte@5.8.1)(vite@5.4.11(less@4.2.0)) - debug: 4.3.7 + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.2(svelte@5.8.1)(vite@5.4.11(less@4.2.1)))(svelte@5.8.1)(vite@5.4.11(less@4.2.1)) + debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.13 + magic-string: 0.30.14 svelte: 5.8.1 - vite: 5.4.11(less@4.2.0) - vitefu: 1.0.4(vite@5.4.11(less@4.2.0)) + vite: 5.4.11(less@4.2.1) + vitefu: 1.0.4(vite@5.4.11(less@4.2.1)) transitivePeerDependencies: - supports-color @@ -2505,13 +2515,15 @@ snapshots: dependencies: intl-messageformat: 10.7.7 - '@tailwindcss/forms@0.5.9(tailwindcss@3.4.15)': + '@tailwindcss/forms@0.5.9(tailwindcss@3.4.16)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.15 + tailwindcss: 3.4.16 '@tsconfig/svelte@5.0.4': {} + '@types/autosize@4.0.3': {} + '@types/cookie@0.6.0': {} '@types/eslint@9.6.1': @@ -2527,86 +2539,90 @@ snapshots: '@types/resolve@1.20.2': {} - '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@types/sanitize-html@2.13.0': + dependencies: + htmlparser2: 8.0.2 + + '@typescript-eslint/eslint-plugin@8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.15.0 - '@typescript-eslint/type-utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) - '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.15.0 - eslint: 9.15.0(jiti@1.21.6) + '@typescript-eslint/parser': 8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/type-utils': 8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.17.0 + eslint: 9.16.0(jiti@1.21.6) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.0(typescript@5.6.3) + ts-api-utils: 1.4.3(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)': dependencies: - '@typescript-eslint/scope-manager': 8.15.0 - '@typescript-eslint/types': 8.15.0 - '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.15.0 - debug: 4.3.7 - eslint: 9.15.0(jiti@1.21.6) + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.17.0 + debug: 4.4.0 + eslint: 9.16.0(jiti@1.21.6) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.15.0': + '@typescript-eslint/scope-manager@8.17.0': dependencies: - '@typescript-eslint/types': 8.15.0 - '@typescript-eslint/visitor-keys': 8.15.0 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/visitor-keys': 8.17.0 - '@typescript-eslint/type-utils@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) - debug: 4.3.7 - eslint: 9.15.0(jiti@1.21.6) - ts-api-utils: 1.4.0(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2) + debug: 4.4.0 + eslint: 9.16.0(jiti@1.21.6) + ts-api-utils: 1.4.3(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.15.0': {} + '@typescript-eslint/types@8.17.0': {} - '@typescript-eslint/typescript-estree@8.15.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.17.0(typescript@5.7.2)': dependencies: - '@typescript-eslint/types': 8.15.0 - '@typescript-eslint/visitor-keys': 8.15.0 - debug: 4.3.7 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/visitor-keys': 8.17.0 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) + ts-api-utils: 1.4.3(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@typescript-eslint/utils@8.17.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0(jiti@1.21.6)) - '@typescript-eslint/scope-manager': 8.15.0 - '@typescript-eslint/types': 8.15.0 - '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.3) - eslint: 9.15.0(jiti@1.21.6) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@1.21.6)) + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + eslint: 9.16.0(jiti@1.21.6) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.15.0': + '@typescript-eslint/visitor-keys@8.17.0': dependencies: - '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/types': 8.17.0 eslint-visitor-keys: 4.2.0 '@zerodevx/svelte-toast@0.9.6(svelte@5.8.1)': @@ -2658,20 +2674,22 @@ snapshots: autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.24.2 - caniuse-lite: 1.0.30001680 + caniuse-lite: 1.0.30001687 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 postcss: 8.4.49 postcss-value-parser: 4.2.0 - axios-jwt@4.0.3(axios@1.7.7): + autosize@6.0.1: {} + + axios-jwt@4.0.3(axios@1.7.9): dependencies: - axios: 1.7.7 + axios: 1.7.9 jwt-decode: 3.1.2 ms: 3.0.0-canary.1 - axios@1.7.7: + axios@1.7.9: dependencies: follow-redirects: 1.15.9 form-data: 4.0.1 @@ -2700,8 +2718,8 @@ snapshots: browserslist@4.24.2: dependencies: - caniuse-lite: 1.0.30001680 - electron-to-chromium: 1.5.63 + caniuse-lite: 1.0.30001687 + electron-to-chromium: 1.5.71 node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) @@ -2709,15 +2727,13 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001680: {} + caniuse-lite@1.0.30001687: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - charenc@0.0.2: {} - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -2770,8 +2786,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypt@0.0.2: {} - css-selector-tokenizer@0.8.0: dependencies: cssesc: 3.0.0 @@ -2797,7 +2811,7 @@ snapshots: dayjs@1.11.13: {} - debug@4.3.7: + debug@4.4.0: dependencies: ms: 2.1.3 @@ -2833,9 +2847,9 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.63: {} + electron-to-chromium@1.5.71: {} - emoji-picker-element@1.24.0: {} + emoji-picker-element@1.25.0: {} emoji-regex@8.0.0: {} @@ -2929,21 +2943,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.15.0(jiti@1.21.6)): + eslint-compat-utils@0.5.1(eslint@9.16.0(jiti@1.21.6)): dependencies: - eslint: 9.15.0(jiti@1.21.6) + eslint: 9.16.0(jiti@1.21.6) semver: 7.6.3 - eslint-config-prettier@9.1.0(eslint@9.15.0(jiti@1.21.6)): + eslint-config-prettier@9.1.0(eslint@9.16.0(jiti@1.21.6)): dependencies: - eslint: 9.15.0(jiti@1.21.6) + eslint: 9.16.0(jiti@1.21.6) - eslint-plugin-svelte@2.46.0(eslint@9.15.0(jiti@1.21.6))(svelte@5.8.1): + eslint-plugin-svelte@2.46.1(eslint@9.16.0(jiti@1.21.6))(svelte@5.8.1): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0(jiti@1.21.6)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@1.21.6)) '@jridgewell/sourcemap-codec': 1.5.0 - eslint: 9.15.0(jiti@1.21.6) - eslint-compat-utils: 0.5.1(eslint@9.15.0(jiti@1.21.6)) + eslint: 9.16.0(jiti@1.21.6) + eslint-compat-utils: 0.5.1(eslint@9.16.0(jiti@1.21.6)) esutils: 2.0.3 known-css-properties: 0.35.0 postcss: 8.4.49 @@ -2971,15 +2985,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.15.0(jiti@1.21.6): + eslint@9.16.0(jiti@1.21.6): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0(jiti@1.21.6)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@1.21.6)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.0 - '@eslint/core': 0.9.0 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.15.0 - '@eslint/plugin-kit': 0.2.3 + '@eslint/js': 9.16.0 + '@eslint/plugin-kit': 0.2.4 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -2988,7 +3002,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7 + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -3196,8 +3210,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-buffer@1.1.6: {} - is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -3238,6 +3250,8 @@ snapshots: jiti@1.21.6: {} + js-sha256@0.11.0: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -3260,7 +3274,7 @@ snapshots: known-css-properties@0.35.0: {} - less@4.2.0: + less@4.2.1: dependencies: copy-anything: 2.0.6 parse-node-version: 1.0.1 @@ -3281,15 +3295,15 @@ snapshots: lilconfig@2.1.0: {} - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} - linkify-html@4.1.4(linkifyjs@4.1.4): + linkify-html@4.2.0(linkifyjs@4.2.0): dependencies: - linkifyjs: 4.1.4 + linkifyjs: 4.2.0 - linkifyjs@4.1.4: {} + linkifyjs@4.2.0: {} locate-character@3.0.0: {} @@ -3305,7 +3319,7 @@ snapshots: dependencies: es5-ext: 0.10.64 - magic-string@0.30.13: + magic-string@0.30.14: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3315,12 +3329,6 @@ snapshots: semver: 5.7.2 optional: true - md5@2.3.0: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - memoizee@0.4.17: dependencies: d: 1.0.2 @@ -3374,7 +3382,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.7: {} + nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -3468,7 +3476,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.4.49): dependencies: - lilconfig: 3.1.2 + lilconfig: 3.1.3 yaml: 2.6.1 optionalDependencies: postcss: 8.4.49 @@ -3495,18 +3503,18 @@ snapshots: postcss@8.4.49: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.1 source-map-js: 1.2.1 prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@5.8.1): + prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.8.1): dependencies: - prettier: 3.3.3 + prettier: 3.4.2 svelte: 5.8.1 - prettier@3.3.3: {} + prettier@3.4.2: {} proxy-from-env@1.1.0: {} @@ -3537,28 +3545,29 @@ snapshots: reusify@1.0.4: {} - rollup@4.27.3: + rollup@4.28.1: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.27.3 - '@rollup/rollup-android-arm64': 4.27.3 - '@rollup/rollup-darwin-arm64': 4.27.3 - '@rollup/rollup-darwin-x64': 4.27.3 - '@rollup/rollup-freebsd-arm64': 4.27.3 - '@rollup/rollup-freebsd-x64': 4.27.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.27.3 - '@rollup/rollup-linux-arm-musleabihf': 4.27.3 - '@rollup/rollup-linux-arm64-gnu': 4.27.3 - '@rollup/rollup-linux-arm64-musl': 4.27.3 - '@rollup/rollup-linux-powerpc64le-gnu': 4.27.3 - '@rollup/rollup-linux-riscv64-gnu': 4.27.3 - '@rollup/rollup-linux-s390x-gnu': 4.27.3 - '@rollup/rollup-linux-x64-gnu': 4.27.3 - '@rollup/rollup-linux-x64-musl': 4.27.3 - '@rollup/rollup-win32-arm64-msvc': 4.27.3 - '@rollup/rollup-win32-ia32-msvc': 4.27.3 - '@rollup/rollup-win32-x64-msvc': 4.27.3 + '@rollup/rollup-android-arm-eabi': 4.28.1 + '@rollup/rollup-android-arm64': 4.28.1 + '@rollup/rollup-darwin-arm64': 4.28.1 + '@rollup/rollup-darwin-x64': 4.28.1 + '@rollup/rollup-freebsd-arm64': 4.28.1 + '@rollup/rollup-freebsd-x64': 4.28.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.28.1 + '@rollup/rollup-linux-arm-musleabihf': 4.28.1 + '@rollup/rollup-linux-arm64-gnu': 4.28.1 + '@rollup/rollup-linux-arm64-musl': 4.28.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.28.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.28.1 + '@rollup/rollup-linux-riscv64-gnu': 4.28.1 + '@rollup/rollup-linux-s390x-gnu': 4.28.1 + '@rollup/rollup-linux-x64-gnu': 4.28.1 + '@rollup/rollup-linux-x64-musl': 4.28.1 + '@rollup/rollup-win32-arm64-msvc': 4.28.1 + '@rollup/rollup-win32-ia32-msvc': 4.28.1 + '@rollup/rollup-win32-x64-msvc': 4.28.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3648,7 +3657,13 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.0.9(picomatch@4.0.2)(svelte@5.8.1)(typescript@5.6.3): + svelte-autosize@1.1.5(svelte@5.8.1): + dependencies: + '@types/autosize': 4.0.3 + autosize: 6.0.1 + svelte: 5.8.1 + + svelte-check@4.1.1(picomatch@4.0.2)(svelte@5.8.1)(typescript@5.7.2): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.1 @@ -3656,7 +3671,7 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 svelte: 5.8.1 - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - picomatch @@ -3675,12 +3690,6 @@ snapshots: '@floating-ui/core': 1.6.8 '@floating-ui/dom': 1.6.12 - svelte-gravatar@1.0.3(svelte@5.8.1): - dependencies: - md5: 2.3.0 - svelte: 5.8.1 - svelte-waypoint: 0.1.4 - svelte-hero-icons@5.2.0(svelte@5.8.1): dependencies: '@steeze-ui/heroicons': 2.4.2 @@ -3697,21 +3706,19 @@ snapshots: svelte: 5.8.1 tiny-glob: 0.2.9 - svelte-preprocess@6.0.3(less@4.2.0)(postcss-load-config@4.0.2(postcss@8.4.49))(postcss@8.4.49)(svelte@5.8.1)(typescript@5.6.3): + svelte-preprocess@6.0.3(less@4.2.1)(postcss-load-config@4.0.2(postcss@8.4.49))(postcss@8.4.49)(svelte@5.8.1)(typescript@5.7.2): dependencies: svelte: 5.8.1 optionalDependencies: - less: 4.2.0 + less: 4.2.1 postcss: 8.4.49 postcss-load-config: 4.0.2(postcss@8.4.49) - typescript: 5.6.3 + typescript: 5.7.2 svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-waypoint@0.1.4: {} - svelte@5.8.1: dependencies: '@ampproject/remapping': 2.3.0 @@ -3725,7 +3732,7 @@ snapshots: esrap: 1.2.3 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.13 + magic-string: 0.30.14 zimmerframe: 1.1.2 sveltekit-i18n@2.4.2(svelte@5.8.1): @@ -3734,7 +3741,7 @@ snapshots: '@sveltekit-i18n/parser-default': 1.1.1 svelte: 5.8.1 - tailwindcss@3.4.15: + tailwindcss@3.4.16: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -3745,7 +3752,7 @@ snapshots: glob-parent: 6.0.2 is-glob: 4.0.3 jiti: 1.21.6 - lilconfig: 2.1.0 + lilconfig: 3.1.3 micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 @@ -3785,9 +3792,9 @@ snapshots: totalist@3.0.1: {} - ts-api-utils@1.4.0(typescript@5.6.3): + ts-api-utils@1.4.3(typescript@5.7.2): dependencies: - typescript: 5.6.3 + typescript: 5.7.2 ts-interface-checker@0.1.13: {} @@ -3799,7 +3806,7 @@ snapshots: type@2.7.3: {} - typescript@5.6.3: {} + typescript@5.7.2: {} update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: @@ -3813,18 +3820,18 @@ snapshots: util-deprecate@1.0.2: {} - vite@5.4.11(less@4.2.0): + vite@5.4.11(less@4.2.1): dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.27.3 + rollup: 4.28.1 optionalDependencies: fsevents: 2.3.3 - less: 4.2.0 + less: 4.2.1 - vitefu@1.0.4(vite@5.4.11(less@4.2.0)): + vitefu@1.0.4(vite@5.4.11(less@4.2.1)): optionalDependencies: - vite: 5.4.11(less@4.2.0) + vite: 5.4.11(less@4.2.1) which@2.0.2: dependencies: diff --git a/frontend/src/app.css b/frontend/src/app.css index 124f156456817ec77b5827a6d553136d341070f8..4368154fd9428955de870d0fb25e757022944fe6 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -3,7 +3,6 @@ @tailwind utilities; .button { - /*@apply bg-secondary text-white font-bold py-2 px-4 rounded hover:bg-secondaryHover hover:cursor-pointer disabled:border-secondary disabled:bg-transparent disabled:border-2 disabled:text-secondary disabled:hover:cursor-default;*/ @apply btn btn-primary; } diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 4850c241bfc53afe235f11b08416e4050016bf7f..cdfa4356b6cabedbb1a2b3037a047aa57615b6bb 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,12 +5,15 @@ declare global { // interface Error {} interface Locals { user: User?; - session: string?; + jwt: string?; locale: string; } // interface PageData {} // interface PageState {} // interface Platform {} + interface FormData { + message: string; + } } } diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index f5b9b640ef1951176f4898ffc2ea8d7a57c3d0e3..1d4d7ef276af03dc34685951d50613380d57b6be 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,21 +1,42 @@ -import type { Handle } from '@sveltejs/kit'; +import { type Handle, type RequestEvent } from '@sveltejs/kit'; import { jwtDecode } from 'jwt-decode'; import { type JWTContent } from '$lib/utils/login'; -import { getUserAPI } from '$lib/api/users'; -import User from '$lib/types/user'; -import { access_cookie } from '$lib/api/apiInstance'; + +const API_BASE_URL = 'http://127.0.0.1:8000/api/v1'; +const PROXY_PATH = '/api'; + +const handleApiProxy = async (event: RequestEvent, cookies: { name: string; value: string }[]) => { + const strippedPath = event.url.pathname.substring(PROXY_PATH.length); + + const urlPath = `${API_BASE_URL}${strippedPath}${event.url.search}`; + const proxiedUrl = new URL(urlPath); + + event.request.headers.delete('connection'); + event.request.headers.set('cookie', cookies.map((c) => `${c.name}=${c.value}`).join('; ')); + + return event.fetch(proxiedUrl.toString(), event.request).catch((err: any) => { + console.log('Could not proxy API request: ', err); + throw err; + }); +}; export const handle: Handle = async ({ event, resolve }) => { event.locals.user = null; - event.locals.session = null; + event.locals.jwt = null; event.locals.locale = 'fr'; - const session = event.cookies.get('access_token_cookie'); - if (!session) { + const cookies = event.cookies.getAll(); + + if (event.url.pathname.startsWith(PROXY_PATH)) { + return await handleApiProxy(event, cookies); + } + + const jwt = event.cookies.get('access_token_cookie'); + if (!jwt) { return resolve(event); } - const decoded = jwtDecode<JWTContent>(session); + const decoded = jwtDecode<JWTContent>(jwt); if (!decoded) { return resolve(event); } @@ -25,8 +46,12 @@ export const handle: Handle = async ({ event, resolve }) => { return resolve(event); } - access_cookie.set(session); - const user = User.parse(await getUserAPI(id)); + const response = await event.fetch(`/api/users/${id}`); + if (!response.ok) { + return resolve(event); + } + + const user = await response.json(); if (!user) { return resolve(event); } @@ -34,8 +59,8 @@ export const handle: Handle = async ({ event, resolve }) => { const localeCookie = event.cookies.get('locale'); const initLocale = localeCookie || event.locals.locale; - event.locals.user = user.toJson(); - event.locals.session = session; + event.locals.user = user; + event.locals.jwt = jwt; event.locals.locale = initLocale; return resolve(event); }; diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index e090487ee4cd13082d44103044b14e6e2f2ed8c9..5385dd1364e6adcae9696f527d18e8e93270b3cf 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -43,7 +43,9 @@ "bookingSuccessful": "Session réservée avec succès", "bookingFailed": "Erreur lors de la réservation de la session", "noCurrentOrFutureSessions": "Aucune session en cours ou planifiée", - "noSessions": "Aucune session" + "noSessions": "Aucune session", + "noContact": "Ajoutez un contact pour commencer", + "newFirstContact": "Ajouter un premier contact" }, "login": { "email": "E-mail", @@ -259,10 +261,10 @@ "title": "Questionnaire hebdomadaire", "description": "Au cours des 7 derniers jours...", "questions": [ - "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...) ?" + "Combien d'heures de <b>cours</b> de {lang} avez vous suivies ?", + "Combien d'heures avez-vous <b>regardé des vidéos</b> en {lang} (films, séries, Youtube...) ou <b>écouté des contenus</b> en {lang} (podcasts, radio, cours universitaires...) ?", + "Combien d'heures avez-vous <b>lu des textes</b> en {lang} (livre, journal, BD, sites web...) ?", + "Combien d'heures avez-vous <b>parlé</b> en {lang} (discussions avec amis, famille, collègues...) ?" ], "answers": { "placeholder": "", @@ -281,14 +283,24 @@ }, "errors": { "null": "Veuillez répondre à toutes les questions", - "submit": "Erreur lors de l'envoi du questionnaire" + "submit": "Erreur lors de l'envoi du questionnaire", + "toggle": "Erreur lors de l'activation ou de la désactivation de la session" }, "success": "Questionnaire envoyé, merci !" } }, "downloadAllMessages": "Télécharger toutes les conversations", "downloadAllMetadata": "Télécharger toutes les métadonnées", - "downloadAllFeedbacks": "Télécharger tous les feedbacks" + "downloadAllFeedbacks": "Télécharger tous les feedbacks", + "errors": { + "create": "Erreur lors de la création de la session", + "delete": "Erreur lors de la suppression de la session", + "addUser": "Erreur lors de l'ajout d'un utilisateur à la session", + "removeUser": "Erreur lors de la suppression d'un utilisateur de la session", + "presence": "Erreur lors de l'envoi de la présence", + "typing": "Erreur lors de l'envoi de l'indicateur de saisie" + }, + "noTopic": "Aucun topic disponible" }, "button": { "create": "Créer", @@ -378,7 +390,8 @@ "date": "Date", "programed": "Programmée", "inProgress": "En cours", - "finished": "Terminée" + "finished": "Terminée", + "topics": "Topics" } }, "inputs": { diff --git a/frontend/src/lib/api/apiInstance.ts b/frontend/src/lib/api/apiInstance.ts deleted file mode 100644 index 2053a846812acc86bf5554e370cba59dd07e97ce..0000000000000000000000000000000000000000 --- a/frontend/src/lib/api/apiInstance.ts +++ /dev/null @@ -1,18 +0,0 @@ -import axios from 'axios'; -import config from '$lib/config'; -import { writable, get } from 'svelte/store'; - -export const access_cookie = writable(''); - -export const axiosPublicInstance = axios.create({ - ...axios.defaults, - baseURL: config.API_URL, - withCredentials: true, - validateStatus: () => true, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${get(access_cookie)}` - } -}); - -export const axiosInstance = axiosPublicInstance; diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts deleted file mode 100644 index 5d3ba02dbac044a628f5b471498b113b0651f6b6..0000000000000000000000000000000000000000 --- a/frontend/src/lib/api/auth.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { axiosPublicInstance } from './apiInstance'; - -export async function loginAPI(email: string, password: string): Promise<string> { - return axiosPublicInstance - .post( - `/auth/login`, - { - email, - username: email, - password - }, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - } - ) - .then((response) => { - if (response.status === 401) { - return response.data.detail ?? 'Unauthorized'; - } else if (response.status === 422) { - return 'Invalid request'; - } else if (response.status === 200) { - return 'OK'; - } - - return 'Unknown error occurred: ' + response.status; - }) - .catch((error) => { - return error.toString(); - }); -} - -export async function registerAPI( - email: string, - password: string, - nickname: string, - tutor: boolean = false -): Promise<string> { - return axiosPublicInstance - .post( - `/auth/register`, - { - email, - username: email, - password, - nickname, - tutor - }, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - } - ) - .then((response) => { - if (response.status === 401) { - return response.data.detail ?? 'Unauthorized'; - } else if (response.status === 422) { - return 'Invalid request'; - } else if (response.status === 201) { - return 'OK'; - } - - return 'Error ' + response.status + ': ' + response.data.detail; - }) - .catch((error) => { - return error.toString(); - }); -} diff --git a/frontend/src/lib/api/sessions.ts b/frontend/src/lib/api/sessions.ts index c49b667d7a231788eb2378c9d7f9cf3fb72c4333..9b861150861266e6cba1ed36cab501dbf28c023e 100644 --- a/frontend/src/lib/api/sessions.ts +++ b/frontend/src/lib/api/sessions.ts @@ -1,165 +1,195 @@ import { formatToUTCDate } from '$lib/utils/date'; -import { toastAlert } from '$lib/utils/toasts'; -import { axiosInstance } from './apiInstance'; +import type { fetchType } from '$lib/utils/types'; -export async function getSessionsAPI() { - const response = await axiosInstance.get(`/sessions`); +export async function getSessionsAPI(fetch: fetchType): Promise<any[]> { + const response = await fetch(`/api/sessions`); + if (!response.ok) return []; - return response.data; + return await response.json(); } -export async function createSessionAPI() { - const response = await axiosInstance.post(`/sessions`); +export async function createSessionAPI(fetch: fetchType): Promise<any | null> { + const response = await fetch(`/api/sessions`, { method: 'POST' }); + if (!response.ok) return null; - if (response.status !== 200) { - toastAlert('Failed to create session'); - } + return await response.json(); } -export async function getSessionAPI(id: number) { - const response = await axiosInstance.get(`/sessions/${id}`); +export async function getSessionAPI(fetch: fetchType, id: number): Promise<any | null> { + const response = await fetch(`/api/sessions/${id}`); + if (!response.ok) return null; - if (response.status !== 200) { - toastAlert('Failed to get session'); - return null; - } - - return response.data; + return await response.json(); } -export async function deleteSessionAPI(id: number) { - const response = await axiosInstance.delete(`/sessions/${id}`); +export async function deleteSessionAPI(fetch: fetchType, id: number): Promise<boolean> { + const response = await fetch(`/api/sessions/${id}`, { method: 'DELETE' }); + if (!response.ok) return false; - if (response.status !== 204) { - toastAlert('Failed to delete session'); - } + return true; } -export async function getMessagesAPI(id: number) { - const response = await axiosInstance.get(`/sessions/${id}/messages`); +export async function getMessagesAPI(fetch: fetchType, id: number): Promise<any | null> { + const response = await fetch(`/api/sessions/${id}/messages`); + if (!response.ok) return null; - return response.data; + return await response.json(); } export async function createMessageAPI( + fetch: fetchType, id: number, content: string, metadata: { message: string; date: number }[], replyTo: number | null ): Promise<any | null> { - const response = await axiosInstance.post(`/sessions/${id}/messages`, { - content, - metadata, - reply_to_message_id: replyTo + const response = await fetch(`/api/sessions/${id}/messages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, metadata, reply_to_message_id: replyTo }) }); + if (!response.ok) return null; - if (response.status !== 201) { - toastAlert('Failed to send message'); - return null; - } - - return response.data; + return await response.json(); } export async function updateMessageAPI( + fetch: fetchType, id: number, message_id: string, content: string, metadata: { message: string; date: number }[] -): Promise<number | null> { - const response = await axiosInstance.post(`/sessions/${id}/messages`, { - message_id, - content, - metadata +): Promise<any | null> { + const response = await fetch(`/api/sessions/${id}/messages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message_id, content, metadata }) }); + if (!response.ok) return null; - if (response.status !== 201) { - toastAlert('Failed to update message'); - return null; - } - - return response.data; + return await response.json(); } export async function createMessageFeedbackAPI( + fetch: fetchType, id: number, message_id: number, start: number, end: number, content: string | null -): Promise<number> { - const response = await axiosInstance.post(`/sessions/${id}/messages/${message_id}/feedback`, { - start, - end, - content +): Promise<number | null> { + const response = await fetch(`/api/sessions/${id}/messages/${message_id}/feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ start, end, content }) }); - if (response.status !== 201) { - toastAlert('Failed to add feedback'); - return -1; - } - return response.data; + if (!response.ok) return null; + + return parseInt(await response.text()); } export async function deleteMessageFeedbackAPI( + fetch: fetchType, id: number, message_id: number, feedback_id: number -) { - const response = await axiosInstance.delete( - `/sessions/${id}/messages/${message_id}/feedback/${feedback_id}` +): Promise<boolean> { + const response = await fetch( + `/api/sessions/${id}/messages/${message_id}/feedback/${feedback_id}`, + { + method: 'DELETE' + } ); - if (response.status !== 204) { - toastAlert('Failed to delete feedback'); - return false; - } - return true; -} -export async function patchLanguageAPI(id: number, language: string) { - const response = await axiosInstance.patch(`/sessions/${id}`, { language }); + return response.ok; +} - if (response.status !== 204) { - toastAlert('Failed to change language'); - return false; - } +export async function patchLanguageAPI( + fetch: fetchType, + id: number, + language: string +): Promise<boolean> { + const response = await fetch(`/api/sessions/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ language }) + }); - return true; + return response.ok; } export async function createSessionSatisfyAPI( + fetch: fetchType, id: number, usefullness: number, easiness: number, remarks: string ): Promise<boolean> { - const response = await axiosInstance.post(`/sessions/${id}/satisfy`, { - usefullness, - easiness, - remarks + const response = await fetch(`/api/sessions/${id}/satisfy`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ usefullness, easiness, remarks }) }); - if (response.status !== 204) { - toastAlert('Failed to satisfy session'); - return false; - } - - return true; + return response.ok; } export async function createSessionFromCalComAPI( + fetch: fetchType, user_id: number, contact_id: number, start: Date, end: Date ): Promise<number | null> { - const response = await axiosInstance.post(`/users/${user_id}/contacts/${contact_id}/bookings`, { - start_time: formatToUTCDate(start), - end_time: formatToUTCDate(end) + const response = await fetch(`/api/users/${user_id}/contacts/${contact_id}/bookings`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + start_time: formatToUTCDate(start), + end_time: formatToUTCDate(end) + }) + }); + if (!response.ok) return null; + + return await response.json(); +} + +export async function addUserToSessionAPI( + fetch: fetchType, + session_id: number, + user_id: number +): Promise<boolean> { + const response = await fetch(`/api/sessions/${session_id}/users/${user_id}`, { method: 'POST' }); + + return response.ok; +} + +export async function patchSessionAPI(fetch: fetchType, id: number, data: any): Promise<boolean> { + const response = await fetch(`/api/sessions/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) }); - if (response.status !== 201) { - toastAlert('Failed to create cal.com session'); - return null; - } + return response.ok; +} + +export async function sendTypingAPI(fetch: fetchType, id: number): Promise<boolean> { + const response = await fetch(`/api/sessions/${id}/typing`, { method: 'POST' }); + return response.ok; +} - return response.data; +export async function sendPresenceAPI(fetch: fetchType, id: number): Promise<boolean> { + const response = await fetch(`/api/sessions/${id}/presence`, { method: 'POST' }); + return response.ok; +} + +export async function removeUserFromSessionAPI( + fetch: fetchType, + session_id: number, + user_id: number +): Promise<boolean> { + const response = await fetch(`/api/sessions/${session_id}/users/${user_id}`, { + method: 'DELETE' + }); + return response.ok; } diff --git a/frontend/src/lib/api/survey.ts b/frontend/src/lib/api/survey.ts index a4f3c956e22ab43af33859fa6c9752fe5cc626e5..52b17a7f00a2c14ecca744aff23e2faa4696784f 100644 --- a/frontend/src/lib/api/survey.ts +++ b/frontend/src/lib/api/survey.ts @@ -1,18 +1,14 @@ -import { toastAlert } from '$lib/utils/toasts'; -import { axiosInstance } from './apiInstance'; +import type { fetchType } from '$lib/utils/types'; -export async function getSurveyAPI(survey_id: number) { - const response = await axiosInstance.get(`/surveys/${survey_id}`); +export async function getSurveyAPI(fetch: fetchType, survey_id: number) { + const response = await fetch(`/api/surveys/${survey_id}`); + if (!response.ok) return null; - if (response.status !== 200) { - toastAlert('Failed to get survey'); - return null; - } - - return response.data; + return await response.json(); } export async function sendSurveyResponseAPI( + fetch: fetchType, code: string, sid: string, uid: number | null, @@ -23,36 +19,34 @@ export async function sendSurveyResponseAPI( response_time: number, text: string = '' ) { - const response = await axiosInstance.post(`/surveys/responses`, { - code, - sid, - uid, - survey_id, - question_id, - group_id, - selected_id: option_id, - response_time, - text + const response = await fetch(`/api/surveys/responses`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code, + sid, + uid, + survey_id, + question_id, + group_id, + selected_id: option_id, + response_time, + text + }) }); - if (response.status !== 201) { - toastAlert('Failed to send survey response'); - return false; - } - - return true; + return response.ok; } -export async function getSurveyScoreAPI(survey_id: number, sid: string) { - const response = await axiosInstance.get(`/surveys/${survey_id}/score/${sid}`); - if (response.status !== 200) { - toastAlert('Failed to retrieve survey score'); - return null; - } - return response.data; +export async function getSurveyScoreAPI(fetch: fetchType, survey_id: number, sid: string) { + const response = await fetch(`/api/surveys/${survey_id}/score/${sid}`); + if (!response.ok) return null; + + return await response.json(); } export async function sendSurveyResponseInfoAPI( + fetch: fetchType, survey_id: number, sid: string, birthyear: number, @@ -60,18 +54,17 @@ export async function sendSurveyResponseInfoAPI( primary_language: string, education: string ) { - const response = await axiosInstance.post(`/surveys/info/${survey_id}`, { - sid, - birthyear, - gender, - primary_language, - education + const response = await fetch(`/api/surveys/info/${survey_id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sid, + birthyear, + gender, + primary_language, + education + }) }); - if (response.status !== 201) { - toastAlert('Failed to send survey response info'); - return false; - } - - return true; + return response.ok; } diff --git a/frontend/src/lib/api/tests.ts b/frontend/src/lib/api/tests.ts index f95de8df0bb4e3569162c8a4e72233d332972a5d..f0019c47f651f4beaa74893563d25b623fbc55e4 100644 --- a/frontend/src/lib/api/tests.ts +++ b/frontend/src/lib/api/tests.ts @@ -1,12 +1,9 @@ -import { axiosInstance } from './apiInstance'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function sendTestVocabularyAPI(data: any): Promise<boolean> { - const response = await axiosInstance.post(`/tests/vocabulary`, { content: data }); - - if (response.status !== 201) { - return false; - } + const response = await fetch(`/api/tests/vocabulary`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); - return true; + return response.ok; } diff --git a/frontend/src/lib/api/users.ts b/frontend/src/lib/api/users.ts index 0d6a1bbc3a9bd705cb2f71896518e8bcab527329..19f124d7787d07e724092ebe50159898a7f00aad 100644 --- a/frontend/src/lib/api/users.ts +++ b/frontend/src/lib/api/users.ts @@ -1,168 +1,118 @@ -import { toastAlert } from '$lib/utils/toasts'; -import { axiosInstance, access_cookie } from './apiInstance'; -import { get } from 'svelte/store'; +import type { fetchType } from '$lib/utils/types'; -export async function getUsersAPI() { - const response = await axiosInstance.get(`/users`); +export async function getUsersAPI(fetch: fetchType): Promise<any[]> { + const response = await fetch(`/api/users`); + if (!response.ok) return []; - if (response.status !== 200) { - toastAlert('Failed to get users'); - return []; - } - - return response.data; + return await response.json(); } -export async function getUserAPI(user_id: number) { - const response = await axiosInstance.get(`/users/${user_id}`, { - headers: { - Authorization: `Bearer ${get(access_cookie)}` - } - }); - - if (response.status !== 200) { - toastAlert('Failed to get user'); - return null; - } +export async function getUserAPI(fetch: fetchType, user_id: number): Promise<any | null> { + const response = await fetch(`/api/users/${user_id}`); + if (!response.ok) return null; - return response.data; + return await response.json(); } -export async function createUserContactAPI(user_id: number, contact_id: number) { - const response = await axiosInstance.post(`/users/${user_id}/contacts/${contact_id}`); - - if (response.status !== 201) { - toastAlert('Failed to create user contact'); - return null; - } +export async function createUserContactAPI( + fetch: fetchType, + user_id: number, + contact_id: number +): Promise<any | null> { + const response = await fetch(`/api/users/${user_id}/contacts/${contact_id}`); + if (!response.ok) return null; - return response.data; + return await response.json(); } -export async function createUserContactFromEmailAPI(user_id: number, email: string) { - const response = await axiosInstance.post(`/users/${user_id}/contacts-email/${email}`); - - if (response.status === 404) { - toastAlert('User not found'); - return null; - } - - if (response.status === 400) { - toastAlert('User already has this contact'); - return null; - } - - if (response.status !== 201) { - toastAlert('Failed to create user contact'); - return null; - } +export async function createUserContactFromEmailAPI( + fetch: fetchType, + user_id: number, + email: string +): Promise<any | null> { + const response = await fetch(`/api/users/${user_id}/contacts-email/${email}`, { method: 'POST' }); + if (!response.ok) return null; - return response.data; + return await response.json(); } -export async function getUserContactsAPI(user_id: number) { - const response = await axiosInstance.get(`/users/${user_id}/contacts`, { - headers: { - Authorization: `Bearer ${get(access_cookie)}` - } - }); - - if (response.status !== 200) { - toastAlert('Failed to get user contacts'); - return []; - } +export async function getUserContactsAPI(fetch: fetchType, user_id: number): Promise<any[]> { + const response = await fetch(`/api/users/${user_id}/contacts`); + if (!response.ok) return []; - return response.data; + return await response.json(); } -export async function getUserContactSessionsAPI(user_id: number, contact_id: number) { - const response = await axiosInstance.get(`/users/${user_id}/contacts/${contact_id}/sessions`, { - headers: { - Authorization: `Bearer ${get(access_cookie)}` - } - }); - - if (response.status !== 200) { - toastAlert('Failed to get user contact sessions'); - return []; - } +export async function getUserContactSessionsAPI( + fetch: fetchType, + user_id: number, + contact_id: number +): Promise<any[]> { + const response = await fetch(`/api/users/${user_id}/contacts/${contact_id}/sessions`); + if (!response.ok) return []; - return response.data; + return await response.json(); } export async function createUserAPI( + fetch: fetchType, nickname: string, email: string, password: string, type: number, is_active: boolean ): Promise<number | null> { - const response = await axiosInstance.post(`/users`, { - nickname, - email, - password, - type, - is_active + const response = await fetch(`/api/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname, email, password, type, is_active }) }); - if (response.status !== 201) { - toastAlert('Failed to create user'); - return null; - } + if (!response.ok) return null; - return response.data; + return parseInt(await response.text()); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function patchUserAPI(user_id: number, data: any): Promise<boolean> { - try { - const response = await axiosInstance.patch(`/users/${user_id}`, data); - - if (response.status !== 204) { - toastAlert('Failed to update user'); - return false; - } - - return true; - } catch (e) { - console.error(e); - toastAlert('Failed to update user due to unknown error'); - return false; - } +export async function patchUserAPI(fetch: fetchType, user_id: number, data: any) { + const response = await fetch(`/api/users/${user_id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return response.ok; } export async function createTestTypingAPI( + fetch: fetchType, user_id: number, entries: typingEntry[] ): Promise<number | null> { - const response = await axiosInstance.post(`/users/${user_id}/tests/typing`, { - entries + const response = await fetch(`/api/users/${user_id}/tests/typing`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries }) }); - if (response.status !== 201) { - toastAlert('Failed to create test'); - return null; - } + if (!response.ok) return null; - return response.data; + return parseInt(await response.text()); } export async function createWeeklySurveyAPI( + fetch: fetchType, user_id: number, q1: number, q2: number, q3: number, q4: number ): Promise<number | null> { - const response = await axiosInstance.post(`/users/${user_id}/surveys/weekly`, { - q1, - q2, - q3, - q4 + const response = await fetch(`/api/users/${user_id}/surveys/weekly`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ q1, q2, q3, q4 }) }); - if (response.status !== 201) { - toastAlert('Failed to create weekly survey'); - return null; - } - return response.data; + + if (!response.ok) return null; + + return parseInt(await response.text()); } diff --git a/frontend/src/lib/components/sessions/emojiPicker.svelte b/frontend/src/lib/components/sessions/emojiPicker.svelte deleted file mode 100644 index daebc8809febe54805825c2235fa88ca327b63a2..0000000000000000000000000000000000000000 --- a/frontend/src/lib/components/sessions/emojiPicker.svelte +++ /dev/null @@ -1,27 +0,0 @@ -<script lang="ts"> - import { EmojiButton } from '@joeattardi/emoji-button'; - import { createEventDispatcher } from 'svelte'; - import { FaceSmile, Icon } from 'svelte-hero-icons'; - - let class_: string = ''; - export { class_ as class }; - - const dispatch = createEventDispatcher(); - - const picker = new EmojiButton({ autoHide: false }); - let trigger: HTMLButtonElement; - - picker.on('emoji', (selection) => { - dispatch('change', selection); - }); - - function togglePicker() { - picker.togglePicker(trigger); - } -</script> - -<button bind:this={trigger} class={class_} on:click={togglePicker}> - <kbd class="kbd"> - <Icon src={FaceSmile} class="w-6" /> - </kbd> -</button> diff --git a/frontend/src/lib/components/tests/typingbox.svelte b/frontend/src/lib/components/tests/typingbox.svelte index fd6b5d5560dfd2b04b4a3cacfeed0360584a6918..b6e8de1129996159a89be22b015ad5914bb19b60 100644 --- a/frontend/src/lib/components/tests/typingbox.svelte +++ b/frontend/src/lib/components/tests/typingbox.svelte @@ -102,7 +102,7 @@ bind:this={textArea} spellcheck="false" disabled={isDone} - on:keyup={(e) => { + on:keyup={() => { if (inProgress) { data[data.length - 1].uptime = new Date().getTime() - startTime; } @@ -134,6 +134,6 @@ } }} class="absolute top-0 resize-none font-mono p-4 w-full h-full bg-transparent select-none text-transparent" - /> + ></textarea> </div> </div> diff --git a/frontend/src/lib/components/tests/typingtest.svelte b/frontend/src/lib/components/tests/typingtest.svelte index 4f79c9ab0ec199d5c93066e844e481b8b9a92e96..103aba2f0d8976d05d740723a9051a73392d2d58 100644 --- a/frontend/src/lib/components/tests/typingtest.svelte +++ b/frontend/src/lib/components/tests/typingtest.svelte @@ -3,16 +3,15 @@ import Typingbox from '$lib/components/tests/typingbox.svelte'; import { get } from 'svelte/store'; import { createTestTypingAPI } from '$lib/api/users'; - import { user } from '$lib/types/user'; - import { toastAlert } from '$lib/utils/toasts'; + import type User from '$lib/types/user'; - export let onFinish: Function; + let { user, onFinish }: { user: User; onFinish: Function } = $props(); - let data: typingEntry[] = []; + let data: typingEntry[] = $state([]); - $: currentExercice = 0; + let currentExercice = $state(0); - let inProgress = false; + let inProgress = $state(false); let exercices = [ { @@ -33,14 +32,7 @@ ]; async function submit() { - const user_id = $user?.id; - - if (!user_id) { - toastAlert('Failed to get user'); - return; - } - - const res = await createTestTypingAPI(user_id, data); + const res = await createTestTypingAPI(fetch, user.id, data); if (!res) return; @@ -71,7 +63,7 @@ {#if currentExercice < exercices.length - 1} <button class="button m-auto" - on:click={() => { + onclick={() => { currentExercice++; inProgress = false; }} @@ -80,7 +72,7 @@ {$t('button.next')} </button> {:else} - <button class="button m-auto" disabled={inProgress} on:click={submit} + <button class="button m-auto" disabled={inProgress} onclick={submit} >{$t('button.submit')}</button > {/if} diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 10b3cf95a5b512c8e378e20cc4c96d3534b14573..ca3b4dd92665f41385f9f1ca930157bf3db87ca8 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,5 +1,5 @@ export default { - API_URL: import.meta.env.VITE_API_URL || 'https://languagelab.sipr.ucl.ac.be/api/v1', + API_URL: import.meta.env.VITE_API_URL || 'https://languagelab.sipr.ucl.ac.be/api', API_PROXY: import.meta.env.VITE_API_PROXY || 'https://languagelab.sipr.ucl.ac.be:8000', APP_URL: import.meta.env.VITE_APP_URL || 'https://languagelab.sipr.ucl.ac.be', WS_URL: import.meta.env.VITE_WS_URL || 'wss://languagelab.sipr.ucl.ac.be/api/v1/ws', diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6c38aec1085db88189bcf492dbb49a1c45..0000000000000000000000000000000000000000 --- a/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/services/auth.ts b/frontend/src/lib/services/auth.ts deleted file mode 100644 index eaab6fd477b62672c0305b0fd6648ad261059ce9..0000000000000000000000000000000000000000 --- a/frontend/src/lib/services/auth.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type ServerLoad, redirect } from '@sveltejs/kit'; -import { loadTranslations } from '$lib/services/i18n'; - -export const requireLogin: ServerLoad = async ({ params, url, cookies }) => { - const initLocale = params.locale || 'fr'; - const { pathname } = url; - - await loadTranslations(initLocale, pathname); - - const session = cookies.get('token'); - - if (!session) { - redirect(302, `/${initLocale}/login`); - } -}; - -export const getLogin: ServerLoad = async ({ cookies }) => { - const session = cookies.get('token'); - - if (!session) { - return null; - } -}; diff --git a/frontend/src/lib/types/feedback.ts b/frontend/src/lib/types/feedback.ts index bfebd112f949b9aad89bc9a969a2313b499e6634..708256717809435ab0f54b5c6432e40b6381a5ea 100644 --- a/frontend/src/lib/types/feedback.ts +++ b/frontend/src/lib/types/feedback.ts @@ -83,7 +83,12 @@ export default class Feedback { } async delete(): Promise<boolean> { - return await deleteMessageFeedbackAPI(this._message.session.id, this._message.id, this._id); + return await deleteMessageFeedbackAPI( + fetch, + this._message.session.id, + this._message.id, + this._id + ); } static parseAll(json: any, message: Message): Feedback[] { diff --git a/frontend/src/lib/types/message.ts b/frontend/src/lib/types/message.ts index 12ee21756174cff581c03e9e1da438a6e513f098..3e7bcd1f4cc2f46aff3e36111caea7d46c4b638b 100644 --- a/frontend/src/lib/types/message.ts +++ b/frontend/src/lib/types/message.ts @@ -16,7 +16,7 @@ export default class Message { private _edited: boolean = false; private _versions = writable([] as { content: string; date: Date }[]); private _feedbacks = writable([] as Feedback[]); - private _replyTo: string; + private _replyTo: number; public constructor( id: number, @@ -25,7 +25,7 @@ export default class Message { created_at: Date, user: User, session: Session, - replyTo: string + replyTo: number ) { this._id = id; this._message_id = message_id; @@ -77,8 +77,26 @@ export default class Message { return `message-${this._message_id}`; } + get replyTo(): number { + return this._replyTo; + } + + get replyToMessage(): Message | undefined { + if (this._replyTo == null) return undefined; + + return get(this._session.messages).find( + (m) => m instanceof Message && m.id == this._replyTo + ) as Message | undefined; + } + async update(content: string, metadata: { message: string; date: number }[]): Promise<boolean> { - const response = await updateMessageAPI(this._session.id, this._message_id, content, metadata); + const response = await updateMessageAPI( + fetch, + this._session.id, + this._message_id, + content, + metadata + ); if (response == null || response.id == null) return false; this._versions.update((v) => [...v, { content: content, date: new Date() }]); @@ -91,7 +109,7 @@ export default class Message { async getMessageById(id: number): Promise<Message | null> { try { - const response = await getMessagesAPI(this._session.id); // Fetch all messages for the session + const response = await getMessagesAPI(fetch, this._session.id); // Fetch all messages for the session if (!response) { toastAlert('Failed to retrieve messages from the server.'); return null; @@ -132,13 +150,17 @@ export default class Message { async addFeedback(start: number, end: number, content: string | null = null): Promise<boolean> { const response = await createMessageFeedbackAPI( + fetch, this._session.id, this._id, start, end, content ); - if (response == -1) return false; + if (!response) { + toastAlert('Failed to create feedback'); + return false; + } const feedback = new Feedback(response, this, start, end, content); this.localFeedback(feedback); diff --git a/frontend/src/lib/types/session.ts b/frontend/src/lib/types/session.ts index d7b465fab3729a0624154336683410c6db34a4db..04cfb8fd773275fea2815161d0898059f0db8ac9 100644 --- a/frontend/src/lib/types/session.ts +++ b/frontend/src/lib/types/session.ts @@ -1,17 +1,25 @@ import { toastAlert } from '$lib/utils/toasts'; import { get, writable, type Writable } from 'svelte/store'; -import User, { user } from './user'; -import { axiosInstance } from '$lib/api/apiInstance'; +import User from './user'; import { + addUserToSessionAPI, createMessageAPI, + createSessionAPI, createSessionSatisfyAPI, + deleteSessionAPI, getMessagesAPI, - patchLanguageAPI + patchLanguageAPI, + patchSessionAPI, + removeUserFromSessionAPI, + sendPresenceAPI, + sendTypingAPI } from '$lib/api/sessions'; import Message from './message'; import config from '$lib/config'; import Feedback from './feedback'; import { parseToLocalDate } from '$lib/utils/date'; +import { t } from '$lib/services/i18n'; +import type { fetchType } from '$lib/utils/types'; const { subscribe, set, update } = writable<Session[]>([]); @@ -40,6 +48,7 @@ export default class Session { private _onlineUsers: Writable<Set<number>> = writable(new Set()); private _onlineTimers: Map<number, number> = new Map(); private _length: number; + private _user: User | null = null; private constructor( id: number, @@ -118,7 +127,7 @@ export default class Session { usersList(maxLength = 30): string { const users = this._users - .filter((u) => u.id != get(user)?.id) + .filter((u) => u.id != this._user?.id) .map((user) => user.nickname) .join(', '); if (users.length < maxLength) { @@ -129,7 +138,7 @@ export default class Session { otherUsersList(maxLength = 30): string { const users = this._users - .filter((u) => u.id != get(user)?.id) + .filter((u) => u.id != this._user?.id) .map((user) => user.nickname) .join(', '); if (users.length < maxLength) { @@ -139,10 +148,9 @@ export default class Session { } async delete(): Promise<boolean> { - const response = await axiosInstance.delete(`/sessions/${this.id}`); - - if (response.status !== 204) { - toastAlert('Failed to delete session'); + const response = await deleteSessionAPI(fetch, this.id); + if (!response) { + toastAlert(get(t)('session.errors.delete')); return false; } @@ -151,12 +159,12 @@ export default class Session { } async toggleDisable(): Promise<boolean> { - const response = await axiosInstance.patch(`/sessions/${this.id}`, { + const response = await patchSessionAPI(fetch, this.id, { is_active: !this.is_active }); - if (response.status !== 204) { - toastAlert('Failed to toggle activite session'); + if (!response) { + toastAlert(get(t)('session.errors.toggle')); return false; } @@ -166,10 +174,9 @@ export default class Session { } async addUser(user: User): Promise<boolean> { - const response = await axiosInstance.post(`/sessions/${this.id}/users/${user.id}`); - - if (response.status !== 201) { - toastAlert('Failed to add user to session'); + const response = await addUserToSessionAPI(fetch, this.id, user.id); + if (!response) { + toastAlert(get(t)('session.errors.addUser')); return false; } @@ -183,8 +190,8 @@ export default class Session { return this._users.some((u) => u.equals(user)); } - async loadMessages(): Promise<boolean> { - const messagesStr = await getMessagesAPI(this.id); + async loadMessages(f: fetchType = fetch): Promise<boolean> { + const messagesStr = await getMessagesAPI(f, this.id); this._messages.set(Message.parseAll(messagesStr)); return true; @@ -196,9 +203,8 @@ export default class Session { metadata: { message: string; date: number }[], replyTo: number | null ): Promise<Message | null> { - const json = await createMessageAPI(this.id, content, metadata, replyTo); - - if (!json || !json.id || !json.message_id) { + const json = await createMessageAPI(fetch, this.id, content, metadata, replyTo); + if (json == null || json.id == null || json.message_id == null) { toastAlert('Failed to parse message'); return null; } @@ -216,9 +222,9 @@ export default class Session { } async sendTyping(): Promise<boolean> { - const response = await axiosInstance.post(`/sessions/${this.id}/typing`); - if (response.status !== 204) { - console.log('Failed to send typing data', response); + const response = await sendTypingAPI(fetch, this.id); + if (!response) { + toastAlert(get(t)('session.errors.typing')); return false; } @@ -226,9 +232,9 @@ export default class Session { } async sendPresence(): Promise<boolean> { - const response = await axiosInstance.post(`/sessions/${this.id}/presence`); - if (response.status !== 204) { - console.log('Failed to send presence data', response); + const response = await sendPresenceAPI(fetch, this.id); + if (!response) { + toastAlert(get(t)('session.errors.presence')); return false; } @@ -236,11 +242,11 @@ export default class Session { } async sendSatisfy(usefullness: number, easiness: number, remarks: string): Promise<boolean> { - return await createSessionSatisfyAPI(this.id, usefullness, easiness, remarks); + return await createSessionSatisfyAPI(fetch, this.id, usefullness, easiness, remarks); } async changeLanguage(language: string): Promise<boolean> { - const res = await patchLanguageAPI(this.id, language); + const res = await patchLanguageAPI(fetch, this.id, language); if (!res) return false; this._language = language; return true; @@ -368,10 +374,9 @@ export default class Session { } async removeUser(user: User): Promise<boolean> { - const response = await axiosInstance.delete(`/sessions/${this.id}/users/${user.id}`); - - if (response.status !== 204) { - toastAlert('Failed to remove user from session'); + const response = await removeUserFromSessionAPI(fetch, this.id, user.id); + if (!response) { + toastAlert(get(t)('session.errors.removeUser')); return false; } @@ -435,13 +440,12 @@ export default class Session { } static async create(): Promise<Session | null> { - const response = await axiosInstance.post('/sessions'); - - if (response.status !== 200) { - toastAlert('Failed to create session'); + const response = await createSessionAPI(fetch); + if (!response) { + toastAlert(get(t)('session.errors.create')); return null; } - return Session.parse(response.data); + return Session.parse(response); } } diff --git a/frontend/src/lib/types/user.ts b/frontend/src/lib/types/user.ts index cb287f7600d5ef855c6b17ee202089069fb6a46b..1d9e25291992044c526ac5f42ea4980103b4d4ce 100644 --- a/frontend/src/lib/types/user.ts +++ b/frontend/src/lib/types/user.ts @@ -1,12 +1,11 @@ import { createUserAPI, getUsersAPI, patchUserAPI } from '$lib/api/users'; import { parseToLocalDate } from '$lib/utils/date'; import { toastAlert } from '$lib/utils/toasts'; +import { sha256 } from 'js-sha256'; import { get, writable } from 'svelte/store'; const { subscribe, set, update } = writable<User[]>([]); -export const user = writable<User | null>(null); - export const users = { subscribe, set, @@ -15,7 +14,7 @@ export const users = { add: (user: User) => update((users) => [...users, user]), delete: (id: number) => update((users) => users.filter((user) => user.id !== id)), search: (email: string) => get(users).find((user) => user.email.includes(email)), - fetch: async () => User.parseAll(await getUsersAPI()) + fetch: async () => User.parseAll(await getUsersAPI(fetch)) }; export default class User { @@ -23,7 +22,6 @@ export default class User { private _email: string; private _nickname: string; private _type: number; - private _availability: bigint; private _is_active: boolean; private _ui_language: string | null; private _home_language: string | null; @@ -39,7 +37,6 @@ export default class User { email: string, nickname: string, type: number, - availability: bigint, is_active: boolean, ui_language: string | null, home_language: string | null, @@ -54,7 +51,6 @@ export default class User { this._email = email; this._nickname = nickname; this._type = type; - this._availability = availability; this._is_active = is_active; this._ui_language = ui_language; this._home_language = home_language; @@ -74,6 +70,10 @@ export default class User { return this._email; } + get emailHash(): string { + return sha256(this._email.toLowerCase()); + } + get nickname(): string { return this._nickname; } @@ -94,10 +94,6 @@ export default class User { return this._type === 1; } - get availability(): bigint { - return this._availability; - } - get ui_language(): string | null { return this._ui_language; } @@ -139,7 +135,7 @@ export default class User { } async setAvailability(availability: bigint, calcom_link: string): Promise<boolean> { - return await patchUserAPI(this.id, { + return await patchUserAPI(fetch, this.id, { availability: availability.toString(), calcom_link: calcom_link }); @@ -155,7 +151,6 @@ export default class User { email: this.email, nickname: this.nickname, type: this.type, - availability: this.availability.toString(), is_active: this.is_active, ui_language: this.ui_language, home_language: this.home_language, @@ -169,12 +164,11 @@ export default class User { } async patch(data: any): Promise<boolean> { - const res = await patchUserAPI(this.id, data); + const res = await patchUserAPI(fetch, this.id, data); if (res) { if (data.email) this._email = data.email; if (data.nickname) this._nickname = data.nickname; if (data.type) this._type = data.type; - if (data.availability) this._availability = BigInt(data.availability); if (data.is_active) this._is_active = data.is_active; if (data.ui_language) this._ui_language = data.ui_language; if (data.home_language) this._home_language = data.home_language; @@ -199,7 +193,7 @@ export default class User { type: number, is_active: boolean ): Promise<User | null> { - const id = await createUserAPI(nickname, email, password, type, is_active); + const id = await createUserAPI(fetch, nickname, email, password, type, is_active); if (id == null) return null; const user = new User( @@ -207,7 +201,6 @@ export default class User { email, nickname, type, - BigInt(0), is_active, null, null, @@ -232,8 +225,6 @@ export default class User { const userFinal = User.parse(userObject); if (userFinal == null || userFinal.id == null || userFinal.id == undefined) return null; - user.set(userFinal); - return userFinal; } @@ -249,7 +240,6 @@ export default class User { json.email, json.nickname, json.type, - BigInt(json.availability), json.is_active, json.ui_language, json.home_language, diff --git a/frontend/src/lib/utils/replyUtils.ts b/frontend/src/lib/utils/replyUtils.ts deleted file mode 100644 index 9efff3672179a1ffefc647908152a5166ec4be49..0000000000000000000000000000000000000000 --- a/frontend/src/lib/utils/replyUtils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Writable } from 'svelte/store'; -import type Message from '$lib/types/message'; -import { writable } from 'svelte/store'; - -export const replyToMessage: Writable<Message | null> = writable(null); - -export function initiateReply(message: Message): void { - replyToMessage.set(message); -} - -export function clearReplyToMessage(): void { - replyToMessage.set(null); -} diff --git a/frontend/src/lib/utils/security.ts b/frontend/src/lib/utils/security.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d8bb0848f395da91a0346892433d4e36617ffca --- /dev/null +++ b/frontend/src/lib/utils/security.ts @@ -0,0 +1,41 @@ +import { redirect } from '@sveltejs/kit'; + +export function isRedirectPathValid(path: string, origin: string): boolean { + try { + const url = new URL(path, origin); + return url.origin == origin; + } catch (e) { + return false; + } +} + +export function safeRedirect(path: string | null | undefined, origin: string) { + if (!path || !isRedirectPathValid(path, origin)) path = '/'; + return redirect(302, path); +} + +export function safeRedirectAuto(url: URL) { + return safeRedirect(url.searchParams.get('redirect'), url.origin); +} + +export function validateEmail(email: unknown): email is string { + return ( + typeof email === 'string' && + email.length >= 3 && + email.length <= 255 && + /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email) + ); +} + +export function validateUsername(username: unknown): username is string { + return ( + typeof username === 'string' && + username.length >= 3 && + username.length <= 31 && + /^[a-z0-9_-]+$/.test(username) + ); +} + +export function validatePassword(password: unknown): password is string { + return typeof password === 'string' && password.length >= 8 && password.length <= 255; +} diff --git a/frontend/src/lib/utils/types.ts b/frontend/src/lib/utils/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..479544ba236a92006d803a94d426b5654307c22a --- /dev/null +++ b/frontend/src/lib/utils/types.ts @@ -0,0 +1 @@ +export type fetchType = typeof fetch; diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index a621bd9cee4de615e70a753670322e7e8aed3daa..b3aecd87510c735f98e5cb872178617dbaeaccd9 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -14,13 +14,12 @@ const isPublic = (path: string) => { export const load: ServerLoad = async ({ locals, url }) => { if (locals.user == null || locals.user == undefined) { if (!isPublic(url.pathname)) { - redirect(307, `/login`); + redirect(303, `/login`); } } return { user: locals.user, - session: locals.session, locale: locals.locale }; }; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index a7c90acd7077955dfa01f2d16d89d3691062ec50..d200c9f18528a75f137b466ad91c8d936bd2c8fd 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,23 +1,20 @@ <script lang="ts"> import { SvelteToast } from '@zerodevx/svelte-toast'; - import Header from '$lib/components/header.svelte'; import '../app.css'; import { t } from '$lib/services/i18n'; - import User from '$lib/types/user.js'; + import Header from './Header.svelte'; + import type { PageData } from './$types'; - export let data; - - User.parseFromServer(data); - - console.log(2); + let { data, children }: { data: PageData; children: any } = $props(); + let user = data.user; </script> <svelte:head> <title>{$t('header.appName')}</title> </svelte:head> -<Header /> +<Header {user} /> -<slot /> +{@render children()} <SvelteToast /> diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts index 8ec665a89c83295e236d219427d8a3b1b0c96a4d..0b0140c24beaa6b54ce5506d96ea71d22696fb49 100644 --- a/frontend/src/routes/+layout.ts +++ b/frontend/src/routes/+layout.ts @@ -1,16 +1,20 @@ export const ssr = true; -import type { Load } from '@sveltejs/kit'; +import { error, type Load } from '@sveltejs/kit'; import { loadTranslations } from '$lib/services/i18n'; +import User from '$lib/types/user'; export const load: Load = async ({ url, data }) => { - const { user, session, locale } = data; + if (!data) { + return error(500, 'No data'); + } + + const { user, locale } = data!; const { pathname } = url; await loadTranslations(locale, pathname); return { - user, - token: session + user: user ? User.parse(user) : null }; }; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ed9f6fef645c15796ec431ddc8aaa58ffe989371..4b5ece68436f1e598bffc54c1b8e5858feb87b37 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -11,7 +11,7 @@ ArrowRightCircle } from 'svelte-hero-icons'; import { t } from '$lib/services/i18n'; - import User, { user } from '$lib/types/user'; + import User from '$lib/types/user'; import { createUserContactFromEmailAPI, getUserContactsAPI, @@ -21,16 +21,18 @@ import { toastAlert, toastSuccess, toastWarning } from '$lib/utils/toasts'; import { get } from 'svelte/store'; - let ready = false; - $: contacts = [] as User[]; - $: contact = null as User | null; - $: contactSessions = [] as Session[]; - let modalNew = false; - let nickname = ''; + let { data } = $props(); + let user = data.user!; + let contacts: User[] = $state(data.contacts); + let contact: User | undefined = $state(data.contact); + let contactSessions: Session[] = $state(data.sessions); - let showTerminatedSessions = false; + let modalNew = $state(false); + let nickname = $state(''); - async function selectContact(c: User | null) { + let showTerminatedSessions = $state(false); + + async function selectContact(c: User | undefined) { showTerminatedSessions = false; contact = c; if (!contact) { @@ -38,20 +40,12 @@ return; } - contactSessions = Session.parseAll(await getUserContactSessionsAPI($user!.id, contact.id)).sort( - (a, b) => b.start_time.getTime() - a.start_time.getTime() - ); + contactSessions = Session.parseAll( + await getUserContactSessionsAPI(fetch, user.id, contact.id) + ).sort((a, b) => b.start_time.getTime() - a.start_time.getTime()); } onMount(async () => { - if (!$user) return; - contacts = User.parseAll(await getUserContactsAPI($user.id)); - if (contacts.length > 0) { - selectContact(contacts[0]); - } - - ready = true; - (function (C: any, A: any, L: any) { let p = function (a: any, ar: any) { a.q.push(ar); @@ -90,7 +84,7 @@ Cal('on', { action: 'bookingSuccessful', callback: async (e: any) => { - if (!contact || !$user || !e.detail.data) { + if (!contact || !user || !e.detail.data) { toastAlert(get(t)('home.bookingFailed')); return; } @@ -99,7 +93,8 @@ const duration = e.detail.data.duration; const end = new Date(date.getTime() + duration * 60000); const sess_id: number | null = await createSessionFromCalComAPI( - $user.id, + fetch, + user.id, contact.id, date, end @@ -110,7 +105,7 @@ } toastSuccess(get(t)('home.bookingSuccessful')); contactSessions = Session.parseAll( - await getUserContactSessionsAPI($user!.id, contact.id) + await getUserContactSessionsAPI(fetch, user!.id, contact.id) ).sort((a, b) => b.start_time.getTime() - a.start_time.getTime()); } }); @@ -127,172 +122,185 @@ } async function searchNickname() { - if (!$user || !nickname || !nickname.includes('@')) { + if (!user || !nickname || !nickname.includes('@')) { toastWarning('Please enter a valid email address'); return; } - const res = await createUserContactFromEmailAPI($user.id, nickname); + const res = await createUserContactFromEmailAPI(fetch, user.id, nickname); if (!res) return; modalNew = false; - contacts = User.parseAll(await getUserContactsAPI($user.id)); + contacts = User.parseAll(await getUserContactsAPI(fetch, user.id)); } </script> -{#if ready} - <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)} - <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" - aria-label={c.nickname} - tabindex="0" - on:keydown={(e) => e.key === 'Enter' && selectContact(c)} - > - <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} class="" /> - {:else} - <Icon src={UserIcon} /> - {/if} - </div> - <div class="text-lg font-bold capitalize flex items-center"> - {c.nickname} - </div> +<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)} + <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} + onclick={() => selectContact(c)} + role="button" + aria-label={c.nickname} + tabindex="0" + onkeydown={(e) => e.key === 'Enter' && selectContact(c)} + > + <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} class="" /> + {:else} + <Icon src={UserIcon} /> + {/if} + </div> + <div class="text-lg font-bold capitalize flex items-center"> + {c.nickname} </div> - {/each} - </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)} - > - + - </button> - </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> - <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> - <div - class="border p-4 mt-4 rounded-xl shadow-[0_0_6px_0_rgba(0,14,156,.2)] overflow-y-scroll no-scrollbar" + {/each} + </div> + <button + class="h-20 w-full flex justify-center items-center text-lg border-gray-200 border-t hover:bg-gray-200" + onclick={() => (modalNew = true)} + > + + + </button> + </div> + {#if contact} + <div class="flex flex-col xl:mx-auto xl:w-[60rem] m-4"> + <div> + <button + onclick={(e) => { + e.preventDefault(); + createSession(); + }} + class="button float-start mr-2" > - <table class="divide-y divide-neutral-300 text-center w-full table-fixed"> - <thead> + {$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> + <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> - <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> + <td colspan="4" class="py-5 text-gray-500">{$t('home.noSessions')}</td> </tr> - </thead> - <tbody class="divide-y divide-neutral-200"> - {#if contactSessions.length === 0} + {: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.noSessions')}</td> + <td colspan="4" class="py-5 text-gray-500" + >{$t('home.noCurrentOrFutureSessions')}</td + > </tr> - {:else} - {#if !showTerminatedSessions && contactSessions.filter((s) => s.end_time >= new Date()).length === 0} + {/if} + {#each contactSessions as s (s.id)} + {#if showTerminatedSessions || s.end_time >= new Date()} <tr> - <td colspan="4" class="py-5 text-gray-500" - >{$t('home.noCurrentOrFutureSessions')}</td - > + <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="/sessions/{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 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)} - > - <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} - </tbody> - </table> - </div> + {/each} + <tr> + <td + class="py-2 hover:cursor-pointer" + colspan="4" + onclick={() => (showTerminatedSessions = !showTerminatedSessions)} + > + <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} + </tbody> + </table> </div> - {/if} - </div> -{/if} + </div> + {:else} + <div class="flex-grow text-center mt-16"> + <div class="text-lg text-gray-500 pt-4 italic">{$t('home.noContact')}</div> + <div> + <button class="mx-auto mt-8 button" onclick={() => (modalNew = true)}> + + {$t('home.newFirstContact')} + </button> + </div> + </div> + {/if} +</div> <dialog class="modal bg-black bg-opacity-50" open={modalNew} - on:close={() => (modalNew = false)} + onclose={() => (modalNew = false)} tabindex="-1" > <div class="modal-box"> @@ -303,9 +311,9 @@ placeholder={$t('home.email')} bind:value={nickname} class="input flex-grow mr-2" - on:keydown={(e) => e.key === 'Escape' && (modalNew = false)} + onkeydown={(e) => e.key === 'Escape' && (modalNew = false)} /> - <button class="button w-16" on:click={searchNickname}> + <button class="button w-16" onclick={searchNickname}> <Icon src={MagnifyingGlass} /> </button> </div> diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..62a009b9964eab8a83544ae481bf25066b59087d --- /dev/null +++ b/frontend/src/routes/+page.ts @@ -0,0 +1,29 @@ +import { getUserContactsAPI, getUserContactSessionsAPI } from '$lib/api/users'; +import Session from '$lib/types/session'; +import User from '$lib/types/user'; +import type { Load } from '@sveltejs/kit'; + +export const load: Load = async ({ parent, fetch }) => { + const { user } = await parent(); + + const contacts = User.parseAll(await getUserContactsAPI(fetch, user.id)); + + if (contacts.length === 0) { + return { + contacts, + contact: undefined, + sessions: [] + }; + } + + const contact = contacts[0]; + const sessions = Session.parseAll( + await getUserContactSessionsAPI(fetch, user.id, contact.id) + ).sort((a, b) => b.start_time.getTime() - a.start_time.getTime()); + + return { + contacts, + contact, + sessions + }; +}; diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/routes/Header.svelte similarity index 71% rename from frontend/src/lib/components/header.svelte rename to frontend/src/routes/Header.svelte index 6f8db00fa5e12bd1f0960983a65c6ebef0710648..2860508ee6c8087caf91af12e86e2dd2ca6811aa 100644 --- a/frontend/src/lib/components/header.svelte +++ b/frontend/src/routes/Header.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import LocalSelector from './header/localSelector.svelte'; + import LocalSelector from '$lib/components/header/localSelector.svelte'; import { t } from '$lib/services/i18n'; - import { - ExclamationTriangle, - Icon, - Bars3, - UserCircle, - Language, - Cog6Tooth - } from 'svelte-hero-icons'; - import { onMount } from 'svelte'; - import { user } from '$lib/types/user'; + import { ExclamationTriangle, Icon, Bars3, Language, Cog6Tooth } from 'svelte-hero-icons'; import { page } from '$app/stores'; + import type User from '$lib/types/user'; - $: displayMetadataWarning = false; + let { user }: { user: User | null } = $props(); - onMount(async () => { - if ($user) { - if (!$user.home_language || !$user.target_language || !$user.birthdate || !$user.gender) { - displayMetadataWarning = true; - } + let displayMetadataWarning = $state(false); + + if (user) { + if (!user.home_language || !user.target_language || !user.birthdate || !user.gender) { + displayMetadataWarning = true; } - }); + } </script> -<header class="navbar shadow"> +<header class="navbar shadow bg-gray-200"> <div class="navbar-start"> <div class="dropdown sm:hidden p-0"> <div tabindex="0" role="button" class="btn btn-ghost"> @@ -34,7 +26,7 @@ tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52" > - {#if $user} + {#if user} <li><a href="/">Item 1</a></li> <li> <a href="/">Parent</a> @@ -58,29 +50,19 @@ </h1> </div> <div class="navbar-end hidden sm:flex"> - <ul class="menu menu-horizontal p-0"> - {#if $user} + <ul class="menu menu-horizontal p-0 flex items-center"> + {#if user} <li> <details> - <summary class="p-3"> - <Icon src={UserCircle} class="h-5 w-5" /> - {$user.nickname} + <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"> - {#if $user?.type === 0 || $user?.type === 1} - <li> - <a data-sveltekit-reload href="/tutor/timeslots"> - {$t('header.availability')} - </a> - </li> - {/if} - {#if $user?.type === 2} - <li> - <a data-sveltekit-reload href="/timeslots"> - {$t('header.tutorSelection')} - </a> - </li> - {/if} <li> <a data-sveltekit-reload href="/logout" class="whitespace-nowrap"> {$t('header.signout')} @@ -89,7 +71,7 @@ </ul> </details> </li> - {#if $user?.type === 0} + {#if user?.type === 0} <li> <details> <summary class="p-3"> diff --git a/frontend/src/routes/admin/+layout.server.ts b/frontend/src/routes/admin/+layout.server.ts index 28121ae92f7596cb99f9eba69fc0ed1624eaac95..fc19640b69189443952b6ec6678c2137e9c930fe 100644 --- a/frontend/src/routes/admin/+layout.server.ts +++ b/frontend/src/routes/admin/+layout.server.ts @@ -2,11 +2,10 @@ import { type ServerLoad, error, redirect } from '@sveltejs/kit'; export const load: ServerLoad = async ({ locals }) => { if (locals.user == null || locals.user == undefined) { - redirect(307, '/login'); + redirect(303, '/login'); } - const user = JSON.parse(locals.user); - if (user == null || user == undefined || user.type != 0) { + if (locals.user == null || locals.user == undefined || locals.user.type != 0) { error(403, 'Forbidden'); } }; diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index e3a114a93ded997e4e4d428edd024247aab0cb2a..8f5550b099a9d9a3a0228e560e97f8c7bcb8dafc 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1,24 +1,18 @@ <script lang="ts"> - import { onMount } from 'svelte'; - import User, { users } from '$lib/types/user'; - import { getUsersAPI } from '$lib/api/users'; + import User from '$lib/types/user'; import { t } from '$lib/services/i18n'; import UserItem from '$lib/components/users/userItem.svelte'; + import type { PageData } from './$types'; - let ready = false; + let { data }: { data: PageData } = $props(); + let users = data.users; - onMount(async () => { - User.parseAll(await getUsersAPI()); + let nickname = $state(''); + let email = $state(''); + let type = $state('2'); + let is_active = $state(true); - ready = true; - }); - - $: nickname = ''; - $: email = ''; - $: type = '2'; - $: is_active = true; - - $: canCreate = nickname !== '' && email !== '' && type !== ''; + let canCreate = $derived(nickname !== '' && email !== '' && type !== ''); async function createUser() { if (!canCreate) return; @@ -40,54 +34,45 @@ } </script> -{#if ready} - <div class="min-w-fit max-w-3xl mx-auto"> - <h1 class="text-xl font-bold m-5 text-center">Users</h1> - <table class="table"> - <thead> - <tr> - <th>#</th> - <th>{$t('users.nickname')}</th> - <th>{$t('users.email')}</th> - <th>{$t('users.category')}</th> - <th>{$t('users.isActive')}</th> - <th>{$t('admin.actions')}</th> - </tr> - </thead> - <tbody> - {#each $users as user (user.id)} - <UserItem {user} /> - {/each} - </tbody> - <tfoot class=""> - <tr class=""> - <td>+</td> - <td><input type="text" class="input input-sm" bind:value={nickname} /></td> - <td><input type="text" class="input input-sm" bind:value={email} /></td> - <td> - <select class="select select-sm select-bordered" bind:value={type}> - <option value="2">{$t('users.type.student')}</option> - <option value="1">{$t('users.type.tutor')}</option> - <option value="0">{$t('users.type.admin')}</option> - </select> - </td> - <td> - <input type="checkbox" class="checkbox" bind:value={is_active} checked /> - </td> - <td> - <button class="btn btn-sm" disabled={!canCreate} on:click={createUser}> - {$t('button.create')} - </button> - </td> - </tr> - </tfoot> - </table> - </div> -{/if} - -<style lang="postcss"> - /* input, - select { - @apply w-full border-2 h-8 text-center border-gray-400 rounded bg-transparent; - } */ -</style> +<div class="min-w-fit max-w-3xl mx-auto"> + <h1 class="text-xl font-bold m-5 text-center">Users</h1> + <table class="table"> + <thead> + <tr> + <th>#</th> + <th>{$t('users.nickname')}</th> + <th>{$t('users.email')}</th> + <th>{$t('users.category')}</th> + <th>{$t('users.isActive')}</th> + <th>{$t('admin.actions')}</th> + </tr> + </thead> + <tbody> + {#each users as user (user.id)} + <UserItem {user} /> + {/each} + </tbody> + <tfoot class=""> + <tr class=""> + <td>+</td> + <td><input type="text" class="input input-sm" bind:value={nickname} /></td> + <td><input type="text" class="input input-sm" bind:value={email} /></td> + <td> + <select class="select select-sm select-bordered" bind:value={type}> + <option value="2">{$t('users.type.student')}</option> + <option value="1">{$t('users.type.tutor')}</option> + <option value="0">{$t('users.type.admin')}</option> + </select> + </td> + <td> + <input type="checkbox" class="checkbox" bind:checked={is_active} /> + </td> + <td> + <button class="btn btn-sm" disabled={!canCreate} onclick={createUser}> + {$t('button.create')} + </button> + </td> + </tr> + </tfoot> + </table> +</div> diff --git a/frontend/src/routes/admin/+page.ts b/frontend/src/routes/admin/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9469731e5a6f922c0d7b269e6c162a198d75ae1 --- /dev/null +++ b/frontend/src/routes/admin/+page.ts @@ -0,0 +1,11 @@ +import { getUsersAPI } from '$lib/api/users'; +import User from '$lib/types/user'; +import { type Load } from '@sveltejs/kit'; + +export const load: Load = async ({ fetch }) => { + const users = User.parseAll(await getUsersAPI(fetch)); + + return { + users + }; +}; diff --git a/frontend/src/routes/admin/sessions/+page.svelte b/frontend/src/routes/admin/sessions/+page.svelte index d50db59d3bd4d476d6b20ab7f92fca747d2eb3a1..dcf1ebbd398b606c3a3c95e8eb10ab06c2fd327d 100644 --- a/frontend/src/routes/admin/sessions/+page.svelte +++ b/frontend/src/routes/admin/sessions/+page.svelte @@ -1,17 +1,13 @@ <script lang="ts"> - import { getSessionsAPI } from '$lib/api/sessions'; import Session from '$lib/types/session'; - import { onMount } from 'svelte'; import { t } from '$lib/services/i18n'; import { displayTime } from '$lib/utils/date'; import { ArrowDownTray, ArrowRightStartOnRectangle, Icon } from 'svelte-hero-icons'; import config from '$lib/config'; + import type { PageData } from './$types'; - let sessions: Session[] = []; - - onMount(async () => { - sessions = Session.parseAll(await getSessionsAPI()); - }); + let { data }: { data: PageData } = $props(); + let sessions: Session[] = data.sessions; </script> <h1 class="text-xl font-bold m-5 text-center">{$t('header.admin.sessions')}</h1> diff --git a/frontend/src/routes/admin/sessions/+page.ts b/frontend/src/routes/admin/sessions/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..de44a7d380e6c4137f91faba212534f13edb56b5 --- /dev/null +++ b/frontend/src/routes/admin/sessions/+page.ts @@ -0,0 +1,11 @@ +import { getSessionsAPI } from '$lib/api/sessions'; +import Session from '$lib/types/session'; +import { type Load } from '@sveltejs/kit'; + +export const load: Load = async ({ fetch }) => { + const sessions = Session.parseAll(await getSessionsAPI(fetch)); + + return { + sessions + }; +}; diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 4a5396591590ac2ed219f30bfd6e6acde8a43e04..cdb6f040ccf0eb7162462b60842a5c1a6e4521e2 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -1,14 +1,44 @@ -import { type ServerLoad, redirect } from '@sveltejs/kit'; +import { safeRedirectAuto } from '$lib/utils/security'; +import { type Actions, type ServerLoad, redirect } from '@sveltejs/kit'; export const load: ServerLoad = async ({ locals, url }) => { if (locals.user != null && locals.user != undefined) { const path = url.searchParams.get('redirect') || '/'; - redirect(307, path); + redirect(303, path); } return { user: locals.user, - session: locals.session, + jwt: locals.jwt, locale: locals.locale }; }; + +export const actions: Actions = { + default: async ({ request, url, fetch }) => { + const formData = await request.formData(); + + const email = formData.get('email'); + const password = formData.get('password'); + + if (!email || !password) { + return { + message: 'Invalid request' + }; + } + + const response = await fetch(`/api/auth/login`, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify({ email, password }) + }); + + if (response.status === 401) return { message: 'Incorrect email or password' }; + if (response.status === 422) return { message: 'Invalid request' }; + if (response.status !== 200) return { message: 'Unknown error occurred' }; + + return safeRedirectAuto(url); + } +}; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index ea3e8952e443644da761d83ebb5961ab01ec9909..cf8fa60202f3045f45e7d2e6ec739f36f0bb71fd 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,26 +1,7 @@ <script lang="ts"> - import { loginAPI } from '$lib/api/auth'; - import { getBaseURL } from '$lib/utils/login'; import { t } from '$lib/services/i18n'; - let email = ''; - let password = ''; - $: message = ''; - - async function login() { - message = ''; - const result = await loginAPI(email, password); - if (result !== 'OK') { - message = result; - return; - } - - const redirect = decodeURIComponent( - new URLSearchParams(window.location.search).get('redirect') ?? getBaseURL() - ); - - window.location.href = redirect; - } + let { form }: { form: FormData } = $props(); </script> <div class="flex justify-center items-center h-screen"> @@ -29,22 +10,16 @@ <p class="self-center text-sm"> {$t('login.noAccountText')} - <a data-sveltekit-reload href="/register" class="link link-secondary"> + <a href="/register" class="link link-secondary"> {$t('login.noAccountLink')} </a> </p> - - <!-- <a class="btn btn-neutral"> - <i class="fa-brands fa-google text-primary"></i> - Log in with Google - </a> - - <div class="divider">OR</div> --> - - <form action="#"> - {#if message} - <div class="alert alert-error"> - {message} + <form method="POST"> + {#if form?.message} + <div + class="border-2-lg rounded border border-red-600 bg-red-200 p-2 text-center text-red-900" + > + {form?.message} </div> {/if} @@ -53,14 +28,7 @@ <span class="label-text">{$t('login.email')}</span> </div> - <input - type="text" - id="email" - name="email" - class="input input-bordered" - bind:value={email} - required - /> + <input type="text" id="email" name="email" class="input input-bordered" required /> </label> <label class="form-control"> @@ -75,34 +43,15 @@ id="password" name="password" class="input input-bordered" - bind:value={password} required /> </label> - <!-- <div class="form-control"> - <label class="cursor-pointer label self-start gap-2"> - TODO: remember me - <input type="checkbox" class="checkbox" /> - <span class="label-text">{$t('login.rememberMe')}</span> - </label> - </div> --> - <div class="form-control mt-4"> - <button type="submit" on:click|preventDefault={login} class="btn btn-primary"> + <button type="submit" class="btn btn-primary"> {$t('login.login')} </button> </div> </form> </div> </div> - -<style lang="postcss"> - /* label { - @apply font-bold pr-4 w-1/3 flex items-center justify-end; - } - - input { - @apply input bg-base-200 w-[400px] py-2 px-4; - } */ -</style> diff --git a/frontend/src/routes/logout/+page.server.ts b/frontend/src/routes/logout/+page.server.ts index da26188eafc7e050b85f02adaabd57d29b413da0..7790fb9c026c13a8dfafae0d1e2b38ee1fb553f1 100644 --- a/frontend/src/routes/logout/+page.server.ts +++ b/frontend/src/routes/logout/+page.server.ts @@ -1,10 +1,8 @@ import { type ServerLoad, redirect } from '@sveltejs/kit'; -import { access_cookie } from '$lib/api/apiInstance'; export const load: ServerLoad = async ({ cookies }) => { cookies.set('access_token_cookie', '', { maxAge: -1, path: '/' }); cookies.set('refresh_token_cookie', '', { maxAge: -1, path: '/' }); - access_cookie.set(''); - redirect(307, '/login'); + redirect(303, '/login'); }; diff --git a/frontend/src/routes/register/+page.server.ts b/frontend/src/routes/register/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..12e004dbfd1149fc827697a68fbdc2a12e403a60 --- /dev/null +++ b/frontend/src/routes/register/+page.server.ts @@ -0,0 +1,85 @@ +import { patchUserAPI } from '$lib/api/users'; +import { formatToUTCDate } from '$lib/utils/date'; +import { validateEmail, validatePassword, validateUsername } from '$lib/utils/security'; +import { redirect, type Actions } from '@sveltejs/kit'; + +export const actions: Actions = { + register: async ({ request, fetch }) => { + const formData = await request.formData(); + + const email = formData.get('email'); + const nickname = formData.get('nickname'); + const password = formData.get('password'); + const confirmPassword = formData.get('confirmPassword'); + + if (!email || !nickname || !password || !confirmPassword) { + return { message: 'Invalid request' }; + } + + if (!validateEmail(email)) return { message: 'Invalid email' }; + if (!validateUsername(nickname)) return { message: 'Invalid username' }; + if (!validatePassword(password)) return { message: 'Invalid password' }; + + if (password !== confirmPassword) return { message: 'Passwords do not match' }; + + let response = await fetch(`/api/auth/register`, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify({ email, nickname, password, is_tutor: false }) + }); + + if (response.status === 400) return { message: 'User already exists' }; + if (response.status === 401) return { message: 'Failed to create user' }; + if (response.status === 422) return { message: 'Invalid request' }; + if (!response.ok) return { message: 'Unknown error occurred' }; + + response = await fetch(`/api/auth/login`, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify({ email, password }) + }); + + if (response.status === 401) return { message: 'Incorrect email or password' }; + if (response.status === 422) return { message: 'Invalid request' }; + if (!response.ok) return { message: 'Unknown error occurred' }; + + return redirect(303, '/register'); + }, + data: async ({ request, fetch, locals }) => { + if (!locals.user) { + return { message: 'Unauthorized' }; + } + + const formData = await request.formData(); + + const homeLanguage = formData.get('homeLanguage'); + const targetLanguage = formData.get('targetLanguage'); + const birthyear = formData.get('birthyear'); + const gender = formData.get('gender'); + + if (!homeLanguage || !targetLanguage || !birthyear || !gender) { + return { message: 'Invalid request' }; + } + + let birthdate; + try { + birthdate = formatToUTCDate(new Date(parseInt(birthyear.toString()), 0, 30)); + } catch (e) { + return { message: 'Invalid request' }; + } + + const response = await patchUserAPI(fetch, locals.user.id, { + home_language: homeLanguage, + target_language: targetLanguage, + gender, + birthdate + }); + if (!response) return { message: 'Unknown error occurred' }; + + redirect(303, '/register'); + } +}; diff --git a/frontend/src/routes/register/+page.svelte b/frontend/src/routes/register/+page.svelte index 1debb565e26144d23e48ac912340039f41815187..4f63297d305c84dfa6f397efc566134d988563e9 100644 --- a/frontend/src/routes/register/+page.svelte +++ b/frontend/src/routes/register/+page.svelte @@ -1,178 +1,48 @@ <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 } 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 { t } from '$lib/services/i18n'; import { Icon, Envelope, Key, UserCircle } from 'svelte-hero-icons'; import Typingtest from '$lib/components/tests/typingtest.svelte'; - import AvailableTutors from '$lib/components/users/availableTutors.svelte'; import { browser } from '$app/environment'; + import type { PageData } from './$types'; import { formatToUTCDate } from '$lib/utils/date'; import Consent from '$lib/components/surveys/consent.svelte'; - let current_step = 0; + let { data, form }: { data: PageData; form: FormData } = $props(); + let user = $state(data.user); + let message = $state(''); + + let current_step = $state( + (() => { + if (user == null) { + if (form?.message) return 2; + return 1; + } else if (!user.home_language || !user.target_language || !user.birthdate || !user.gender) { + return 3; + } else { + return 5; + } + })() + ); - $: 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 target_language: string; - let birthdate: string; - let gender: string; let study_id: number | null = (() => { if (!browser) return null; let study_id_str = new URLSearchParams(window.location.search).get('study'); if (!study_id_str) return null; return parseInt(study_id_str) || null; })(); - - 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); - - 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; - } - - if (study_id === null) document.location.href = '/register'; - else document.location.href = `/register?study=${study_id}`; - - 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 || !target_language || !birthdate || !gender) { - message = $t('register.error.emptyFields'); - return; - } - - const res = await patchUserAPI(user_id, { - ui_language, - home_language, - target_language, - birthdate, - gender, - study_id - }); - - if (!res) { - message = $t('register.error.metadata'); - return; - } - - current_step++; - } - - async function onTutor(tutor: User) { - const user_id = get(user)?.id; - - if (!user_id) { - toastAlert('Failed to get current user ID'); - return; - } - - if (confirm($t('register.confirmTutor').replaceAll('{NAME}', tutor.nickname)) === false) return; - - const res = await createUserContactAPI(user_id, tutor.id); - - if (!res) { - message = $t('register.error.tutor'); - 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')} + {#if current_step >= 1 && current_step <= 2} + <button onclick={() => (current_step = 1)}> + {$t('register.tab.consent')} + </button> + {:else} + {$t('register.tab.consent')} + {/if} </li> <li class="step" class:step-primary={current_step >= 2}> {$t('register.tab.signup')} @@ -196,7 +66,11 @@ </div> <div class="max-w-screen-md mx-auto p-5"> - {#if message} + {#if form?.message} + <div class="alert alert-error text-content text-base-100 py-2 mb-4"> + {form.message} + </div> + {:else if message} <div class="alert alert-error text-content text-base-100 py-2 mb-4"> {message} </div> @@ -211,74 +85,80 @@ rights={$t('register.consent.rights')} /> <div class="form-control"> - <button class="button mt-4" on:click={() => current_step++}> + <button class="button mt-4" onclick={() => current_step++}> {$t('register.consent.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')} - /> + <form method="POST" action="?/register"> + <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="email" + name="email" + class="grow" + placeholder={$t('register.email.ph')} + required + /> + </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" + name="nickname" + class="grow" + placeholder={$t('register.nickname.ph')} + required + /> + </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" + name="password" + class="grow" + placeholder={$t('register.password')} + required + /> + </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" + name="confirmPassword" + class="grow" + placeholder={$t('register.confirmPassword')} + required + /> + </div> + </label> + <div class="form-control"> + <button class="button mt-2">{$t('register.signup')}</button> </div> - </label> - <div class="form-control"> - <button class="button mt-2" on:click={onRegister}>{$t('register.signup')}</button> - </div> + </form> </div> {:else if current_step == 3} - <div class="space-y-2"> + <form class="space-y-2" method="POST" action="?/data"> <div class="p-5 text-sm text-prose"> {@html $t('register.welcome')} </div> @@ -287,13 +167,7 @@ <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} - > + <select class="select select-bordered" id="homeLanguage" name="homeLanguage" required> <option disabled selected value="">{$t('register.homeLanguage')}</option> {#each Object.entries(config.PRIMARY_LANGUAGE) as [code, name]} <option value={code}>{name}</option> @@ -305,13 +179,7 @@ <span class="label-text">{$t('register.targetLanguage')}</span> <span class="label-text-alt">{$t('register.targetLanguage.note')}</span> </label> - <select - class="select select-bordered" - id="targetLanguage" - name="targetLanguage" - required - bind:value={target_language} - > + <select class="select select-bordered" id="targetLanguage" name="targetLanguage" required> {#each Object.entries(config.LEARNING_LANGUAGES) as [code, name]} <option value={code}>{name}</option> {/each} @@ -321,15 +189,9 @@ <label for="birthyear" class="label"> <span class="label-text">{$t('register.birthyear')}</span> </label> - <select - class="select select-bordered" - id="birthyear" - name="birthyear" - required - on:change={(e) => (birthdate = formatToUTCDate(new Date(e.target.value, 1, 1)))} - > + <select class="select select-bordered" id="birthyear" name="birthyear" required> <option disabled selected value="">{$t('register.birthyear')}</option> - {#each Array.from({ length: 82 }, (_, i) => i + 1931).reverse() as year} + {#each Array.from({ length: 90 }, (_, i) => i + 1931).reverse() as year} <option value={year}>{year}</option> {/each} </select> @@ -340,87 +202,59 @@ <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')} - /> + <input type="radio" class="radio" id="male" name="gender" value="male" required /> <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')} - /> + <input type="radio" class="radio" id="female" name="gender" value="female" required /> <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')} - /> + <input type="radio" class="radio" id="other" name="gender" value="other" required /> <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')} - /> + <input type="radio" class="radio" id="na" name="gender" value="na" required /> <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> + <button class="button mt-4">{$t('button.submit')}</button> </div> - </div> + </form> {:else if current_step == 4} - <!--{#if get(user)}--> - <h2 class="my-4 text-xl">{$t('timeslots.availabilities')}</h2> - <Timeslots bind:timeslots /> - <AvailableTutors users={filteredUsers} {timeslots} onSelect={onTutor} /> + <h2 class="my-4 text-xl">This page is disabled. Please continue.</h2> + <button onclick={() => current_step++}>{$t('button.continue')}</button> {: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)}> + <button class="button mt-4 w-full" onclick={() => (current_step = 6)}> {$t('register.continueButton')} </button> - <button class="button mt-4 w-full" on:click={() => (document.location.href = '/')}> + <button class="button mt-4 w-full" onclick={() => (document.location.href = '/')}> {$t('register.startFastButton')} </button> </div> {:else if current_step == 6} - <Typingtest onFinish={onTyping} /> + {#if user} + <Typingtest onFinish={() => current_step++} {user} /> + {/if} {: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 = '/')}> + <button class="button mt-4 m-auto" onclick={() => (document.location.href = '/')}> {$t('register.startButton')} </button> </div> diff --git a/frontend/src/routes/session/+page.svelte b/frontend/src/routes/session/+page.svelte deleted file mode 100644 index 9791e4807987da8efc14689b66312dcd7599edbf..0000000000000000000000000000000000000000 --- a/frontend/src/routes/session/+page.svelte +++ /dev/null @@ -1,69 +0,0 @@ -<script lang="ts"> - import { page } from '$app/stores'; - import { getSessionAPI } from '$lib/api/sessions'; - import Chatbox from '$lib/components/sessions/chatbox.svelte'; - import Session from '$lib/types/session'; - import { getBaseURL } from '$lib/utils/login'; - import { onMount } from 'svelte'; - import { user } from '$lib/types/user'; - import Gravatar from 'svelte-gravatar'; - import WeeklySurvey from '$lib/components/users/weeklySurvey.svelte'; - import config from '$lib/config.js'; - export let data; - - let session: Session | null = null; - $: onlineUsers = session ? session.onlineUsers : null; - - onMount(async () => { - const param = $page.url.searchParams.get('id'); - if (!param) { - window.location.href = getBaseURL(); - return; - } - - const id = parseInt(param); - - if (!id) return; - else { - session = Session.parse(await getSessionAPI(id)); - } - }); -</script> - -{#if session} - <div class="h-full grid lg:grid-cols-7"> - <div class="justify-evenly h-full p-2"> - <ul class="ml-2"> - {#each session.users as sessionUser (sessionUser.id)} - <li - class="list-disc list-inside {sessionUser.id == $user?.id || - $onlineUsers?.has(sessionUser.id) - ? 'marker:text-green-500' - : 'marker:text-red-500'} marker:text-3xl" - > - <div class="inline-flex space-x-2"> - <div class="rounded-full mx-2 chat-image size-6" title={sessionUser.nickname}> - <Gravatar - email={sessionUser.email} - size={64} - title={sessionUser.nickname} - class="rounded-full" - /> - </div> - - <span class:font-bold={sessionUser === $user}>{sessionUser.nickname}</span> - </div> - </li> - {/each} - </ul> - </div> - <div class="flex flex-row flex-grow col-span-5"> - <Chatbox {session} token={data.token} /> - </div> - <div class=""></div> - </div> -{/if} - -{#if $user} - <WeeklySurvey /> -{/if} diff --git a/frontend/src/routes/sessions/[id]/+page.server.ts b/frontend/src/routes/sessions/[id]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..03b3624e6109b7d4bf7996a2b940889c1858c567 --- /dev/null +++ b/frontend/src/routes/sessions/[id]/+page.server.ts @@ -0,0 +1,9 @@ +import { type ServerLoad, redirect } from '@sveltejs/kit'; + +export const load: ServerLoad = async ({ params, locals }) => { + if (locals.user == null || locals.user == undefined) { + redirect(303, '/login?redirect=/sessions/' + params.id); + } + + return { jwt: locals.jwt, user: locals.user }; +}; diff --git a/frontend/src/routes/sessions/[id]/+page.svelte b/frontend/src/routes/sessions/[id]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6f53193a995279fac3963f4a03bafce9e1a34d2e --- /dev/null +++ b/frontend/src/routes/sessions/[id]/+page.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import { t } from '$lib/services/i18n'; + import type { PageData } from './$types.js'; + import WeeklySurvey from './WeeklySurvey.svelte'; + import Chatbox from './Chatbox.svelte'; + + let { data }: { data: PageData } = $props(); + let user = data.user!; + + let { session, jwt } = data; + let { onlineUsers } = session; +</script> + +<div class="h-full flex lg:flex-row flex-col pt-2 lg:pt-0"> + <div + class="border rounded-xl p-4 shadow-[0_0_6px_0_rgba(0,14,156,.2)] m-2 my-0 lg:mt-2 h-fit lg:w-96 text-lg space-y-2" + > + <h2 class="text-center font-bold">{$t('utils.words.participants')}</h2> + <div class="pb-2 space-y-2"> + {#each session.users as sessionUser (sessionUser.id)} + <div class="flex space-x-2"> + <div class="rounded-full mx-2 chat-image size-6" title={sessionUser.nickname}> + <img + src={`https://gravatar.com/avatar/${sessionUser.emailHash}?d=identicon`} + alt={sessionUser.nickname} + class="rounded-full border-2 text-sm {sessionUser.id == user?.id || + $onlineUsers.has(sessionUser.id) + ? 'border-green-500' + : 'border-red-500'}" + /> + </div> + + <span class:font-bold={sessionUser === user}>{sessionUser.nickname}</span> + </div> + {/each} + </div> + <h2 class="text-center font-bold border-t pt-2">{$t('utils.words.topics')}</h2> + <p class="text-center text-sm text-neutral-500 italic">{$t('session.noTopic')}</p> + </div> + <div class="flex flex-row flex-grow col-span-5"> + <Chatbox {session} {jwt} {user} /> + </div> +</div> + +<WeeklySurvey {user} /> diff --git a/frontend/src/routes/sessions/[id]/+page.ts b/frontend/src/routes/sessions/[id]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..251ba545835504f12d817b4ff4eee7d6bfb406df --- /dev/null +++ b/frontend/src/routes/sessions/[id]/+page.ts @@ -0,0 +1,26 @@ +import { getSessionAPI } from '$lib/api/sessions'; +import Session from '$lib/types/session'; +import { error, type Load } from '@sveltejs/kit'; + +export const load: Load = async ({ params, fetch, data }) => { + const jwt = data?.jwt; + + const id = params.id; + if (!id) { + error(404, 'Not found'); + } + + const nid = parseInt(id); + if (isNaN(nid)) { + error(404, 'Not found'); + } + + const session = Session.parse(await getSessionAPI(fetch, nid)); + if (!session) { + error(404, 'Not found'); + } + + await session.loadMessages(fetch); + + return { session, jwt }; +}; diff --git a/frontend/src/lib/components/sessions/chatbox.svelte b/frontend/src/routes/sessions/[id]/Chatbox.svelte similarity index 83% rename from frontend/src/lib/components/sessions/chatbox.svelte rename to frontend/src/routes/sessions/[id]/Chatbox.svelte index d4f6ad2a6f30ef9610137c321faa0fddb3030ec5..56c7a9a3ed797cf080dbf0bd2b845d514b7384fe 100644 --- a/frontend/src/lib/components/sessions/chatbox.svelte +++ b/frontend/src/routes/sessions/[id]/Chatbox.svelte @@ -1,30 +1,31 @@ <script lang="ts"> import type Session from '$lib/types/session'; import { onDestroy, onMount } from 'svelte'; - import MessageC from './message.svelte'; + import MessageC from './Message.svelte'; import { get } from 'svelte/store'; - import Writebox from './writebox.svelte'; + import Writebox from './Writebox.svelte'; import { t } from '$lib/services/i18n'; import { toastSuccess } from '$lib/utils/toasts'; import { Icon, PencilSquare } from 'svelte-hero-icons'; import Message from '$lib/types/message'; import Feedback from '$lib/types/feedback'; + import type User from '$lib/types/user'; - export let token: string; + const { session, jwt, user }: { session: Session; jwt: string; user: User } = $props(); - export let session: Session; - let messages = get(session.messages); + const { messages } = session; - session.messages.subscribe((newMessages) => { + let replyTo: Message | undefined = $state(); + + messages.subscribe((newMessages) => { let news = newMessages .filter( (m) => - !messages.find( + !$messages.find( (m2) => m instanceof Message && m2 instanceof Message && m2.message_id === m.message_id ) ) .at(-1); - messages = newMessages; if (!news || !(news instanceof Message)) return; if (document.hidden) { @@ -36,7 +37,7 @@ } }); - let wsConnected = true; + let wsConnected = $state(true); let timeout: number; session.wsConnected.subscribe((newConnected) => { clearTimeout(timeout); @@ -50,7 +51,7 @@ } }); - $: isTyping = false; + let isTyping = $state(false); let timeoutTyping: number; session.lastTyping.subscribe((lastTyping) => { @@ -102,9 +103,9 @@ }, timeBeforeSatify); } - let satisfyQ1: number = 2; - let satisfyQ2: number = 2; - let satisfyQ3: string = ''; + let satisfyQ1: number = $state(2); + let satisfyQ2: number = $state(2); + let satisfyQ3: string = $state(''); async function submitSatisfy() { const res = await session.sendSatisfy(satisfyQ1, satisfyQ2, satisfyQ3); @@ -115,8 +116,7 @@ } onMount(async () => { - await session.loadMessages(); - session.wsConnect(token); + session.wsConnect(jwt); presenceIndicator(); satisfyModal(); Notification.requestPermission(); // Should do something with denial ? @@ -127,14 +127,14 @@ }); </script> -<div class="flex flex-col w-full h-full border-x-2 scroll-smooth"> +<div class="flex flex-col w-full max-w-5xl mx-auto h-full scroll-smooth"> <div class="flex-grow h-48 overflow-auto flex-col-reverse px-4 flex mb-2"> <div class:hidden={!isTyping}> <span class="loading loading-dots loading-md"></span> </div> - {#each messages.sort((a, b) => b.created_at.getTime() - a.created_at.getTime()) as message (message.uuid)} + {#each $messages.sort((a, b) => b.created_at.getTime() - a.created_at.getTime()) as message (message.uuid)} {#if message instanceof Message} - <MessageC {message} /> + <MessageC {user} {message} bind:replyTo /> {:else if message instanceof Feedback} <a class="text-center italic text-gray-500 my-2" href="#{message.message.uuid}"> {$t('session.feedbackInline')} "{message.message.content.length > 20 @@ -151,14 +151,20 @@ Real-time sync lost. You may need to refresh the page to see new messages. </div> {/if} - <div class="flex flex-row h-30"> - <Writebox {session} /> + <div class="flex flex-row"> + <Writebox {user} {session} bind:replyTo /> </div> </div> <dialog bind:this={satisfyModalElement} class="modal"> <div class="modal-box"> - <form method="post" on:submit|preventDefault={submitSatisfy}> + <form + method="post" + onsubmit={(e) => { + e.preventDefault(); + submitSatisfy(); + }} + > <h3 class="text-lg font-bold">{$t('session.modal.satisfy.title')}</h3> <p class="mt-4 mb-2">{$t('session.modal.satisfy.q1')}</p> <input @@ -204,7 +210,7 @@ <div class="absolute bottom-4 right-4"> <button - on:click={() => { + onclick={() => { satisfyModalElement.showModal(); }} class="btn btn-primary btn-circle" diff --git a/frontend/src/lib/components/sessions/message.svelte b/frontend/src/routes/sessions/[id]/Message.svelte similarity index 77% rename from frontend/src/lib/components/sessions/message.svelte rename to frontend/src/routes/sessions/[id]/Message.svelte index 8f9f25026e5d788fc2610783b039a96e36f0085e..a5532fb1325c34f3f9edc517a9c8f350505dee42 100644 --- a/frontend/src/lib/components/sessions/message.svelte +++ b/frontend/src/routes/sessions/[id]/Message.svelte @@ -1,55 +1,37 @@ <script lang="ts"> - import type Message from '$lib/types/message'; import { displayTime } from '$lib/utils/date'; - import { Pencil, Check, Icon, ArrowUturnLeft } from 'svelte-hero-icons'; - import { user } from '$lib/types/user'; - import Gravatar from 'svelte-gravatar'; + import { ArrowUturnLeft, Check, Icon, Pencil } from 'svelte-hero-icons'; import { t } from '$lib/services/i18n'; import { onMount } from 'svelte'; import SpellCheck from '$lib/components/icons/spellCheck.svelte'; - import ChatBubble from '../icons/chatBubble.svelte'; + import ChatBubble from '$lib/components/icons/chatBubble.svelte'; import type Feedback from '$lib/types/feedback'; import linkifyHtml from 'linkify-html'; import { sanitize } from '$lib/utils/sanitize'; - import CloseIcon from '../icons/closeIcon.svelte'; - import { initiateReply } from '$lib/utils/replyUtils'; + import CloseIcon from '$lib/components/icons/closeIcon.svelte'; + import Message from '$lib/types/message'; + import type User from '$lib/types/user'; + import { get } from 'svelte/store'; - export let message: Message; + let { + user, + message, + replyTo = $bindable() + }: { user: User; message: Message; replyTo: Message | undefined } = $props(); - let replyTo: string | undefined; + let displayedTime = $state(displayTime(message.created_at)); - $: replyTo = message['_replyTo']; + setInterval(() => { + displayedTime = displayTime(message.created_at); + }, 1000); - let replyToMessage: Message | null = null; + let isEdit = $state(false); - $: if (replyTo) { - findMessageById(replyTo).then((msg) => { - replyToMessage = msg; - }); - } - - async function findMessageById(id: string): Promise<Message | null> { - try { - const resolvedMessage = await message.getMessageById(Number(id)); - return resolvedMessage; - } catch (error) { - console.error(`Error resolving message ID ${id}:`, error); - return null; - } - } + let replyToMessage: Message | undefined = $state(message.replyToMessage); - let timer: number; - $: displayedTime = displayTime(message.created_at); - $: { - clearInterval(timer); - timer = setInterval(() => { - displayedTime = displayTime(message.created_at); - }, 1000); - } - let isEdit = false; - let contentDiv: HTMLDivElement; + let contentDiv: HTMLDivElement | null = $state(null); let historyModal: HTMLDialogElement; - $: messageVersions = message.versions; + let messageVersions = $state(message.versions); function startEdit() { isEdit = true; @@ -60,6 +42,8 @@ } async function endEdit(validate = true) { + if (!contentDiv) return; + if (!validate) { contentDiv.innerText = message.content; isEdit = false; @@ -79,10 +63,8 @@ } function truncateMessage(content: string, maxLength: number = 20): string { - if (content.length > maxLength) { - return content.slice(0, maxLength) + '...'; - } - return content; + if (content.length <= maxLength) return content; + return content.slice(0, maxLength) + '...'; } let hightlight: HTMLDivElement; @@ -91,6 +73,8 @@ }); function getSelectionCharacterOffsetWithin() { + if (!contentDiv) return { start: 0, end: 0 }; + var start = 0; var end = 0; var doc = contentDiv.ownerDocument; @@ -184,10 +168,15 @@ return parts; } - $: fbs = message.feedbacks; - $: parts = getParts(message.content, $fbs); + let fbs = $state([] as Feedback[]); + let parts = $state([] as { text: string; feedback: Feedback | null }[]); + fbs = get(message.feedbacks); + message.feedbacks.subscribe((value) => { + fbs = value; + parts = getParts(message.content, fbs); + }); - const isSender = message.user.id == $user?.id; + const isSender = message.user.id == user.id; async function deleteFeedback(feedback: Feedback | null) { if (!feedback) return; @@ -204,11 +193,10 @@ class:chat-end={isSender} > <div class="rounded-full mx-2 chat-image size-12" title={message.user.nickname}> - <Gravatar - email={message.user.email} - size={64} - title={message.user.nickname} - class="rounded-full" + <img + src={`https://gravatar.com/avatar/${user.emailHash}?d=identicon`} + alt={user.nickname} + class="rounded-full border border-neutral-400 text-sm" /> </div> @@ -228,35 +216,28 @@ </a> {/if} - <button - class="absolute -right-6 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer" - on:click={() => initiateReply(message)} - > - <Icon src={ArrowUturnLeft} class="w-4 h-4 text-gray-800" /> - </button> - {#if isEdit} <div contenteditable="true" bind:this={contentDiv} - class="bg-blue-50 whitespace-pre-wrap min-h-8 p-2" + class="bg-blue-50 whitespace-pre-wrap min-h-8 p-2 text-lg" > {message.content} </div> <button class="float-end border rounded-full px-4 py-2 mt-2 bg-white text-blue-700" - on:click={() => endEdit()} + onclick={() => endEdit()} > {$t('button.save')} </button> <button class="float-end border rounded-full px-4 py-2 mt-2 mr-2" - on:click={() => endEdit(false)} + onclick={() => endEdit(false)} > {$t('button.cancel')} </button> {:else} - <div class="whitespace-pre-wrap" bind:this={contentDiv}> + <div class="whitespace-pre-wrap text-lg" bind:this={contentDiv}> {#each parts as part} {#if isEdit || !part.feedback} {@html linkifyHtml(sanitize(part.text), { className: 'underline', target: '_blank' })} @@ -275,7 +256,7 @@ aria-label="close" class:ml-1={part.feedback.content} class="hover:border-inherit border border-transparent rounded" - on:click={() => deleteFeedback(part.feedback)} + onclick={() => deleteFeedback(part.feedback)} > <CloseIcon /> </button> @@ -293,17 +274,23 @@ {#if isSender} <button class="absolute bottom-0 left-[-1.5rem] invisible group-hover:visible h-full p-0" - on:click={() => (isEdit ? endEdit() : startEdit())} + onclick={() => (isEdit ? endEdit() : startEdit())} > <Icon src={Pencil} class="w-5 h-full text-gray-500 hover:text-gray-800" /> </button> {/if} + <button + class="absolute bottom-0 left-[-3.5rem] invisible group-hover:visible h-full p-0" + onclick={() => (replyTo = message)} + > + <Icon src={ArrowUturnLeft} class="w-5 h-full text-gray-500 hover:text-gray-800" /> + </button> </div> <div class="chat-footer opacity-50"> <Icon src={Check} class="w-4 inline" /> {displayedTime} {#if message.edited} - <button class="italic cursor-help" on:click={() => historyModal.showModal()}> + <button class="italic cursor-help" onclick={() => historyModal.showModal()}> {$t('chatbox.edited')} </button> {/if} @@ -315,13 +302,13 @@ bind:this={hightlight} > <button - on:click={() => onSelect(false)} + onclick={() => onSelect(false)} class="bg-opacity-0 bg-blue-200 hover:bg-opacity-100 p-2 pl-4 rounded-l-xl" > <SpellCheck /> </button><!--- --><button - on:click={() => onSelect(true)} + onclick={() => onSelect(true)} class="bg-opacity-0 bg-blue-200 hover:bg-opacity-100 p-2 pr-4 rounded-r-xl" > <ChatBubble /> diff --git a/frontend/src/lib/components/users/weeklySurvey.svelte b/frontend/src/routes/sessions/[id]/WeeklySurvey.svelte similarity index 74% rename from frontend/src/lib/components/users/weeklySurvey.svelte rename to frontend/src/routes/sessions/[id]/WeeklySurvey.svelte index 8143e783b884b73140fb9f8f69cddec53a50a731..2b269c2133a4863be20a368e30a37de812e92249 100644 --- a/frontend/src/lib/components/users/weeklySurvey.svelte +++ b/frontend/src/routes/sessions/[id]/WeeklySurvey.svelte @@ -2,31 +2,31 @@ import { createWeeklySurveyAPI } from '$lib/api/users'; import config from '$lib/config'; import { t } from '$lib/services/i18n'; - import { user } from '$lib/types/user'; + import type User from '$lib/types/user'; import { formatToUTCDate } from '$lib/utils/date'; import { toastAlert, toastSuccess } from '$lib/utils/toasts'; - let open = - !$user?.is_tutor && - !$user?.is_admin && - (!$user?.last_survey || - $user.last_survey.getTime() + config.WEEKLY_SURVEY_INTERVAL < Date.now()); + let { user }: { user: User } = $props(); - async function send() { - if (!$user) return; + let open = $state( + !user.is_tutor && + !user.is_admin && + (!user.last_survey || user.last_survey.getTime() + config.WEEKLY_SURVEY_INTERVAL < Date.now()) + ); + async function send() { const data = Array.from({ length: 4 }, (_, i) => { const value = (document.getElementById('questions-' + i) as HTMLSelectElement).value; return value === '-1' ? null : parseFloat(value); }); - const res = await createWeeklySurveyAPI($user.id, data[0]!, data[1]!, data[2]!, data[3]!); + const res = await createWeeklySurveyAPI(fetch, user.id, data[0]!, data[1]!, data[2]!, data[3]!); if (!res) { toastAlert($t('session.modal.weekly.errors.submit')); } - await $user.patch({ last_survey: formatToUTCDate(new Date()) }); + await user.patch({ last_survey: formatToUTCDate(new Date()) }); open = false; @@ -34,14 +34,7 @@ } </script> -<dialog - class="modal bg-black bg-opacity-50" - {open} - on:close={() => (open = false)} - on:keydown={(e) => e.key === 'Escape' && (open = false)} - tabindex="0" - aria-modal="true" -> +<dialog class="modal bg-black bg-opacity-50" {open} aria-modal="true"> <div class="modal-box max-w-[800px]"> <h2 class="text-xl font-bold mb-4">{$t('session.modal.weekly.title')}</h2> <p>{@html $t('session.modal.weekly.description')}</p> @@ -50,7 +43,7 @@ <div class="label"> <span class="label-text" >{@html $t('session.modal.weekly.questions.' + i, { - lang: $t('utils.language.' + $user?.target_language).toLowerCase() + lang: $t('utils.language.' + user.target_language).toLowerCase() })}</span > </div> @@ -73,6 +66,6 @@ </select> </label> {/each} - <button class="btn btn-primary w-full mt-10" on:click={send}>{$t('button.submit')}</button> + <button class="btn btn-primary w-full mt-10" onclick={send}>{$t('button.submit')}</button> </div> </dialog> diff --git a/frontend/src/lib/components/sessions/writebox.svelte b/frontend/src/routes/sessions/[id]/Writebox.svelte similarity index 62% rename from frontend/src/lib/components/sessions/writebox.svelte rename to frontend/src/routes/sessions/[id]/Writebox.svelte index 9c6f2f864d95db4208f64385e343ace10b53a0e5..ae77e62d0e02ad4c277f54f054863f8c6553757e 100644 --- a/frontend/src/lib/components/sessions/writebox.svelte +++ b/frontend/src/routes/sessions/[id]/Writebox.svelte @@ -1,39 +1,38 @@ <script lang="ts"> import config from '$lib/config'; import { t } from '$lib/services/i18n'; - import type Session from '$lib/types/session'; import { toastAlert } from '$lib/utils/toasts'; import { Icon, PaperAirplane } from 'svelte-hero-icons'; - import { user } from '$lib/types/user'; import { onMount } from 'svelte'; - import { get } from 'svelte/store'; + import autosize from 'svelte-autosize'; + import type User from '$lib/types/user'; - import { replyToMessage, clearReplyToMessage } from '$lib/utils/replyUtils'; + import type Session from '$lib/types/session'; + import type Message from '$lib/types/message'; onMount(async () => { await import('emoji-picker-element'); }); - export let session: Session; + let { + user, + session, + replyTo = $bindable() + }: { user: User; session: Session; replyTo: Message | undefined } = $props(); - let currentReplyToMessage = null; let metadata: { message: string; date: number }[] = []; let lastMessage = ''; - let message = ''; - let showPicker = false; - let showSpecials = false; + let message = $state(''); + let showPicker = $state(false); + let showSpecials = $state(false); let textearea: HTMLTextAreaElement; - $: currentReplyToMessage = $replyToMessage; - function cancelReply() { - clearReplyToMessage(); + replyTo = undefined; } - let us = get(user); let disabled = - us == null || - session.users.find((u) => us.id === u.id) === undefined || + session.users.find((u: User) => user.id === u.id) === undefined || new Date().getTime() > session.end_time.getTime() + 3600000 || new Date().getTime() < session.start_time.getTime() - 3600000; @@ -41,28 +40,20 @@ message = message.trim(); if (message.length == 0) return; - if ($user === null) { - toastAlert($t('chatbox.sendError')); - return; - } - try { - const m = await session.sendMessage( - structuredClone($user), - message, - [...metadata], - $replyToMessage?.id || null - ); + const m = await session.sendMessage(user, message, [...metadata], replyTo?.id || null); if (m === null) { toastAlert($t('chatbox.sendError')); return; } - // Reset after sending message = ''; metadata = []; - clearReplyToMessage(); + setTimeout(() => { + autosize.update(textearea); + }, 10); + cancelReply(); } catch (error) { console.error('Failed to send message:', error); toastAlert($t('chatbox.sendError')); @@ -86,15 +77,15 @@ } </script> -<div class="flex flex-col w-full py-2 relative"> - {#if currentReplyToMessage} +<div class="flex flex-col w-full py-2 relative mb-2"> + {#if replyTo} <div class="flex items-center justify-between bg-gray-100 p-2 rounded-md mb-1 text-sm text-gray-600" > <p class="text-xs text-gray-400"> - Replying to: <span class="text-xs text-gray-400">{currentReplyToMessage.content}</span> + Replying to: <span class="text-xs text-gray-400">{replyTo.content}</span> </p> - <button class="text-xs text-blue-500 underline ml-4 cursor-pointer" on:click={cancelReply}> + <button class="text-xs text-blue-500 underline ml-4 cursor-pointer" onclick={cancelReply}> Cancel </button> </div> @@ -105,7 +96,7 @@ {#each config.SPECIAL_CHARS as char (char)} <button class="border-none" - on:click={() => { + onclick={() => { message += char; textearea.focus(); }} @@ -117,38 +108,30 @@ {/each} </ul> {/if} - - <div class="w-full flex relative"> - <textarea - bind:this={textearea} - class="flex-grow border border-gray-300 rounded-md p-2 text-base resize-none" - placeholder={disabled ? $t('chatbox.disabled') : $t('chatbox.placeholder')} - {disabled} - bind:value={message} - on:keypress={keyPress} - /> - + <div class="w-full flex items-center relative"> <div - class="absolute top-1/2 right-20 transform -translate-y-1/2 text-lg select-none cursor-pointer" - on:click={() => (showPicker = !showPicker)} + class="text-2xl select-none cursor-pointer mx-4" + onclick={() => (showPicker = !showPicker)} data-tooltip-target="tooltip-emoji" data-tooltip-placement="right" + data-riple-light="true" aria-hidden={false} role="button" tabindex="0" > 😀 </div> - <div class="relative"> <div id="tooltip-emoji" + data-tooltip="tooltip-emoji" + role="tooltip" class:hidden={!showPicker} - class="absolute z-10 bottom-16 right-0 lg:left-0 lg:right-auto hidden" + class="absolute z-10 tooltip bottom-16 right-0 lg:left-0 lg:right-auto" > <emoji-picker class="light" - on:emoji-click={(event) => { + onemoji-click={(event) => { message += event.detail.unicode; textearea.focus(); }} @@ -156,18 +139,26 @@ </emoji-picker> </div> </div> - + <textarea + bind:this={textearea} + class="flex-grow p-2 resize-none overflow-hidden py-4 pr-12 border rounded-[32px]" + placeholder={disabled ? $t('chatbox.disabled') : $t('chatbox.placeholder')} + {disabled} + bind:value={message} + use:autosize + rows={1} + onkeypress={keyPress} + ></textarea> <div - class="absolute top-1/2 right-28 kbd transform -translate-y-1/2 text-sm select-none cursor-pointer" - on:click={() => (showSpecials = !showSpecials)} + class="absolute right-28 kbd text-sm select-none cursor-pointer" + onclick={() => (showSpecials = !showSpecials)} aria-hidden={false} role="button" tabindex="0" > É </div> - - <button class="btn btn-primary rounded-none size-16" on:click={sendMessage}> + <button class="btn btn-primary rounded-full size-14 mx-4" onclick={sendMessage}> <Icon src={PaperAirplane} /> </button> </div> diff --git a/frontend/src/routes/tests/[id]/+layout.server.ts b/frontend/src/routes/tests/[id]/+layout.server.ts deleted file mode 100644 index 0c0a1405302c40587c0e1cf82529670ef4c82254..0000000000000000000000000000000000000000 --- a/frontend/src/routes/tests/[id]/+layout.server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type ServerLoad, redirect } from '@sveltejs/kit'; - -const publicly_allowed = ['/login', '/register', '/tests', '/surveys']; - -const isPublic = (path: string) => { - for (const allowed of publicly_allowed) { - if (path.startsWith(allowed)) { - return true; - } - } - return false; -}; - -export const load: ServerLoad = async ({ locals, url }) => { - if (locals.user == null || locals.user == undefined) { - if (!isPublic(url.pathname)) { - redirect(307, `/login`); - } - } - - return { - user: locals.user, - session: locals.session, - locale: locals.locale - }; -}; diff --git a/frontend/src/routes/tests/[id]/+layout.ts b/frontend/src/routes/tests/[id]/+layout.ts deleted file mode 100644 index e87df190ee4256ad1c12a6925e66302bf57eeee3..0000000000000000000000000000000000000000 --- a/frontend/src/routes/tests/[id]/+layout.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const ssr = true; - -import type { Load } from '@sveltejs/kit'; -import { loadTranslations } from '$lib/services/i18n'; - -export const load: Load = async ({ url, data }) => { - const { user, session, locale } = data; - const { pathname } = url; - - await loadTranslations(locale, pathname); - - return { - user: user, - token: session - }; -}; diff --git a/frontend/src/routes/tests/[id]/+page.svelte b/frontend/src/routes/tests/[id]/+page.svelte index 4c0f09943f9a4aeeec00ff3f2622275965d63a75..298c40a0544f83a3fac345bf6f143237c45cc687 100644 --- a/frontend/src/routes/tests/[id]/+page.svelte +++ b/frontend/src/routes/tests/[id]/+page.svelte @@ -1,23 +1,20 @@ <script lang="ts"> import { sendSurveyResponseAPI, sendSurveyResponseInfoAPI } from '$lib/api/survey'; import { getSurveyScoreAPI } from '$lib/api/survey'; - - import Survey from '$lib/types/survey.js'; import { t } from '$lib/services/i18n'; import { toastWarning } from '$lib/utils/toasts.js'; import { get } from 'svelte/store'; - import User from '$lib/types/user.js'; import type SurveyGroup from '$lib/types/surveyGroup'; import Gapfill from '$lib/components/surveys/gapfill.svelte'; + import type { PageData } from './$types'; import Consent from '$lib/components/surveys/consent.svelte'; import Dropdown from '$lib/components/surveys/dropdown.svelte'; import config from '$lib/config'; - import { formatToUTCDate } from '$lib/utils/date'; - - export let data; + import type User from '$lib/types/user'; + import type Survey from '$lib/types/survey'; - const survey: Survey = data.survey!; - const user = data.user ? User.parse(JSON.parse(data.user)) : null; + let { data }: { data: PageData } = $props(); + let { user, survey }: { user: User | null; survey: Survey } = data; let sid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); @@ -27,24 +24,24 @@ return group.questions.sort(() => Math.random() - 0.5); } - $: step = user ? 1 : 0; - $: uuid = user?.email || ''; - $: uid = user?.id || null; - $: code = ''; - $: subStep = 0; + let step = $state(user ? 2 : 0); + let uuid = $state(user?.email || ''); + let uid = $state(user?.id || null); + let code = $state(''); + let subStep = $state(0); - let currentGroupId = 0; - let currentGroup = survey.groups[currentGroupId]; - let questionsRandomized = getSortedQuestions(currentGroup); - let currentQuestionId = 0; - let currentQuestion = questionsRandomized[currentQuestionId]; - let type = currentQuestion.question.split(':')[0]; - let value = currentQuestion.question.split(':').slice(1).join(':'); - let gaps = type === 'gap' ? gapParts(currentQuestion.question) : null; + let currentGroupId = $state(0); + let currentGroup = $derived(survey.groups[currentGroupId]); + let questionsRandomized = $derived(getSortedQuestions(currentGroup)); + let currentQuestionId = $state(0); + let currentQuestion = $derived(questionsRandomized[currentQuestionId]); + let type = $derived(currentQuestion.question.split(':')[0]); + let value = $derived(currentQuestion.question.split(':').slice(1).join(':')); + let gaps = $derived(type === 'gap' ? gapParts(currentQuestion.question) : null); let soundPlayer: HTMLAudioElement; - let displayQuestionOptions: string[] = [...(currentQuestion.options ?? [])]; - shuffle(displayQuestionOptions); - let finalScore: number | null = null; + let displayQuestionOptions: string[] = $derived([...(currentQuestion.options ?? [])]); + $effect(() => shuffle(displayQuestionOptions)); + let finalScore: number | null = $state(null); let selectedOption: string; let endSurveyAnswers: { [key: string]: any } = {}; @@ -63,27 +60,20 @@ function setGroupId(id: number) { currentGroupId = id; - currentGroup = survey.groups[currentGroupId]; if (currentGroup.id < 1100) { - questionsRandomized = getSortedQuestions(currentGroup); setQuestionId(0); } } function setQuestionId(id: number) { currentQuestionId = id; - currentQuestion = questionsRandomized[currentQuestionId]; - type = currentQuestion.question.split(':')[0]; - value = currentQuestion.question.split(':').slice(1).join(':'); - gaps = type === 'gap' ? gapParts(currentQuestion.question) : null; - displayQuestionOptions = [...(currentQuestion.options ?? [])]; - shuffle(displayQuestionOptions); if (soundPlayer) soundPlayer.load(); } async function selectOption(option: string) { if ( !(await sendSurveyResponseAPI( + fetch, code, sid, uid, @@ -114,8 +104,10 @@ if ( !(await sendSurveyResponseAPI( - uuid, + fetch, + code, sid, + uid, survey.id, currentGroupId, questionsRandomized[currentQuestionId]['_id'], @@ -139,7 +131,7 @@ setGroupId(currentGroupId + 1); //special group id for end of survey questions if (currentGroup.id >= 1100) { - const scoreData = await getSurveyScoreAPI(survey.id, sid); + const scoreData = await getSurveyScoreAPI(fetch, survey.id, sid); if (scoreData) { finalScore = scoreData.score; } @@ -147,7 +139,7 @@ return; } } else { - const scoreData = await getSurveyScoreAPI(survey.id, sid); + const scoreData = await getSurveyScoreAPI(fetch, survey.id, sid); if (scoreData) { finalScore = scoreData.score; } @@ -168,19 +160,6 @@ step += 1; } - function checkUUID() { - if (!uuid) { - toastWarning(get(t)('surveys.invalidEmail')); - return; - } - if (!uuid.includes('@')) { - toastWarning(get(t)('surveys.invalidEmail')); - return; - } - - step = 1; - } - function gapParts(question: string): { text: string; gap: string | null }[] { if (!question.startsWith('gap:')) return []; @@ -202,6 +181,7 @@ subStep += 1; if (subStep == 4) { await sendSurveyResponseInfoAPI( + fetch, survey.id, sid, endSurveyAnswers.birthYear, @@ -225,13 +205,12 @@ type="text" placeholder="Code" class="input block mx-auto w-full max-w-xs border border-gray-300 rounded-md py-2 px-3 text-center" - on:keydown={(e) => e.key === 'Enter' && checkCode()} + onkeydown={(e) => e.key === 'Enter' && checkCode()} bind:value={code} /> - <!-- Button --> <button class="button mt-4 block bg-yellow-500 text-white rounded-md py-2 px-6 hover:bg-yellow-600 transition" - on:click={checkCode} + onclick={checkCode} > {$t('button.next')} </button> @@ -247,13 +226,13 @@ rights={$t('register.consent.rights')} /> <div class="form-control"> - <button class="button mt-4" on:click={() => step++}> + <button class="button mt-4" onclick={() => step++}> {$t('register.consent.ok')} </button> </div> </div> {:else if step == 2} - {#if type == 'gap'} + {#if type == 'gap' && gaps} <div class="mx-auto mt-16 center flex flex-col"> <div> {#each gaps as part} @@ -264,7 +243,7 @@ {/if} {/each} </div> - <button class="button mt-8" on:click={sendGap}>{$t('button.next')}</button> + <button class="button mt-8" onclick={sendGap}>{$t('button.next')}</button> </div> {:else} <div class="mx-auto mt-16 text-center"> @@ -282,7 +261,7 @@ <div class="mx-auto mt-16"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4"> - {#each displayQuestionOptions as option (option)} + {#each displayQuestionOptions as option, i (option)} {@const type = option.split(':')[0]} {#if type == 'dropdown'} {@const value = option.split(':')[1].split(', ')} @@ -290,8 +269,8 @@ class="select select-bordered !ml-0" id="dropdown" name="dropdown" - bind:value={option} - on:change={() => selectOption(option)} + bind:value={displayQuestionOptions[i]} + onchange={() => selectOption(option)} required > {#each value as op} @@ -306,7 +285,7 @@ type="radio" name="dropdown" value={op} - on:change={() => selectOption(op)} + onchange={() => selectOption(op)} required class="radio-button" /> @@ -317,9 +296,9 @@ {@const value = option.split(':').slice(1).join(':')} <div class="h-48 w-48 overflow-hidden rounded-lg border border-black" - on:click={() => selectOption(option)} + onclick={() => selectOption(option)} role="button" - on:keydown={() => selectOption(option)} + onkeydown={() => selectOption(option)} tabindex="0" > {#if type === 'text'} @@ -335,7 +314,14 @@ class="object-cover h-full w-full transition-transform duration-200 ease-in-out transform hover:scale-105" /> {:else if type == 'audio'} - <audio controls class="w-full" on:click|preventDefault|stopPropagation> + <audio + controls + class="w-full" + onclick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + > <source src={value} type="audio/mpeg" /> Your browser does not support the audio element. </audio> @@ -377,7 +363,7 @@ type="radio" name="gender" {value} - on:change={() => selectAnswer('gender', value)} + onchange={() => selectAnswer('gender', value)} required class="radio-button" /> @@ -418,7 +404,6 @@ </div> {/if} {:else} - <!--In case special id received not defined, can still keep going--> {(step += 1)} {/if} {:else if step == 4} diff --git a/frontend/src/routes/tests/[id]/+page.ts b/frontend/src/routes/tests/[id]/+page.ts index 7ada027448295fe88193014b3e7387c544c24c88..62c35f258fa6bd1e92334e62c421ce1c47a341e9 100644 --- a/frontend/src/routes/tests/[id]/+page.ts +++ b/frontend/src/routes/tests/[id]/+page.ts @@ -1,38 +1,24 @@ import { getSurveyAPI } from '$lib/api/survey'; import Survey from '$lib/types/survey'; -import type { Load } from '@sveltejs/kit'; +import { error, type Load } from '@sveltejs/kit'; export const ssr = false; -export const load: Load = async ({ params, parent }) => { - const data = await parent(); +export const load: Load = async ({ params, parent, fetch }) => { + const { user } = await parent(); + + if (!params.id) return error(400, 'Invalid survey ID'); + const survey_id = parseInt(params.id); - const { user, token } = data; - - if (isNaN(survey_id)) { - return { - status: 400, - body: { - error: 'Invalid survey id' - } - }; - } - - const survey = Survey.parse(await getSurveyAPI(survey_id)); - - if (!survey) { - return { - status: 404, - body: { - error: 'Survey not found' - } - }; - } + + if (isNaN(survey_id)) return error(400, 'Invalid survey ID'); + + const survey = Survey.parse(await getSurveyAPI(fetch, survey_id)); + + if (!survey) return error(404, 'Survey not found'); return { - survey_id, survey, - user, - token + user }; }; diff --git a/frontend/src/routes/tests/typing/+page.server.ts b/frontend/src/routes/tests/typing/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bdcc4ffa4d7daa8a62ce92a8bfcb493f5010a65 --- /dev/null +++ b/frontend/src/routes/tests/typing/+page.server.ts @@ -0,0 +1,7 @@ +import { redirect, type ServerLoad } from '@sveltejs/kit'; + +export const load: ServerLoad = async ({ params, locals }) => { + if (locals.user == null || locals.user == undefined) { + redirect(303, '/login?redirect=/tests/typing/' + params.id); + } +}; diff --git a/frontend/src/routes/tests/typing/+page.svelte b/frontend/src/routes/tests/typing/+page.svelte index dea8911226b03a1d8d99a48cd61e53945fec93f9..ad66eb237c3ffa9eacca42b7f8410255af5be9c0 100644 --- a/frontend/src/routes/tests/typing/+page.svelte +++ b/frontend/src/routes/tests/typing/+page.svelte @@ -2,11 +2,14 @@ import Typingtest from '$lib/components/tests/typingtest.svelte'; import { t } from '$lib/services/i18n'; - let finished = false; + let finished = $state(false); + + let { data } = $props(); + let user = data.user!; </script> {#if finished} <p>{$t('surveys.complete')}</p> {:else} - <Typingtest onFinish={() => (finished = true)} /> + <Typingtest {user} onFinish={() => (finished = true)} /> {/if} diff --git a/frontend/src/routes/timeslots/+page.server.ts b/frontend/src/routes/timeslots/+page.server.ts deleted file mode 100644 index 9e9464e76bdedf2c759c84b269c84ffe49eda503..0000000000000000000000000000000000000000 --- a/frontend/src/routes/timeslots/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type ServerLoad, redirect } from '@sveltejs/kit'; - -export const load: ServerLoad = async ({ locals }) => { - if (locals.user == null || locals.user == undefined) { - redirect(307, `/login`); - } -}; diff --git a/frontend/src/routes/timeslots/+page.svelte b/frontend/src/routes/timeslots/+page.svelte deleted file mode 100644 index f175825e28c4cd451388376c97ba62f8098b480d..0000000000000000000000000000000000000000 --- a/frontend/src/routes/timeslots/+page.svelte +++ /dev/null @@ -1,27 +0,0 @@ -<script lang="ts"> - import { onMount } from 'svelte'; - import { t } from '$lib/services/i18n'; - import Timeslots from '$lib/components/users/timeslots.svelte'; - import { getUsersAPI } from '$lib/api/users'; - import User, { users } from '$lib/types/user'; - import AvailableTutors from '$lib/components/users/availableTutors.svelte'; - - onMount(async () => { - User.parseAll(await getUsersAPI()); - }); - - let timeslots = 0n; - - $: filteredUsers = $users.filter((user) => { - if (user.availability === 0n) return false; - if (timeslots === 0n) return true; - - return user.availability & timeslots; - }); -</script> - -<div class="w-4/5 m-auto mt-4"> - <h2 class="my-4 text-xl">{$t('timeslots.availabilities')}</h2> - <Timeslots bind:timeslots /> - <AvailableTutors users={filteredUsers} {timeslots} /> -</div> diff --git a/frontend/src/routes/tutor/+layout.server.ts b/frontend/src/routes/tutor/+layout.server.ts index a03ab77e6b293c8c5432ec77e535dda144160890..742f936e9ed4f60fe8fe0aa7e1955c5d8d9cd00f 100644 --- a/frontend/src/routes/tutor/+layout.server.ts +++ b/frontend/src/routes/tutor/+layout.server.ts @@ -5,7 +5,7 @@ export const load: ServerLoad = async ({ locals, url }) => { if (url.pathname.startsWith('/tutor/register')) { return {}; } - redirect(307, '/login'); + redirect(303, '/login'); } const user = JSON.parse(locals.user); diff --git a/frontend/src/routes/tutor/register/+page.svelte b/frontend/src/routes/tutor/register/+page.svelte index 9b893472cb3d949f52e9aebe48e8dbb5ffcf2587..82b3f07754538ad47776a9ec530f365e05a46d29 100644 --- a/frontend/src/routes/tutor/register/+page.svelte +++ b/frontend/src/routes/tutor/register/+page.svelte @@ -1,37 +1,36 @@ <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 User from '$lib/types/user'; import { getUsersAPI, patchUserAPI, getUserContactsAPI } from '$lib/api/users'; import { Icon, Envelope, Key, UserCircle, Calendar, QuestionMarkCircle } from 'svelte-hero-icons'; import Typingtest from '$lib/components/tests/typingtest.svelte'; import { formatToUTCDate } from '$lib/utils/date'; + import type { PageData } from './$types'; - let current_step = 0; + let { data }: { data: PageData } = $props(); + let user = data.user; - $: message = ''; + let current_step = $state(0); - onMount(async () => { - const u = get(user); + let message = $state(''); - if (u == null) { + onMount(async () => { + if (user == null) { current_step = 1; return; } - User.parseAll(await getUsersAPI()); + User.parseAll(await getUsersAPI(fetch)); - if (!u.home_language || !u.target_language || !u.birthdate || !u.gender) { + if (!user.home_language || !user.target_language || !user.birthdate || !user.gender) { current_step = 3; return; } - const contacts = User.parseAll(await getUserContactsAPI(u.id)); + const contacts = User.parseAll(await getUserContactsAPI(fetch, user.id)); if (contacts.length == 0) { current_step = 4; @@ -95,7 +94,7 @@ } async function onData() { - const user_id = get(user)?.id; + const user_id = user.id; if (!user_id) { toastAlert('Failed to get current user ID'); @@ -107,7 +106,7 @@ return; } - const res = await patchUserAPI(user_id, { + const res = await patchUserAPI(fetch, user_id, { ui_language, home_language, birthdate, @@ -128,7 +127,7 @@ return; } - const res = $user?.setAvailability(timeslots, calcom_link); + const res = user.setAvailability(timeslots, calcom_link); if (!res) return; diff --git a/frontend/src/routes/tutor/timeslots/+page.svelte b/frontend/src/routes/tutor/timeslots/+page.svelte deleted file mode 100644 index c38fea0ba1f7114fc9e25ee4dda8b217bc7ad607..0000000000000000000000000000000000000000 --- a/frontend/src/routes/tutor/timeslots/+page.svelte +++ /dev/null @@ -1,122 +0,0 @@ -<script lang="ts"> - import { onMount } from 'svelte'; - import { t } from '$lib/services/i18n'; - import Timeslots from '$lib/components/users/timeslots.svelte'; - import { user } from '$lib/types/user'; - import { toastWarning } from '$lib/utils/toasts'; - import { Icon, Calendar, QuestionMarkCircle } from 'svelte-hero-icons'; - - $: lastTimeslots = 0n; - $: timeslots = 0n; - $: calcom_link = ''; - $: last_calcom_link = ''; - let ready = false; - let sent = false; - - onMount(async () => { - if ($user != null) { - timeslots = $user.availability; - lastTimeslots = timeslots; - calcom_link = $user.calcom_link || ''; - last_calcom_link = calcom_link; - ready = true; - } - }); - - async function send() { - if (!calcom_link || calcom_link.length == 0) { - toastWarning($t('timeslots.calcomWarning')); - return; - } - - const res = $user?.setAvailability(timeslots, calcom_link); - - if (!res) return; - - lastTimeslots = timeslots; - last_calcom_link = calcom_link; - sent = true; - setTimeout(() => (sent = false), 3000); - } -</script> - -<svelte:head> - <script> - (function (C, A, L) { - let p = function (a, ar) { - a.q.push(ar); - }; - let d = C.document; - C.Cal = - C.Cal || - function () { - let cal = C.Cal; - let ar = arguments; - if (!cal.loaded) { - cal.ns = {}; - cal.q = cal.q || []; - d.head.appendChild(d.createElement('script')).src = A; - cal.loaded = true; - } - if (ar[0] === L) { - const api = function () { - p(api, arguments); - }; - const namespace = ar[1]; - api.q = api.q || []; - if (typeof namespace === 'string') { - cal.ns[namespace] = cal.ns[namespace] || api; - p(cal.ns[namespace], ar); - p(cal, ['initNamespace', namespace]); - } else p(cal, ar); - return; - } - p(cal, ar); - }; - })(window, 'https://app.cal.com/embed/embed.js', 'init'); - Cal('init'); - </script> -</svelte:head> - -<div class="max-w-screen-md mx-auto p-2"> - <h2 class="my-4 text-xl">{$t('timeslots.setAvailabilities')}</h2> - {#if ready} - <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="username/tutoring" - bind:value={calcom_link} - /> - </div> - </div> - - <div class="form-control mt-4"> - <button class="button" data-cal-link={calcom_link}>{$t('button.tryit')}</button> - <button - class="button mt-4" - disabled={sent || (lastTimeslots === timeslots && calcom_link === last_calcom_link)} - on:click={send}>{$t(sent ? 'button.updated' : 'button.update')}</button - > - </div> - {/if} -</div> diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 180277378b4e369a23ddffb6333720078fb24b97..a76f2ab5f34f065df0bc5571047c14c0788ac7b8 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -10,11 +10,6 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "baseUrl": "./", - "paths": { - "$lib/*": ["src/lib/*"], - "$routes/*": ["src/routes/*"] - }, "moduleResolution": "node", "target": "ESNext", "module": "ESNext", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4fe702bfd25658ce68b5a7faad57c7c1a3e89818..fd70ec442a25fdcb9c262229952f779f547f7f20 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,14 +12,5 @@ export default defineConfig({ optimizeDeps: { exclude: ['emoji-picker-element'], include: ['svelte-gravatar', 'svelte-waypoint'] - }, - server: { - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, - secure: false - } - } } });