commit 0526ecc057dc483c078badb707814f07e4dfd031 Author: Jérémy Munsch Date: Sun Aug 10 12:00:35 2025 +0200 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..89bcd48 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f3fdb69 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# - APP VARS +PUBLIC_APP_NAME="Z3" + +# - BACKEND VARS +BACKEND_URL=http://localhost + +# Size in KB (51200Ko => 50Mo) +BODY_SIZE_LIMIT=51200 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94b0c04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,287 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,svelte,visualstudiocode,vercel,intellij+all,dotenv +# Edit at https://www.toptal.com/developers/gitignore?templates=node,svelte,visualstudiocode,vercel,intellij+all,dotenv + +### dotenv ### +.env + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Svelte ### +# gitignore template for the SvelteKit, frontend web component framework +# website: https://kit.svelte.dev/ + +.svelte-kit/ +package + +### Vercel ### +.vercel + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/node,svelte,visualstudiocode,vercel,intellij+all,dotenv + +# Output +.output +.vercel +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# VSCODE personal launch +.vscode/launch.json +.vscode/tasks.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..dd2abde --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,42 @@ +image: quay.io/buildah/stable + +stages: + - release + - build + +build-staging: + stage: build + variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:staging + before_script: + - buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - buildah bud -t "$IMAGE_TAG" . + - buildah push "$IMAGE_TAG" + only: + - main + +branch-release: + image: docker.io/bitnami/git + variables: + GIT_STRATEGY: clone + stage: release + script: + # create branch ; if the tag is v2024.01, the branch will be rel/2024.01 (without the v) + - git switch -c rel/${CI_COMMIT_TAG:1} + - git push origin rel/${CI_COMMIT_TAG:1} + rules: + # only for tags like v2024.01, v2024.01.01, v2024.01-1, v2024.01.01-1 + - if: $CI_COMMIT_TAG =~ /^v\d{4}\.\d{2}(\.\d{2})?(-\d+)?$/ + +build-release: + stage: build + variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:latest + before_script: + - buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - buildah bud -t "$IMAGE_TAG" . + - buildah push "$IMAGE_TAG" + only: + - /^rel\// diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6430ce5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..aaaf66a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,66 @@ +{ + "typescript.experimental.updateImportsOnPaste": true, + "[html]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "editor.defaultFormatter": "vscode.html-language-features", + "editor.formatOnSave": true + }, + "[scss]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "editor.defaultFormatter": "stylelint.vscode-stylelint", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always", + "source.organizeImports": "always" + }, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[svelte]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true + }, + "svelte.plugin.svelte.format.enable": false, + "svelte.plugin.typescript.codeActions.enable": false, + "svelte.plugin.css.completions.enable": true, + "svelte.enable-ts-plugin": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "svelte", + "typescript" + ], + "html.format.unformattedContentDelimiter": "", + "html.customData": [ + "./node_modules/@shoelace-style/shoelace/dist/vscode.html-custom-data.json" + ], + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#b1d38b", + "activityBar.background": "#b1d38b", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#4683b9", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#b1d38b", + "statusBar.background": "#98c566", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#7fb444", + "statusBarItem.remoteBackground": "#98c566", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#98c566", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#98c56699", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#98c566" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1be661b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,188 @@ +## 1.1.0 (2025-03-27) + +### Features + +* Add download btn on statistics graphics ([1106898](https://gitlab.pegase.tech/z3/z3-frontend/commit/1106898adb9bebb78e78eb86a478b66939c76bcd)) +* Add most used referrers by brand ([8ce2db7](https://gitlab.pegase.tech/z3/z3-frontend/commit/8ce2db771d3c880ed55989b6c54ac8342d48194e)) +* Add most used source by brand statistic ([53a7a29](https://gitlab.pegase.tech/z3/z3-frontend/commit/53a7a296fd6e361f9626be86b21d51e63c794d98)) +* WIP Application Statistics with associated help section ([c30e2d5](https://gitlab.pegase.tech/z3/z3-frontend/commit/c30e2d56d0a2067b718a9ae8f8ec3ccf61f5a0b1)) + +### Bug Fixes + +* :bug: BrandService: Prevent requesting more brands on getAll if returned brands amount is not equal to perPage ([324f5c8](https://gitlab.pegase.tech/z3/z3-frontend/commit/324f5c8700f1e3aa6556144cfce9d5265f391489)) +* :bug: Fixed search links debounce ([ab0e60a](https://gitlab.pegase.tech/z3/z3-frontend/commit/ab0e60a76e50d9aed34fc79361eaa259d6e5f894)) +* :lipstick: Fix link input search appeareance ([f4fac73](https://gitlab.pegase.tech/z3/z3-frontend/commit/f4fac73ac1ddd54ad57ddd8a266751c8457e2b2b)) +* :lipstick: Fix some UI issues on indexes view background color of the action column ([488e522](https://gitlab.pegase.tech/z3/z3-frontend/commit/488e522c3db2a56cf29e13089af6b9e212c80e58)) +* Add missing help page title translation for visits ([9f4bf8e](https://gitlab.pegase.tech/z3/z3-frontend/commit/9f4bf8e3af077f5d4631a6b617de019a59065a9d)) +* Add missing padding X on login inputs ([75cf351](https://gitlab.pegase.tech/z3/z3-frontend/commit/75cf35167303de9dafd38206a668cbcffa605520)) +* Add missing statistic canvas title for unique visitors per catalog ([6a0898d](https://gitlab.pegase.tech/z3/z3-frontend/commit/6a0898dc7ab6be017281d1545609dab01ea8be85)) +* fully translate main menu ([e5f2fbc](https://gitlab.pegase.tech/z3/z3-frontend/commit/e5f2fbc1dee6c01d74ed1835b362e52d4dc0a806)) +* link.viewAny page responsiveness ([bbb9a81](https://gitlab.pegase.tech/z3/z3-frontend/commit/bbb9a81d84477b7ef55aa6cfb0aefc12c1199857)) +* remove unwanted padding on 'container-fluid' class ([ab5ab30](https://gitlab.pegase.tech/z3/z3-frontend/commit/ab5ab301f7d3dc41e259f929bc1391afc806e9cf)) +* tabs on link import and export pages ([72d08fa](https://gitlab.pegase.tech/z3/z3-frontend/commit/72d08fa786e9340ed9e8f4ff0b9f8bd5fe46ed1e)) +* vite config __dirname was not found ([255c220](https://gitlab.pegase.tech/z3/z3-frontend/commit/255c2203311ae6ea44e67917de93a1447e9f2798)) +* wrong previous button placements on sompte pages ([2ff463e](https://gitlab.pegase.tech/z3/z3-frontend/commit/2ff463eb16a0d18f238e6ad88070a37705b8ee84)) +* Wrong title on top lists statistics ([4fababd](https://gitlab.pegase.tech/z3/z3-frontend/commit/4fababd03c36e1740f3528e5cf40c0c78ba3db2e)) +## 1.0.5 (2025-03-05) + +### Features + +* :sparkles: Add FQDN to brand for crud actions ([42d4ef1](https://gitlab.pegase.tech/z3/z3-frontend/commit/42d4ef189e386147cd8776db8a827ab94ea33f01)) +* Closes [#6](https://gitlab.pegase.tech/z3/z3-frontend/issues/6) add shortcut ton links from catalogs and tags ([ae560d2](https://gitlab.pegase.tech/z3/z3-frontend/commit/ae560d240d30afce9adddfa2d7b6a253b812c8e5)) +* Introcude link validity third state ([839a5cc](https://gitlab.pegase.tech/z3/z3-frontend/commit/839a5cc5939893f68c26cc214d8da0b5dcb14a5b)) +* Old values can now handle arrays ([9052568](https://gitlab.pegase.tech/z3/z3-frontend/commit/9052568779c8de589f63c03d2f133f443f549c23)) + +### Bug Fixes + +* :bug: Closes [#3](https://gitlab.pegase.tech/z3/z3-frontend/issues/3) Missing user menu on mobile ([349c250](https://gitlab.pegase.tech/z3/z3-frontend/commit/349c250e74cefd7b0a5ecbeb2255d271a0a147ad)) +* :bug: Closes [#7](https://gitlab.pegase.tech/z3/z3-frontend/issues/7) with wrong button tooltips ([2ba5112](https://gitlab.pegase.tech/z3/z3-frontend/commit/2ba51120936191962c68aedd19c5daef540ebf2b)) +* :bug: Prevent double invalidate all on brand switch ([1d5f790](https://gitlab.pegase.tech/z3/z3-frontend/commit/1d5f7909457caad471b1d744303fadf82b6e5851)) +* :bug: Refresh stats for each stat subpage on brand selector change ([05eaf2a](https://gitlab.pegase.tech/z3/z3-frontend/commit/05eaf2ab6581dc612abc0397b8fb0141a4a235d1)) +* Closes [#2](https://gitlab.pegase.tech/z3/z3-frontend/issues/2) Fix all brands stats refresh on state change ([7124af5](https://gitlab.pegase.tech/z3/z3-frontend/commit/7124af501cf3bd12692eda924e00fd4ef05d4921)) +* onCopyResource argument was url ([6843ce0](https://gitlab.pegase.tech/z3/z3-frontend/commit/6843ce093b327fde5e1228445cfed537c8bdbdda)) +* Write better statistics wordings ([1807ab8](https://gitlab.pegase.tech/z3/z3-frontend/commit/1807ab8a890466d959b1043c36774048a827d9c1)) +## 1.0.3 (2025-01-22) + +### Features + +* :sparkles: Allow to set ReportingSubscriptions for any user (Invalid links alerts) ([3536bc7](https://gitlab.pegase.tech/z3/z3-frontend/commit/3536bc7a127d5f2bc3a8317ca47000499f063628)) +* :sparkles: Validate link feature ([b7452cf](https://gitlab.pegase.tech/z3/z3-frontend/commit/b7452cf3bd121558926ba225af92f3d3743ec463)) +* Add initial Helper page ([400b5d4](https://gitlab.pegase.tech/z3/z3-frontend/commit/400b5d4376d01b07163ec37856fc9246ddb3e094)) +* Added inital Help ([badfec9](https://gitlab.pegase.tech/z3/z3-frontend/commit/badfec9500b7dcadb935edd73ee58b9221ba1c1b)) +* Properly display import information ([31073e7](https://gitlab.pegase.tech/z3/z3-frontend/commit/31073e77563720d27f171bf1ecbe9f3303b189cf)) + +### Bug Fixes + +* :bug: Select a default active brand when user is admin ([7e196bc](https://gitlab.pegase.tech/z3/z3-frontend/commit/7e196bc7cdf454f27cdb69af57d65a88e30063fd)) +## 1.0.2 (2024-12-10) + +### Features + +* Add loading and keep focus on link search ([3fa9ffe](https://gitlab.pegase.tech/z3/z3-frontend/commit/3fa9ffed9b8507194aad9d3ce7b47d6a61b83435)) +* Allow to set tags while importing links ([79307c7](https://gitlab.pegase.tech/z3/z3-frontend/commit/79307c78f311d9ba974db45980d3434e03fc8e63)) + +### Bug Fixes + +* :bug: Handle api error on hooks.erver load and clear session when calli backend ([e716787](https://gitlab.pegase.tech/z3/z3-frontend/commit/e716787c9a0ff49fb6fd69ffbe705873e29ded94)) +* :lipstick: Fix menu responsiveness ([981f740](https://gitlab.pegase.tech/z3/z3-frontend/commit/981f7406c0dcc029b383a89419a5c8f5c55e2a1f)) +* :lipstick: Fix some colors issues and change light theme ([1d3e6db](https://gitlab.pegase.tech/z3/z3-frontend/commit/1d3e6db56ae689b4b0af74fd0f1b974688b06b8c)) +* :lipstick: page min with removal for Chrome compat with ascensors ([ba4c95a](https://gitlab.pegase.tech/z3/z3-frontend/commit/ba4c95a7ca43e90a24a20687fa346194e39872c5)) +* correct password change rate limiter ([ef69420](https://gitlab.pegase.tech/z3/z3-frontend/commit/ef6942014bb9eaa67a7be3cf0cb105fab61ac630)) +* Create link, change source input to text instead of url ([c6aa313](https://gitlab.pegase.tech/z3/z3-frontend/commit/c6aa313f91ff267e3951e233e3d756fdb87f91e9)) +* Export: Prevent sveltekit error on navigating to an API URL ([19940ed](https://gitlab.pegase.tech/z3/z3-frontend/commit/19940ed2e4727f2cf09039bdb6f5ac4a9a92fa70)) +* File import fix ([93dd594](https://gitlab.pegase.tech/z3/z3-frontend/commit/93dd59428226dd69049c95317968d5ab99832a7b)) +* flash message circle that prevents navigating after a create or update ([c906d31](https://gitlab.pegase.tech/z3/z3-frontend/commit/c906d311e47cfe6475e6bea85593877941d18545)) +* Import was returning wrong message ([175cb38](https://gitlab.pegase.tech/z3/z3-frontend/commit/175cb38a074a0f7d7fa445125fe6d0db7834d154)) +* Link import correctly format output message ([2bba644](https://gitlab.pegase.tech/z3/z3-frontend/commit/2bba644669a5a4809e5bcd90c501a969c38f4a97)) +* Remove forgotten password link on login screen ([1572b20](https://gitlab.pegase.tech/z3/z3-frontend/commit/1572b20a32d738a7e6e1c6ea60eabe19ef9497c2)) +* reset forms after link import ([f01f776](https://gitlab.pegase.tech/z3/z3-frontend/commit/f01f7769d8a9a94f7b7ac70234e03755f4ac8b66)) +* set first active brand available for newly loggedin user ([d235394](https://gitlab.pegase.tech/z3/z3-frontend/commit/d2353947e9d7686a6facc0ac7cd94cbc35ba7681)) +* Statistics export was getting empty files ([ce520a6](https://gitlab.pegase.tech/z3/z3-frontend/commit/ce520a66b9260b0ec7a1cdb39e95d8dee611c88e)) +## 1.0.1 (2024-12-08) + +### Features + +* :sparkles: Allo to assign any user some brands, these will be used to change the current app context ([1140c3b](https://gitlab.pegase.tech/z3/z3-frontend/commit/1140c3b01734ae397d0079259cf84c66bcced443)) +* :sparkles: Allow user to change its password ([a77d1d3](https://gitlab.pegase.tech/z3/z3-frontend/commit/a77d1d322571906278da752beaff8ec64d0f071b)) +* :sparkles: User password reset ([42cf00f](https://gitlab.pegase.tech/z3/z3-frontend/commit/42cf00f36ab067898eb20c6d05cd6487e476dfea)) +* Add global statistics ([bf0f5b8](https://gitlab.pegase.tech/z3/z3-frontend/commit/bf0f5b897e368413d1912a0237265ec50c646be0)) +* Display brand stats on home page ([7b23276](https://gitlab.pegase.tech/z3/z3-frontend/commit/7b23276c5e5587e3cfae995f16d12d3da45d2e87)) +* Link statistics and export ([d12ee89](https://gitlab.pegase.tech/z3/z3-frontend/commit/d12ee89ac89acc212ef543d9e0131c15931b43b8)) + +### Bug Fixes + +* :lipstick: Message component text was not responsive ([da4a927](https://gitlab.pegase.tech/z3/z3-frontend/commit/da4a9272d815848e1910d83198da23ea08bae26c)) +* back navigation for import and export links ([2d8a530](https://gitlab.pegase.tech/z3/z3-frontend/commit/2d8a530c0e8e41ee40d95a26fdb97592ebe73212)) +* Bearer token was a singleton ... ([faaa44f](https://gitlab.pegase.tech/z3/z3-frontend/commit/faaa44f97d2dbb997b81e93153e0281ebcb694f2)) +* Brand crud navigation fixes ([466ed97](https://gitlab.pegase.tech/z3/z3-frontend/commit/466ed97e78aa221691694236b0905a8c4c33ae9d)) +* Catalog crud fixes ([161b6c2](https://gitlab.pegase.tech/z3/z3-frontend/commit/161b6c2c5c63bbf0049e999ccbdaf55622e4bcbc)) +* Set Accept-Language header to translate quarkus messages automatically ([956ef24](https://gitlab.pegase.tech/z3/z3-frontend/commit/956ef24ecf7d59dbd0b3f1bd49d582d52efbf9f2)) +* Users.show Hide user delete button for and admin own account ([c703885](https://gitlab.pegase.tech/z3/z3-frontend/commit/c703885642a12ac4c1e16d8e51d283d3d4efce2e)) +* Users.show Hide user reset form for user own profile ([0649c18](https://gitlab.pegase.tech/z3/z3-frontend/commit/0649c18134af26f8d3c577eb0b1b3fd0c7499ac5)) +## 0.0.1 (2024-09-27) +## [1.0.5](https://gitlab.pegase.tech/z3/z3-frontend/compare/1.0.4...1.0.5) (2025-03-05) + +### Features + +* :sparkles: Add FQDN to brand for crud actions ([42d4ef1](https://gitlab.pegase.tech/z3/z3-frontend/commit/42d4ef189e386147cd8776db8a827ab94ea33f01)) +* Old values can now handle arrays ([9052568](https://gitlab.pegase.tech/z3/z3-frontend/commit/9052568779c8de589f63c03d2f133f443f549c23)) + +### Bug Fixes + +* onCopyResource argument was url ([6843ce0](https://gitlab.pegase.tech/z3/z3-frontend/commit/6843ce093b327fde5e1228445cfed537c8bdbdda)) +* Write better statistics wordings ([1807ab8](https://gitlab.pegase.tech/z3/z3-frontend/commit/1807ab8a890466d959b1043c36774048a827d9c1)) + +## [1.0.4](https://gitlab.pegase.tech/z3/z3-frontend/compare/1.0.3...1.0.4) (2025-02-19) + +### Features + +* Introcude link validity third state ([839a5cc](https://gitlab.pegase.tech/z3/z3-frontend/commit/839a5cc5939893f68c26cc214d8da0b5dcb14a5b)) +* Closes [#6](https://gitlab.pegase.tech/z3/z3-frontend/issues/6) add shortcut ton links from catalogs and tags ([ae560d2](https://gitlab.pegase.tech/z3/z3-frontend/commit/ae560d240d30afce9adddfa2d7b6a253b812c8e5)) + +### Bug Fixes + +* :bug: Closes [#3](https://gitlab.pegase.tech/z3/z3-frontend/issues/3) Missing user menu on mobile ([349c250](https://gitlab.pegase.tech/z3/z3-frontend/commit/349c250e74cefd7b0a5ecbeb2255d271a0a147ad)) +* :bug: Prevent double invalidate all on brand switch ([1d5f790](https://gitlab.pegase.tech/z3/z3-frontend/commit/1d5f7909457caad471b1d744303fadf82b6e5851)) +* :bug: Refresh stats for each stat subpage on brand selector change ([05eaf2a](https://gitlab.pegase.tech/z3/z3-frontend/commit/05eaf2ab6581dc612abc0397b8fb0141a4a235d1)) +* Closes [#2](https://gitlab.pegase.tech/z3/z3-frontend/issues/2) Fix all brands stats refresh on state change ([7124af5](https://gitlab.pegase.tech/z3/z3-frontend/commit/7124af501cf3bd12692eda924e00fd4ef05d4921)) +* :bug: Closes [#7](https://gitlab.pegase.tech/z3/z3-frontend/issues/7) with wrong button tooltips ([2ba5112](https://gitlab.pegase.tech/z3/z3-frontend/commit/2ba51120936191962c68aedd19c5daef540ebf2b)) + +## [1.0.3](https://gitlab.pegase.tech/z3/z3-frontend/compare/1.0.2...1.0.3) (2025-01-22) + +### Features + +* :sparkles: Allow to set ReportingSubscriptions for any user (Invalid links alerts) ([3536bc7](https://gitlab.pegase.tech/z3/z3-frontend/commit/3536bc7a127d5f2bc3a8317ca47000499f063628)) +* :sparkles: Validate link feature ([b7452cf](https://gitlab.pegase.tech/z3/z3-frontend/commit/b7452cf3bd121558926ba225af92f3d3743ec463)) +* Add initial Helper page ([400b5d4](https://gitlab.pegase.tech/z3/z3-frontend/commit/400b5d4376d01b07163ec37856fc9246ddb3e094)) +* Added inital Help ([badfec9](https://gitlab.pegase.tech/z3/z3-frontend/commit/badfec9500b7dcadb935edd73ee58b9221ba1c1b)) +* Properly display import information ([31073e7](https://gitlab.pegase.tech/z3/z3-frontend/commit/31073e77563720d27f171bf1ecbe9f3303b189cf)) + +### Bug Fixes + +* :bug: Select a default active brand when user is admin ([7e196bc](https://gitlab.pegase.tech/z3/z3-frontend/commit/7e196bc7cdf454f27cdb69af57d65a88e30063fd)) +## [1.0.2](https://gitlab.pegase.tech/z3/z3-frontend/compare/1.0.1...1.0.2) (2024-12-10) + +### Features + +* Add loading and keep focus on link search ([3fa9ffe](https://gitlab.pegase.tech/z3/z3-frontend/commit/3fa9ffed9b8507194aad9d3ce7b47d6a61b83435)) +* Allow to set tags while importing links ([79307c7](https://gitlab.pegase.tech/z3/z3-frontend/commit/79307c78f311d9ba974db45980d3434e03fc8e63)) + +### Bug Fixes + +* :bug: Handle api error on hooks.erver load and clear session when calli backend ([e716787](https://gitlab.pegase.tech/z3/z3-frontend/commit/e716787c9a0ff49fb6fd69ffbe705873e29ded94)) +* :lipstick: Fix menu responsiveness ([981f740](https://gitlab.pegase.tech/z3/z3-frontend/commit/981f7406c0dcc029b383a89419a5c8f5c55e2a1f)) +* :lipstick: Fix some colors issues and change light theme ([1d3e6db](https://gitlab.pegase.tech/z3/z3-frontend/commit/1d3e6db56ae689b4b0af74fd0f1b974688b06b8c)) +* :lipstick: page min with removal for Chrome compat with ascensors ([ba4c95a](https://gitlab.pegase.tech/z3/z3-frontend/commit/ba4c95a7ca43e90a24a20687fa346194e39872c5)) +* correct password change rate limiter ([ef69420](https://gitlab.pegase.tech/z3/z3-frontend/commit/ef6942014bb9eaa67a7be3cf0cb105fab61ac630)) +* Create link, change source input to text instead of url ([c6aa313](https://gitlab.pegase.tech/z3/z3-frontend/commit/c6aa313f91ff267e3951e233e3d756fdb87f91e9)) +* Export: Prevent sveltekit error on navigating to an API URL ([19940ed](https://gitlab.pegase.tech/z3/z3-frontend/commit/19940ed2e4727f2cf09039bdb6f5ac4a9a92fa70)) +* File import fix ([93dd594](https://gitlab.pegase.tech/z3/z3-frontend/commit/93dd59428226dd69049c95317968d5ab99832a7b)) +* flash message circle that prevents navigating after a create or update ([c906d31](https://gitlab.pegase.tech/z3/z3-frontend/commit/c906d311e47cfe6475e6bea85593877941d18545)) +* Import was returning wrong message ([175cb38](https://gitlab.pegase.tech/z3/z3-frontend/commit/175cb38a074a0f7d7fa445125fe6d0db7834d154)) +* Link import correctly format output message ([2bba644](https://gitlab.pegase.tech/z3/z3-frontend/commit/2bba644669a5a4809e5bcd90c501a969c38f4a97)) +* Remove forgotten password link on login screen ([1572b20](https://gitlab.pegase.tech/z3/z3-frontend/commit/1572b20a32d738a7e6e1c6ea60eabe19ef9497c2)) +* reset forms after link import ([f01f776](https://gitlab.pegase.tech/z3/z3-frontend/commit/f01f7769d8a9a94f7b7ac70234e03755f4ac8b66)) +* set first active brand available for newly loggedin user ([d235394](https://gitlab.pegase.tech/z3/z3-frontend/commit/d2353947e9d7686a6facc0ac7cd94cbc35ba7681)) +* Statistics export was getting empty files ([ce520a6](https://gitlab.pegase.tech/z3/z3-frontend/commit/ce520a66b9260b0ec7a1cdb39e95d8dee611c88e)) +## [1.0.1](https://gitlab.pegase.tech/z3/z3-frontend/compare/0.0.1...1.0.1) (2024-12-08) + +### Features + +* :sparkles: Allo to assign any user some brands, these will be used to change the current app context ([1140c3b](https://gitlab.pegase.tech/z3/z3-frontend/commit/1140c3b01734ae397d0079259cf84c66bcced443)) +* :sparkles: Allow user to change its password ([a77d1d3](https://gitlab.pegase.tech/z3/z3-frontend/commit/a77d1d322571906278da752beaff8ec64d0f071b)) +* :sparkles: User password reset ([42cf00f](https://gitlab.pegase.tech/z3/z3-frontend/commit/42cf00f36ab067898eb20c6d05cd6487e476dfea)) +* Add global statistics ([bf0f5b8](https://gitlab.pegase.tech/z3/z3-frontend/commit/bf0f5b897e368413d1912a0237265ec50c646be0)) +* Display brand stats on home page ([7b23276](https://gitlab.pegase.tech/z3/z3-frontend/commit/7b23276c5e5587e3cfae995f16d12d3da45d2e87)) +* Link statistics and export ([d12ee89](https://gitlab.pegase.tech/z3/z3-frontend/commit/d12ee89ac89acc212ef543d9e0131c15931b43b8)) + +### Bug Fixes + +* :lipstick: Message component text was not responsive ([da4a927](https://gitlab.pegase.tech/z3/z3-frontend/commit/da4a9272d815848e1910d83198da23ea08bae26c)) +* back navigation for import and export links ([2d8a530](https://gitlab.pegase.tech/z3/z3-frontend/commit/2d8a530c0e8e41ee40d95a26fdb97592ebe73212)) +* Bearer token was a singleton ... ([faaa44f](https://gitlab.pegase.tech/z3/z3-frontend/commit/faaa44f97d2dbb997b81e93153e0281ebcb694f2)) +* Brand crud navigation fixes ([466ed97](https://gitlab.pegase.tech/z3/z3-frontend/commit/466ed97e78aa221691694236b0905a8c4c33ae9d)) +* Catalog crud fixes ([161b6c2](https://gitlab.pegase.tech/z3/z3-frontend/commit/161b6c2c5c63bbf0049e999ccbdaf55622e4bcbc)) +* Set Accept-Language header to translate quarkus messages automatically ([956ef24](https://gitlab.pegase.tech/z3/z3-frontend/commit/956ef24ecf7d59dbd0b3f1bd49d582d52efbf9f2)) +* Users.show Hide user delete button for and admin own account ([c703885](https://gitlab.pegase.tech/z3/z3-frontend/commit/c703885642a12ac4c1e16d8e51d283d3d4efce2e)) +* Users.show Hide user reset form for user own profile ([0649c18](https://gitlab.pegase.tech/z3/z3-frontend/commit/0649c18134af26f8d3c577eb0b1b3fd0c7499ac5)) +## 0.0.1 (2024-09-27) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..108b285 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine AS build + +WORKDIR /app +COPY . . +# Needed for Vite to define the environment variables that are importable in the code +COPY .env.example .env +RUN npm install +RUN npm run build + +FROM node:22-alpine AS production +WORKDIR /app +RUN rm -rf ./* +COPY --from=build /app/package.json . +COPY --from=build /app/build . +ENV NODE_ENV=production +RUN npm install --omit=dev +# For uploading large files, set the body size limit to Infinity +# Size limit is delegated to SvelteKit handle function. +# https://svelte.dev/docs/kit/adapter-node#Environment-variables-BODY_SIZE_LIMIT +ENV BODY_SIZE_LIMIT=Infinity +CMD ["node", "index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..59ee988 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Z3 – A link to the path + +*Z3* est un raccourcisseur d'URL. + +Le nom *Z3* est un clin d'œil à *Zelda 3 : A Link to the Past*, un jeu vidéo sorti en 1991 sur Super +Nintendo. + +## Configuration + +### APP NAME + +- `PUBLIC_APP_NAME` est utilisé à des fins d'affichage. + +### BACKEND + +- L'url du backend doit être renseigné `BACKEND_URL` + Si il ne l'est pas ou si le backend n'est pas joignable sera génèrera une erreur `500` de la part de `Sveltekit` + +### UPLOADS + +**! Cette valeur dois être raccord avec la partie back (quarkus) !** + +- La taille max des uploads peut être définie `BODY_SIZE_LIMIT` -> `BODY_SIZE_LIMIT=51200` + Elle est en `kilobytes`, example (`51200Ko` => `50Mo`) + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e5a2581 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,81 @@ +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...ts.configs.stylistic, + ...svelte.configs['flat/recommended'], + stylistic.configs['recommended-flat'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['*.svelte', '**/*.svelte'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'error', + // Security + 'svelte/no-at-html-tags': 'error', + 'svelte/no-target-blank': 'warn', + // Best pratices + 'svelte/button-has-type': 'error', + 'svelte/no-at-debug-tags': 'error', + 'svelte/no-ignored-unsubscribe': 'error', + 'svelte/no-immutable-reactive-statements': 'error', + 'svelte/no-inline-styles': 'error', + 'svelte/no-inspect': 'error', + 'svelte/no-reactive-functions': 'error', + 'svelte/no-reactive-literals': 'error', + 'svelte/no-svelte-internal': 'error', + 'svelte/no-unused-class-name': 'off', + 'svelte/no-unused-svelte-ignore': 'warn', + 'svelte/no-useless-mustaches': 'error', + 'svelte/prefer-destructured-store-props': 'error', + 'svelte/require-each-key': 'error', + 'svelte/require-event-dispatcher-types': 'error', + 'svelte/require-optimized-style-attribute': 'error', + 'svelte/require-stores-init': 'error', + 'svelte/valid-each-key': 'error', + // Styllistic + 'svelte/derived-has-same-inputs-outputs': 'warn', + 'svelte/first-attribute-linebreak': 'warn', + 'svelte/html-closing-bracket-new-line': 'warn', + 'svelte/html-closing-bracket-spacing': 'warn', + 'svelte/html-quotes': 'warn', + 'svelte/max-attributes-per-line': 'warn', + 'svelte/mustache-spacing': 'warn', + 'svelte/no-extra-reactive-curlies': 'warn', + 'svelte/no-spaces-around-equal-signs-in-attribute': 'warn', + 'svelte/prefer-class-directive': 'warn', + 'svelte/prefer-style-directive': 'warn', + 'svelte/shorthand-attribute': 'warn', + 'svelte/shorthand-directive': 'warn', + 'svelte/sort-attributes': 'warn', + 'svelte/spaced-html-comment': 'warn', + // Extension + 'svelte/no-trailing-spaces': 'warn', + // Svelte Kit + 'svelte/no-navigation-without-base': 'warn', + 'svelte/prefer-const': 'warn', + 'svelte/no-useless-children-snippet': 'warn', + 'svelte/consistent-selector-style': 'warn', + }, + } +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a934a70 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7740 @@ +{ + "name": "z3-frontend", + "version": "1.0.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "z3-frontend", + "version": "1.0.5", + "dependencies": { + "@humanspeak/svelte-markdown": "^0.7.12", + "@tailwindcss/typography": "^0.5.16", + "@zerodevx/svelte-toast": "^0.9.6", + "chart.js": "^4.4.7", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-adapter-dayjs-4": "^1.0.4", + "chartjs-plugin-zoom": "^2.2.0", + "color-string": "^1.9.1", + "daisyui": "^5.0.0", + "dayjs": "^1.11.13", + "dotenv": "^16.4.5", + "focus-trap": "^7.6.1", + "i18next": "^24.0.5", + "jwt-decode": "^4.0.0", + "lucide-svelte": "^0.456.0", + "pino": "^9.5.0", + "pino-pretty": "^13.0.0", + "svelte-modals": "^2.0.0", + "sveltekit-i18n": "^2.4.2", + "sveltekit-rate-limiter": "^0.6.1", + "tailwindcss": "^4.0.7", + "wicg-inert": "^3.1.3", + "zod": "^3.23.8", + "zod-i18n-map": "^2.27.0" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@playwright/test": "^1.49.1", + "@stylistic/eslint-plugin": "^2.10.0", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/postcss": "^4.0.12", + "@tailwindcss/vite": "^4.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.4", + "@types/color-string": "^1.5.5", + "eslint": "^9.18.0", + "eslint-plugin-svelte": "^2.46.1", + "globals": "^15.14.0", + "jsdom": "^25.0.1", + "postcss": "^8.5.3", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.0.0", + "vite-plugin-checker": "^0.9.0", + "vite-plugin-svelte-checker": "^0.1.2", + "vitest": "^3.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", + "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.2", + "@csstools/css-color-parser": "^3.0.8", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.7.tgz", + "integrity": "sha512-xvv7hJE32yhegJ8xNAnb62ggiAwTYHBpUCWhRxEj/ksvgDJuSXfoDkBcRYaYNFiJ+jH0IE3K16hd+xXzhBgNbg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanspeak/svelte-markdown": { + "version": "0.7.19", + "resolved": "https://registry.npmjs.org/@humanspeak/svelte-markdown/-/svelte-markdown-0.7.19.tgz", + "integrity": "sha512-4n6GsRBLDmNmMF1b6LUnQs47qbELRBYUA+GQc73735qrFmPdzheGKWg1ZKZBcygGWd7dgnnFw0tHoplWuoCyaw==", + "license": "MIT", + "dependencies": { + "github-slugger": "^2.0.0", + "htmlparser2": "^10.0.0", + "marked": "^15.0.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/humanspeak" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", + "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz", + "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz", + "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz", + "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz", + "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz", + "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz", + "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz", + "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz", + "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz", + "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz", + "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz", + "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz", + "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz", + "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz", + "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz", + "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz", + "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz", + "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz", + "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz", + "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/cli": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/cli/-/cli-1.29.2.tgz", + "integrity": "sha512-tHU3/Hat4lLdA1dE601vJHyO4aXf0k1Tr8E7A/ic57r3ni895Vpe7Ha1+HbX7D6i24p920r2QJsqWx6+CNUJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "chalk": "^5.4.1", + "minimist": "^1.2.8", + "shiki": "1.29.2" + }, + "bin": { + "cli": "bin.mjs", + "shiki": "bin.mjs", + "shiki-cli": "bin.mjs", + "skat": "bin.mjs" + } + }, + "node_modules/@shikijs/cli/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", + "integrity": "sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.1.tgz", + "integrity": "sha512-XXd6hQKi9le+8rYIKsxTfgABjB3b8S21qZmMUTvAC5kuVA1AXvYPVEmxrMhRqyOacXu3e6P3ag5HtJi6j9K7UQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@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 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltekit-i18n/base": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@sveltekit-i18n/base/-/base-1.3.7.tgz", + "integrity": "sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==", + "license": "MIT", + "peerDependencies": { + "svelte": ">=3.49.0" + } + }, + "node_modules/@sveltekit-i18n/parser-default": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@sveltekit-i18n/parser-default/-/parser-default-1.1.1.tgz", + "integrity": "sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", + "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", + "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-x64": "4.0.14", + "@tailwindcss/oxide-freebsd-x64": "4.0.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-x64-musl": "4.0.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", + "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", + "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", + "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", + "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", + "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", + "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", + "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", + "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", + "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", + "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", + "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.14.tgz", + "integrity": "sha512-+uIR6KtKhla1XeIanF27KtrfYy+PX+R679v5LxbkmEZlhQe3g8rk+wKj7Xgt++rWGRuFLGMXY80Ek8JNn+kN/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.0.14", + "@tailwindcss/oxide": "4.0.14", + "lightningcss": "1.29.2", + "postcss": "^8.4.41", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz", + "integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.0.14", + "@tailwindcss/oxide": "4.0.14", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/svelte": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.7.tgz", + "integrity": "sha512-aGhUaFmEXEVost4QOsbHUUbHLwi7ZZRRxAHFDO2Cmr0BZD3/3+XvaYEPq70Rdw0NRNjdqZHdARBEcrCOkPuAqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.0.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", + "vite": "*", + "vitest": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-p9+C1ssJsjnHV8nn96rkimm2h90LclLIwgBfiMCHW0oUr6jLmB+wzZUEGJPduB/D2RzI2Ahoe69xKNOawX6jgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", + "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", + "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", + "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", + "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.9", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", + "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", + "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.9", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@zerodevx/svelte-toast": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.9.6.tgz", + "integrity": "sha512-nHlTrCjverlPK9yukK6fqbG3e/R+f10ldrc4nJHOe2qNDScuPTuYVSFEk2dDDtzWAwTN5pmdEXgA3M2RbT8jiw==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3.57.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, + "node_modules/chartjs-adapter-dayjs-4": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz", + "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "chart.js": ">=4.0.1", + "dayjs": "^1.9.7" + } + }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", + "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.45", + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz", + "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.1", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/daisyui": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.6.tgz", + "integrity": "sha512-/e/9Gw/2y9oawBJlWkJMSEhRXdmfOLvcPl+6q/x2rPEdIVOtebs1t3ex2vwySl9vCRs1GGNBKCiL+P60Ps/wUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz", + "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", + "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/i18next": { + "version": "24.2.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", + "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-svelte": { + "version": "0.456.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.456.0.tgz", + "integrity": "sha512-SSdmxJOmL1h42ZypuGr54dYsXAT5o6WkQPKVDFnnrD0liOaXzfNNJuA7JbpkO9FFozBchyVpB1A1mKRmXOD8ug==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz", + "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.19.tgz", + "integrity": "sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz", + "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.36.0", + "@rollup/rollup-android-arm64": "4.36.0", + "@rollup/rollup-darwin-arm64": "4.36.0", + "@rollup/rollup-darwin-x64": "4.36.0", + "@rollup/rollup-freebsd-arm64": "4.36.0", + "@rollup/rollup-freebsd-x64": "4.36.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.36.0", + "@rollup/rollup-linux-arm-musleabihf": "4.36.0", + "@rollup/rollup-linux-arm64-gnu": "4.36.0", + "@rollup/rollup-linux-arm64-musl": "4.36.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.36.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0", + "@rollup/rollup-linux-riscv64-gnu": "4.36.0", + "@rollup/rollup-linux-s390x-gnu": "4.36.0", + "@rollup/rollup-linux-x64-gnu": "4.36.0", + "@rollup/rollup-linux-x64-musl": "4.36.0", + "@rollup/rollup-win32-arm64-msvc": "4.36.0", + "@rollup/rollup-win32-ia32-msvc": "4.36.0", + "@rollup/rollup-win32-x64-msvc": "4.36.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/source-map/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/source-map/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.23.2.tgz", + "integrity": "sha512-PHP1o0aYJNMatiZ+0nq1W/Z1W1/l5Z94B9nhMIo7gsuTBbxC454g4O5SQMjQpZBUZi5ANYUrXJOE4gPzcN/VQw==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.3", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz", + "integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-modals": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-2.0.1.tgz", + "integrity": "sha512-4nGWxveM0eILQnkjmlA7nComRSsCW5QssY9d1qNklZaNlnwnziW5xYqLN3ufjyA0/Cn9oMO4i9+8s8zmlw4IiQ==", + "license": "MIT", + "peerDependencies": { + "svelte": ">=5" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/sveltekit-i18n": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/sveltekit-i18n/-/sveltekit-i18n-2.4.2.tgz", + "integrity": "sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==", + "license": "MIT", + "workspaces": [ + "./examples/*/" + ], + "dependencies": { + "@sveltekit-i18n/base": "~1.3.0", + "@sveltekit-i18n/parser-default": "~1.1.0" + }, + "peerDependencies": { + "svelte": ">=3.49.0" + } + }, + "node_modules/sveltekit-rate-limiter": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sveltekit-rate-limiter/-/sveltekit-rate-limiter-0.6.1.tgz", + "integrity": "sha512-5b0ZAuJsGeFt3k8qe+SvPZXeg8f0XVcnUnL/5cOZVh1tLoi8NE9bDOtAkGN3L+66hjSkbBayhd7FVyjN4DV7pw==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1" + }, + "peerDependencies": { + "@sveltejs/kit": "1.x || 2.x" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", + "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.84", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.84.tgz", + "integrity": "sha512-aRGIbCIF3teodtUFAYSdQONVmDRy21REM3o6JnqWn5ZkQBJJ4gHxhw6OfwQ+WkSAi3ASamrS4N4nyazWx6uTYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.84" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.84", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.84.tgz", + "integrity": "sha512-NaQa1W76W2aCGjXybvnMYzGSM4x8fvG2AN/pla7qxcg0ZHbooOPhA8kctmOZUDfZyhDL27OGNbwAeig8P4p1vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.4.0.tgz", + "integrity": "sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.25.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/tsup/node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", + "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", + "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.9.1.tgz", + "integrity": "sha512-neH3CSNWdkZ+zi+WPt/0y5+IO2I0UAI0NX6MaXqU/KxN1Lz6np/7IooRB6VVAMBa4nigqm1GRF6qNa4+EL5jDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "chokidar": "^4.0.3", + "npm-run-path": "^6.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "strip-ansi": "^7.1.0", + "tiny-invariant": "^1.3.3", + "tinyglobby": "^0.2.12", + "vscode-uri": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=7", + "meow": "^13.2.0", + "optionator": "^0.9.4", + "stylelint": ">=16", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": "~2.2.2" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-svelte-checker": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-svelte-checker/-/vite-plugin-svelte-checker-0.1.2.tgz", + "integrity": "sha512-HtQlZOSbmPWEKroSCM6cxwoODfPTgHyhLEb3j4WTiif2da9HR/nVCCcgiG+OWWy8Yf2SCRiq3RGyJ8nO13WniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/cli": "^1.24.1", + "chalk": "^5.3.0", + "strip-ansi": "^7.1.0", + "tsup": "^8.3.5" + }, + "peerDependencies": { + "vite": ">=5.0.0" + } + }, + "node_modules/vite-plugin-svelte-checker/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", + "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.9", + "@vitest/mocker": "3.0.9", + "@vitest/pretty-format": "^3.0.9", + "@vitest/runner": "3.0.9", + "@vitest/snapshot": "3.0.9", + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.9", + "@vitest/ui": "3.0.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wicg-inert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.3.tgz", + "integrity": "sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ==", + "license": "W3C-20150513" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-i18n-map": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/zod-i18n-map/-/zod-i18n-map-2.27.0.tgz", + "integrity": "sha512-ORu9XpiVh3WDiEUs5Cr9siGgnpeODoBsTIgSD8sQCH9B//f9KowlzqHUEdPYb3vFonaSH8yPvPCOFM4niwp3Sg==", + "license": "MIT", + "peerDependencies": { + "i18next": ">=21.3.0", + "zod": ">=3.17.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ce05b3 --- /dev/null +++ b/package.json @@ -0,0 +1,81 @@ +{ + "name": "z3-frontend", + "version": "1.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run && npm run test:e2e", + "test:e2e": "playwright test" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@playwright/test": "^1.49.1", + "@stylistic/eslint-plugin": "^2.10.0", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/postcss": "^4.0.12", + "@tailwindcss/vite": "^4.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.4", + "@types/color-string": "^1.5.5", + "eslint": "^9.18.0", + "eslint-plugin-svelte": "^2.46.1", + "globals": "^15.14.0", + "jsdom": "^25.0.1", + "postcss": "^8.5.3", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.0.0", + "vite-plugin-checker": "^0.9.0", + "vite-plugin-svelte-checker": "^0.1.2", + "vitest": "^3.0.0" + }, + "dependencies": { + "@humanspeak/svelte-markdown": "^0.7.12", + "@tailwindcss/typography": "^0.5.16", + "@zerodevx/svelte-toast": "^0.9.6", + "chart.js": "^4.4.7", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-adapter-dayjs-4": "^1.0.4", + "chartjs-plugin-zoom": "^2.2.0", + "color-string": "^1.9.1", + "daisyui": "^5.0.0", + "dayjs": "^1.11.13", + "dotenv": "^16.4.5", + "focus-trap": "^7.6.1", + "i18next": "^24.0.5", + "jwt-decode": "^4.0.0", + "lucide-svelte": "^0.456.0", + "pino": "^9.5.0", + "pino-pretty": "^13.0.0", + "svelte-modals": "^2.0.0", + "sveltekit-i18n": "^2.4.2", + "sveltekit-rate-limiter": "^0.6.1", + "tailwindcss": "^4.0.7", + "wicg-inert": "^3.1.3", + "zod": "^3.23.8", + "zod-i18n-map": "^2.27.0" + }, + "overrides": { + "@sveltejs/kit": { + "cookie": "1.0.2" + } + }, + "browserslist": [ + "defaults and fully supports es6-module", + "maintained node versions" + ] +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2012830 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,12 @@ +import type { PlaywrightTestConfig } from '@playwright/test' + +const config: PlaywrightTestConfig = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173, + }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/, +} + +export default config diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..533dea1 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,32 @@ +import type { FlashMessage } from '$customType/FlashMessage.type' +import type Brand from '$lib/models/Brand.type' +import type JwtToken from '$lib/models/JwtToken.type' +import type User from '$lib/models/User.type' + +declare global { + namespace App { + interface Error { + /** Redirect instructions for the global +error component */ + redirect?: { + code: number + url: string + } + } + + interface Locals { + /** User selected active brand */ + activeBrand: Brand | undefined + /** User available brands */ + brands: Brand[] + /** User current session */ + session?: { + token: JwtToken + user: User + } + /** Messages that should be displayed once to the user */ + flashMessages?: FlashMessage[] + } + } +} + +export { } diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..56a7f2d --- /dev/null +++ b/src/app.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + %sveltekit.head% + + + + + + +
+ %sveltekit.body% +
+ + + diff --git a/src/css/app.css b/src/css/app.css new file mode 100644 index 0000000..3937da8 --- /dev/null +++ b/src/css/app.css @@ -0,0 +1,7 @@ +@import './tailwind.css'; +@import './fonts.css'; + +@import './tailwind-custom.css'; +@import './daisyui-custom.css'; + +@import './grid.css'; diff --git a/src/css/daisyui-custom.css b/src/css/daisyui-custom.css new file mode 100644 index 0000000..c046f79 --- /dev/null +++ b/src/css/daisyui-custom.css @@ -0,0 +1,51 @@ +/* OVERLOADS */ + +a { + @apply link +} + +.btn { + @apply no-underline; +} + +.menu a { + @apply no-underline; +} + +.input { + @apply bg-base-200/45; +} + +.select { + @apply bg-base-200/45; +} + +.toggle { + --tglbg: var(--fallback-b1, oklch(var(--b1)/0.90)); +} + + +/* CUSTOM COMPONENTS */ + +/* + * Displays a Model + .display-entity + |_div + |__dt + |__dl + */ +.display-entity { + @apply divide-y divide-accent-content rounded-lg overflow-hidden; + + > div { + @apply px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-4; + + > dt { + @apply mb-0 font-extrabold; + } + + > dd { + @apply mb-0 mt-1 text-sm/6 sm:col-span-2 sm:mt-0 sm:text-lg; + } + } +} diff --git a/src/css/fonts.css b/src/css/fonts.css new file mode 100644 index 0000000..7547092 --- /dev/null +++ b/src/css/fonts.css @@ -0,0 +1,125 @@ +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-BoldItalic.woff2') format('woff2'), + url('/fonts/Ubuntu-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Mono'; + src: url('/fonts/UbuntuMono-BoldItalic.woff2') format('woff2'), + url('/fonts/UbuntuMono-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Light.woff2') format('woff2'), + url('/fonts/Ubuntu-Light.woff') format('woff'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Mono'; + src: url('/fonts/UbuntuMono-Italic.woff2') format('woff2'), + url('/fonts/UbuntuMono-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Mono'; + src: url('/fonts/UbuntuMono-Bold.woff2') format('woff2'), + url('/fonts/UbuntuMono-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Medium.woff2') format('woff2'), + url('/fonts/Ubuntu-Medium.woff') format('woff'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-LightItalic.woff2') format('woff2'), + url('/fonts/Ubuntu-LightItalic.woff') format('woff'); + font-weight: 300; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Bold.woff2') format('woff2'), + url('/fonts/Ubuntu-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Italic.woff2') format('woff2'), + url('/fonts/Ubuntu-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Sans'; + src: url('/fonts/UbuntuSans-Regular.woff2') format('woff2'), + url('/fonts/UbuntuSans-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Regular.woff2') format('woff2'), + url('/fonts/Ubuntu-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-MediumItalic.woff2') format('woff2'), + url('/fonts/Ubuntu-MediumItalic.woff') format('woff'); + font-weight: 500; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Sans'; + src: url('/fonts/UbuntuSans-Italic.woff2') format('woff2'), + url('/fonts/UbuntuSans-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Ubuntu Mono'; + src: url('/fonts/UbuntuMono-Regular.woff2') format('woff2'), + url('/fonts/UbuntuMono-Regular.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} diff --git a/src/css/grid.css b/src/css/grid.css new file mode 100644 index 0000000..8d41958 --- /dev/null +++ b/src/css/grid.css @@ -0,0 +1,96 @@ +@reference "tailwindcss"; + +@utility container { + @apply max-w-full w-full mx-auto px-4; + + @variant sm { + @apply max-w-7xl; + } +} + +@utility container-fluid { + @apply w-full mx-auto px-0; +} + +@utility row { + @apply flex flex-wrap; + > * { + @apply shrink w-full max-w-full ps-2 pe-2 mt-2; + } +} + +@utility col { + flex: 1 0 0%; +} + +@utility col-1 { + flex: 0 0 auto; + + @apply w-1/12; +} + +@utility col-2 { + flex: 0 0 auto; + + @apply w-2/12; +} + +@utility col-3 { + flex: 0 0 auto; + + @apply w-3/12; +} + +@utility col-4 { + flex: 0 0 auto; + + @apply w-4/12; +} + +@utility col-5 { + flex: 0 0 auto; + + @apply w-5/12; +} + +@utility col-6 { + flex: 0 0 auto; + + @apply w-6/12; +} + +@utility col-7 { + flex: 0 0 auto; + + @apply w-7/12; +} + +@utility col-8 { + flex: 0 0 auto; + + @apply w-8/12; +} + +@utility col-9 { + flex: 0 0 auto; + + @apply w-9/12; +} + +@utility col-10 { + flex: 0 0 auto; + + @apply w-10/12; +} + +@utility col-11 { + flex: 0 0 auto; + + @apply w-11/12; +} + +@utility col-12 { + flex: 0 0 auto; + + @apply w-12/12; +} diff --git a/src/css/tailwind-custom.css b/src/css/tailwind-custom.css new file mode 100644 index 0000000..26439cd --- /dev/null +++ b/src/css/tailwind-custom.css @@ -0,0 +1,34 @@ +@layer base { + :root { + + } +} + +@layer base { + #svelte-container > main { + min-height: 100vh; + } + + p, span { + font-family: Ubuntu; + } + + h1,.h1 { + @apply text-2xl mb-2; + } + h2,.h2 { + @apply text-xl mb-2; + } + h3,.h3 { + @apply text-lg mb-2; + } + h4,.h4 { + @apply text-base mb-2; + } + h5,.h5 { + @apply text-sm mb-1; + } + h6,.h6 { + @apply text-xs mb-1; + } +} diff --git a/src/css/tailwind.css b/src/css/tailwind.css new file mode 100644 index 0000000..d1b0496 --- /dev/null +++ b/src/css/tailwind.css @@ -0,0 +1,107 @@ +@import 'tailwindcss'; +@plugin "@tailwindcss/typography"; +@plugin "daisyui" { + themes: corporate --default, dracula --prefersdark; +} + +@theme { + --font-serif: + 'Ubuntu', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-sans: + 'Ubuntu Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-mono: + 'Ubuntu Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + + --font: var(--font-sans); + --default-font-family: var(--font); + --heading-font-family: var(--font-serif); + --monospace-font-family: var(--font-mono); + + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --breakpoint-2xl: 1400px; +} + +@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); +@custom-variant light (&:where([data-theme=light], [data-theme=light] *)); + +@plugin "daisyui/theme" { + /* corporate */ + name: "light"; + default: true; + prefersdark: false; + color-scheme: "light"; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(93% 0 0); + --color-base-300: oklch(86% 0 0); + --color-base-content: oklch(22.389% 0.031 278.072); + --color-primary: oklch(58% 0.158 241.966); + --color-primary-content: oklch(100% 0 0); + --color-secondary: oklch(55% 0.046 257.417); + --color-secondary-content: oklch(100% 0 0); + --color-accent: oklch(60% 0.118 184.704); + --color-accent-content: oklch(100% 0 0); + --color-neutral: oklch(0% 0 0); + --color-neutral-content: oklch(100% 0 0); + --color-info: oklch(60% 0.126 221.723); + --color-info-content: oklch(100% 0 0); + --color-success: oklch(62% 0.194 149.214); + --color-success-content: oklch(100% 0 0); + --color-warning: oklch(85% 0.199 91.936); + --color-warning-content: oklch(0% 0 0); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(0% 0 0); + --radius-selector: 0.5rem; + --radius-field: 0.5rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + + + + +@plugin "daisyui/theme" { + /* dracula */ + name: "dark"; + default: false; + prefersdark: false; + color-scheme: "dark"; + --color-base-100: oklch(28.822% 0.022 277.508); + --color-base-200: oklch(26.805% 0.02 277.508); + --color-base-300: oklch(24.787% 0.019 277.508); + --color-base-content: oklch(97.747% 0.007 106.545); + --color-primary: oklch(75.461% 0.183 346.812); + --color-primary-content: oklch(15.092% 0.036 346.812); + --color-secondary: oklch(74.202% 0.148 301.883); + --color-secondary-content: oklch(14.84% 0.029 301.883); + --color-accent: oklch(83.392% 0.124 66.558); + --color-accent-content: oklch(16.678% 0.024 66.558); + --color-neutral: oklch(39.445% 0.032 275.524); + --color-neutral-content: oklch(87.889% 0.006 275.524); + --color-info: oklch(88.263% 0.093 212.846); + --color-info-content: oklch(17.652% 0.018 212.846); + --color-success: oklch(87.099% 0.219 148.024); + --color-success-content: oklch(17.419% 0.043 148.024); + --color-warning: oklch(95.533% 0.134 112.757); + --color-warning-content: oklch(19.106% 0.026 112.757); + --color-error: oklch(68.22% 0.206 24.43); + --color-error-content: oklch(13.644% 0.041 24.43); + --radius-selector: 0.5rem; + --radius-field: 0.5rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + + diff --git a/src/error.html b/src/error.html new file mode 100644 index 0000000..3f228df --- /dev/null +++ b/src/error.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + + %sveltekit.error.message% + + + + + +
+

Une erreur est survenue sur Z3

+
+ %sveltekit.status% +
+

%sveltekit.error.message%

+
+
+
+ + + + \ No newline at end of file diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..7a2cefd --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,22 @@ +import { isHttpError, type HandleClientError } from '@sveltejs/kit' +import { toast } from '@zerodevx/svelte-toast' + +export const handleError: HandleClientError = async ({ event, error }) => { + console.error(error) + console.error(event.route) + + const message = 'Une erreur est survenue' + if (isHttpError(error) && error.status != 404) { + setTimeout(() => { + toast.push(`${message}, la page va être rechargée`, { + classes: ['bg-error'], + }) + window.location.reload() + }, 5000) + } + + return { + message, + error, + } +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..5d68ced --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,180 @@ +import { redirect, type Cookies, type Handle, type HandleServerError } from '@sveltejs/kit' +import { toast } from '@zerodevx/svelte-toast' +import { get } from 'svelte/store' + +import type { PinoLogger } from '$customType' +import type { FormFieldsOlds } from '$customType/forms' +import { Role } from '$lib/enums' +import type { Brand } from '$lib/models' +import { AuthenticationError, AuthorizationError, JwtAuthenticationError, NotFoundError, ValidationError } from '$lib/server/errors' +import { brandService, sessionService } from '$lib/server/services' +import type { SessionData } from '$lib/server/services/SessionService' +import { pinoLoggerStore } from '$lib/server/stores' + +export const handle: Handle = async ({ event, resolve }) => { + const { locals, cookies, request } = event + const sessionData = sessionService.getSessionData(cookies) + + if (sessionData === undefined) { + return await resolve(event) + } + + const localUser = sessionData.session.getUser() + + locals.session = { + token: sessionData.session.getToken(), + user: localUser, + } + + await selectDefaultActiveBrand(locals, cookies, sessionData) + await flashOldFormInputs(request, cookies) + + return await resolve(event) +} + +export const handleError: HandleServerError = async ({ event, error, message }) => { + const pinoLogger: PinoLogger = get(pinoLoggerStore) + const { url, cookies } = event + + let redirect: { + code: number + url: string + } | undefined = undefined + + if (error instanceof AuthenticationError) { + // Remove all toasts + toast.pop(0) + // Remove all Flash messages + sessionService.getFlash(cookies).consumeMessages() + sessionService.getFlash(cookies).flashError('Vous avez été deconnecté') + sessionService.removeSession(sessionService.getSessionId(cookies)) + + pinoLogger.debug(error, 'Authentication error') + + redirect = { + code: 301, + url: '/', + } + } + else if (error instanceof NotFoundError) { + pinoLogger.debug(`Not found error ${error.message}`) + } + else if (error instanceof JwtAuthenticationError) { + sessionService.getFlash(cookies).flashInfo( + `Votre session a expiré, veuillez vous reconnecter`, + ) + + pinoLogger.debug(`Session expired`) + + redirect = { + code: 302, + url: '/', + } + } + else if (error instanceof AuthorizationError) { + message = 'Vous n\'êtes pas authorisé a effectuer cette action' + sessionService.getFlash(cookies).flashError(message) + pinoLogger.debug('Authorization error') + + redirect = { + code: 403, + url: url.pathname, + } + } + else if (error instanceof ValidationError) { + sessionService.getFlash(cookies).flashWarning('Des données n´ont pas pu être validées') + sessionService.getFlashErrors(cookies).setErrors(error.getFieldErrors()) + + pinoLogger.debug('Validation error') + + redirect = { + code: 422, + url: url.pathname, + } + } + else { + pinoLogger.error(error, 'Unhandled error') + } + + return { + message, + redirect, + } +} + +// -- PRIVATE FUNCTIONS + +async function selectDefaultActiveBrand(locals: App.Locals, cookies: Cookies, sessionData: SessionData): Promise { + try { + const session = sessionData.session, localUser = session.getUser() + const brands = await brandService.getAllBrands(cookies) + + // Filter user accessible brands + locals.brands = brands.filter((brand) => { + if ( + localUser.role === Role.ADMINISTRATOR + || localUser.brands?.indexOf(brand.uuid) !== -1) { + return brand + } + return false + }) + + let activeBrand: Brand | undefined = sessionData?.session.getActiveBrand() + + if (activeBrand === undefined) { + // Automatically select first available brand + const firstBrandVailable = localUser.role === Role.ADMINISTRATOR + ? brands.at(0) + : brands.find(brand => localUser.brands?.indexOf(brand.uuid) !== -1) + if (firstBrandVailable) { + activeBrand = firstBrandVailable + sessionData?.session.setActiveBrand(firstBrandVailable) + } + } + + locals.activeBrand = activeBrand + } + catch (e) { + if (e instanceof JwtAuthenticationError) { + get(pinoLoggerStore).debug('Authentication error') + sessionService.getFlash(cookies).flashInfo('Vous avez été deconecté') + sessionService.removeSession(sessionData.sessionId) + redirect(302, '/') + } + get(pinoLoggerStore).error('Unhandled error', e) + throw e + } +} + +/** + * Flash current request inputs to reuse them in the next page show + */ +async function flashOldFormInputs(request: Request, cookies: Cookies): Promise { + const oldFormData: FormFieldsOlds = {} + if (request.method === 'GET' || request.method === 'HEAD') { + return + } + + // * Map old inputs into oldFormData + (await request.clone().formData()).entries() + .forEach((entry) => { + const isArray = entry[0].endsWith('[]') + const name = entry[0].replace(/\[\]$/, '') + const value = entry[1] + + if (value instanceof File) { + return + } + + if (isArray) { + const previousValue = oldFormData[name] ?? [] + oldFormData[name] = [...previousValue, value] + return + } + + oldFormData[name] = value + }) + + // * Flash old form values to repopulate inputs + sessionService.getFlashOlds(cookies).setOlds(oldFormData) +} diff --git a/src/lib/client/index.ts b/src/lib/client/index.ts new file mode 100644 index 0000000..ebd63ce --- /dev/null +++ b/src/lib/client/index.ts @@ -0,0 +1,9 @@ +import { initPreferedTheme, setTheme, wantedTheme, type Theme } from './preferedTheme.svelte' +import { ToastType, Toaster } from './toaster' + +export { + ToastType, Toaster, + setTheme, wantedTheme, initPreferedTheme as watchPreferedTheme, + type Theme +} + diff --git a/src/lib/client/preferedTheme.svelte.ts b/src/lib/client/preferedTheme.svelte.ts new file mode 100644 index 0000000..f15089e --- /dev/null +++ b/src/lib/client/preferedTheme.svelte.ts @@ -0,0 +1,49 @@ +import { on } from "svelte/events" +// import { MediaQuery } from "svelte/reactivity" + +export type Theme = 'auto' | 'light' | 'dark' + +const preferedDefaultAutoTheme: Theme = 'auto' +// const preferedAutoTheme = new MediaQuery('prefers-color-scheme: dark'); + +let selectedTheme = $state() + +const currentTheme = $derived(selectedTheme === undefined ? preferedDefaultAutoTheme : selectedTheme) + + +function onPreferedThemeChange(event: MediaQueryListEvent): void { + const newColorScheme = event.matches ? 'dark' : 'light' + + setTheme(newColorScheme) +} + +export function initPreferedTheme(): void { + const previouslySelectedTheme: Theme | undefined = localStorage['z3-theme'] + + if (previouslySelectedTheme !== undefined) { + setTheme(previouslySelectedTheme) + } else { + setTheme('dark') + } + + setTheme(wantedTheme()) + + const hasMatchMedia = window.matchMedia + + if (hasMatchMedia !== undefined) { + const mediaMatchDarkTheme = matchMedia('(prefers-color-scheme: dark)') + + on(mediaMatchDarkTheme, 'change', onPreferedThemeChange) + } +} + +export function wantedTheme(): Theme { + return currentTheme +} + +export function setTheme(theme: Theme): void { + selectedTheme = theme + + localStorage['z3-theme'] = currentTheme + document.documentElement.dataset['theme'] = currentTheme +} diff --git a/src/lib/client/services/index.ts b/src/lib/client/services/index.ts new file mode 100644 index 0000000..d8e836a --- /dev/null +++ b/src/lib/client/services/index.ts @@ -0,0 +1,5 @@ +import { validateLink } from './validateLink.svelte' + +export { + validateLink, +} diff --git a/src/lib/client/services/validateLink.svelte.ts b/src/lib/client/services/validateLink.svelte.ts new file mode 100644 index 0000000..68d9c33 --- /dev/null +++ b/src/lib/client/services/validateLink.svelte.ts @@ -0,0 +1,24 @@ +import { invalidateAll } from '$app/navigation' +import type { LinkValidity } from '$lib/models' +import { Toaster } from '..' + +export const validateLink = async (linkSource: string): Promise => { + const response = await fetch(`/api/links/${linkSource}/verify`, { + method: 'put', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + const linkValidity = await response.json() as LinkValidity + + await invalidateAll() + + if (linkValidity.isValid === true) { + Toaster.toastInfo('Le lien est valide') + } + if (linkValidity.isValid === false) { + Toaster.toastWarning('Le lien n\'est pas valide') + } + + return linkValidity +} diff --git a/src/lib/client/stores/brandStore.svelte.ts b/src/lib/client/stores/brandStore.svelte.ts new file mode 100644 index 0000000..70732af --- /dev/null +++ b/src/lib/client/stores/brandStore.svelte.ts @@ -0,0 +1,30 @@ +import { writable } from 'svelte/store' + +import type { Brand, ListOf } from '$lib/models' +import { Toaster } from '..' + +export const initialData = { + list: [], + page: 1, + perPage: 1, + totalItems: 0, + totalPages: 1, +} + +export const loadPaginatedBrands = async (page: number, perPage = 15, fetch: typeof window.fetch | null = null): Promise> => { + const res = await (fetch ?? window.fetch)(`/api/brands?page=${page}&per-page=${perPage}`) + if (!res.ok) { + const message = 'Failed to fetch brand data' + console.error(message) + Toaster.toastError('Echec du chargement des enseignes') + + return initialData + } + + const data = await res.json() + + paginatedBrands.set(data) + return data +} + +export const paginatedBrands = writable>(initialData) diff --git a/src/lib/client/stores/catalogStore.svelte.ts b/src/lib/client/stores/catalogStore.svelte.ts new file mode 100644 index 0000000..b4839d0 --- /dev/null +++ b/src/lib/client/stores/catalogStore.svelte.ts @@ -0,0 +1,30 @@ +import { writable } from 'svelte/store' + +import type { Catalog, ListOf } from '$lib/models' +import { Toaster } from '..' + +export const initialData = { + list: [], + page: 1, + perPage: 1, + totalItems: 0, + totalPages: 1, +} + +export const loadPaginatedCatalogs = async (page: number, perPage = 15, fetch: typeof window.fetch | null = null): Promise> => { + const res = await (fetch ?? window.fetch)(`/api/catalogs?page=${page}&per-page=${perPage}`) + if (!res.ok) { + const message = 'Failed to fetch catalog data' + console.error(message) + Toaster.toastError('Echec du chargement des catalogues') + + return initialData + } + + const data = await res.json() + + paginatedCatalogs.set(data) + return data +} + +export const paginatedCatalogs = writable>(initialData) diff --git a/src/lib/client/stores/errorsStore.svelte.ts b/src/lib/client/stores/errorsStore.svelte.ts new file mode 100644 index 0000000..56302cf --- /dev/null +++ b/src/lib/client/stores/errorsStore.svelte.ts @@ -0,0 +1,16 @@ +import { writable, type Writable } from 'svelte/store' + +import type { FormFieldsError } from '$customType/forms' + +function createErrors(): Pick, 'subscribe' | 'set'> & { + set: (errors: FormFieldsError) => void +} { + const store = writable({}) + + return { + subscribe: store.subscribe, + set: store.set, + } +} + +export const inputErrors = createErrors() diff --git a/src/lib/client/stores/index.ts b/src/lib/client/stores/index.ts new file mode 100644 index 0000000..1e5c23b --- /dev/null +++ b/src/lib/client/stores/index.ts @@ -0,0 +1,26 @@ +import { loadPaginatedBrands, paginatedBrands } from './brandStore.svelte' +import { loadPaginatedCatalogs, paginatedCatalogs } from './catalogStore.svelte' +import { inputErrors } from './errorsStore.svelte' +import flashMessages from './messagesStore.svelte' +import { breadcrumb } from './pageBreadcrumbStore.svelte' +import { title } from './pageTitleStore.svelte' +import { loadPaginatedTags, paginatedTags } from './tagStore.svelte' + +export { + loadPaginatedBrands, + paginatedBrands, +} + +export { + loadPaginatedCatalogs, + paginatedCatalogs, +} + +export { + loadPaginatedTags, + paginatedTags, +} + +export { breadcrumb, title } + +export { flashMessages, inputErrors } diff --git a/src/lib/client/stores/messagesStore.svelte.ts b/src/lib/client/stores/messagesStore.svelte.ts new file mode 100644 index 0000000..6625303 --- /dev/null +++ b/src/lib/client/stores/messagesStore.svelte.ts @@ -0,0 +1,21 @@ +import { writable, type Writable } from 'svelte/store' + +import type { FlashMessage } from '$customType' + +function createFlashMessage(): Pick, 'subscribe' | 'set'> & { + setOne: (message: FlashMessage) => void +} { + const store = writable([]) + + return { + subscribe: store.subscribe, + setOne(message): void { + store.set([message]) + }, + set: store.set, + } +} + +const flashMessages = createFlashMessage() + +export default flashMessages diff --git a/src/lib/client/stores/pageBreadcrumbStore.svelte.ts b/src/lib/client/stores/pageBreadcrumbStore.svelte.ts new file mode 100644 index 0000000..6a3d538 --- /dev/null +++ b/src/lib/client/stores/pageBreadcrumbStore.svelte.ts @@ -0,0 +1,19 @@ +import { writable, type Writable } from 'svelte/store' + +interface BreadCrumb { + previous?: false | string | { text: string, url: string } + current?: string +} + +function createBreadcrumb(): Pick, 'subscribe' | 'set'> { + const { subscribe, set } = writable({}) + + return { + subscribe, + set: (value): void => { + set(value) + }, + } +} + +export const breadcrumb = createBreadcrumb() diff --git a/src/lib/client/stores/pageTitleStore.svelte.ts b/src/lib/client/stores/pageTitleStore.svelte.ts new file mode 100644 index 0000000..818bfc1 --- /dev/null +++ b/src/lib/client/stores/pageTitleStore.svelte.ts @@ -0,0 +1,16 @@ +import { writable, type Writable } from 'svelte/store' + +import { PUBLIC_APP_NAME } from '$env/static/public' + +function createTitle(): Pick, 'subscribe' | 'set'> { + const { subscribe, set } = writable('') + + return { + subscribe, + set: (value): void => { + set(`${value} • ${PUBLIC_APP_NAME}`) + }, + } +} + +export const title = createTitle() diff --git a/src/lib/client/stores/tagStore.svelte.ts b/src/lib/client/stores/tagStore.svelte.ts new file mode 100644 index 0000000..81bbd0f --- /dev/null +++ b/src/lib/client/stores/tagStore.svelte.ts @@ -0,0 +1,30 @@ +import { writable } from 'svelte/store' + +import type { ListOf, Tag } from '$lib/models' +import { Toaster } from '..' + +export const initialData = { + list: [], + page: 1, + perPage: 1, + totalItems: 0, + totalPages: 1, +} + +export const loadPaginatedTags = async (page: number, perPage = 15, fetch: typeof window.fetch | null = null): Promise> => { + const res = await (fetch ?? window.fetch)(`/api/tags?page=${page}&per-page=${perPage}`) + if (!res.ok) { + const message = 'Failed to fetch tag data' + console.error(message) + Toaster.toastError('Echec du chargement des étiquettes') + + return initialData + } + + const data = await res.json() + + paginatedTags.set(data) + return data +} + +export const paginatedTags = writable>(initialData) diff --git a/src/lib/client/toaster.ts b/src/lib/client/toaster.ts new file mode 100644 index 0000000..05c2967 --- /dev/null +++ b/src/lib/client/toaster.ts @@ -0,0 +1,35 @@ +import { toast } from '@zerodevx/svelte-toast' + +export enum ToastType { + DEFAULT = 'b3', + INFO = 'in', + WARNING = 'wa', + ERROR = 'er', + SUCCESS = 'suc', +} + +export abstract class Toaster { + public static toast(message: string, type: ToastType): void { + toast.push(message, { + theme: { + '--toastBarBackground': `oklch(var(--${type}))`, + }, + }) + } + + public static toastWarning(message: string): void { + this.toast(message, ToastType.WARNING) + } + + public static toastInfo(message: string): void { + this.toast(message, ToastType.INFO) + } + + public static toastSuccess(message: string): void { + this.toast(message, ToastType.SUCCESS) + } + + public static toastError(message: string): void { + this.toast(message, ToastType.ERROR) + } +} diff --git a/src/lib/client/tools/applyZ3Action.tools.ts b/src/lib/client/tools/applyZ3Action.tools.ts new file mode 100644 index 0000000..0c03bfc --- /dev/null +++ b/src/lib/client/tools/applyZ3Action.tools.ts @@ -0,0 +1,28 @@ +import type { ActionResult } from '@sveltejs/kit' + +import { applyAction } from '$app/forms' +import { flashMessages, inputErrors } from '../stores' + +export default function applyZ3Action(result: ActionResult): void { + applyAction(result) + if (result.type === 'failure') { + if (result.data?.['message']) { + flashMessages.setOne({ + message: String(result.data?.['message']), + type: result.status === 422 ? 'warning' : 'error', + }) + } + if (result.data?.['errors']) { + inputErrors.set(result.data?.['errors']) + } + } + if (result.type === 'success') { + if (result.data?.['message']) { + flashMessages.setOne({ + message: String(result.data?.['message']), + type: result.data?.['type'] === 'warning' ? 'warning' : 'success', + }) + } + inputErrors.set({}) + } +} diff --git a/src/lib/client/tools/authorization/can.tools.ts b/src/lib/client/tools/authorization/can.tools.ts new file mode 100644 index 0000000..56ca3db --- /dev/null +++ b/src/lib/client/tools/authorization/can.tools.ts @@ -0,0 +1,45 @@ +import type { Policy } from '$customType' +import type { User } from '$lib/models' +import { BrandPolicy, UserPolicy } from './policies' + +type PolicyList = Record + +class PolicyManager { + readonly #policies: PolicyList + + constructor(policiesList: Record) { + this.#policies = {} + + Object.entries(policiesList).forEach((policiesListEntry) => { + const policyFamilyName = policiesListEntry[0], + policyFamilySuite = policiesListEntry[1] + Object.entries(policyFamilySuite).forEach((policySuiteEntry) => { + const policyName = `${policyFamilyName}:${policySuiteEntry[0]}`, + policy = policySuiteEntry[1] + this.#policies[policyName] = policy + }) + }) + } + + public can(policy: string, user: User): boolean { + const resolvedPolicy = this.resolvePolicy(policy) + return resolvedPolicy.call(resolvedPolicy, user) + } + + private resolvePolicy(policyName: string): Policy { + if (!this.#policies[policyName]) { + throw new Error(`Missing policy ${policyName}`) + } + + return this.#policies[policyName] + } +} + +const policies = new PolicyManager({ + user: new UserPolicy().getPolicies(), + brand: new BrandPolicy().getPolicies(), +}) + +export default function can(policy: string, user: User): boolean { + return policies.can(policy, user) +} diff --git a/src/lib/client/tools/authorization/policies/BrandPolicy.ts b/src/lib/client/tools/authorization/policies/BrandPolicy.ts new file mode 100644 index 0000000..c4dbe28 --- /dev/null +++ b/src/lib/client/tools/authorization/policies/BrandPolicy.ts @@ -0,0 +1,30 @@ +import type { Policies, Policy } from '$customType' +import { Role } from '$lib/enums' +import type { User } from '$lib/models' + +export default class BrandPolicy implements Policies { + getPolicies(): Record { + return { + viewAny: this.viewAny, + create: this.create, + view: this.view, + update: this.update, + } + } + + public viewAny(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public create(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public view(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public update(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } +} diff --git a/src/lib/client/tools/authorization/policies/UserPolicy.ts b/src/lib/client/tools/authorization/policies/UserPolicy.ts new file mode 100644 index 0000000..c42ec2b --- /dev/null +++ b/src/lib/client/tools/authorization/policies/UserPolicy.ts @@ -0,0 +1,45 @@ +import type { Policies, Policy } from '$customType' +import { Role } from '$lib/enums' +import type { User } from '$lib/models' + +export default class UserPolicy implements Policies { + getPolicies(): Record { + return { + viewAny: this.viewAny, + create: this.create, + view: this.view, + update: this.update, + delete: this.delete, + reset: this.reset, + setLinksAlerts: this.setLinksAlerts, + } + } + + public viewAny(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public create(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public view(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public update(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public delete(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public reset(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } + + public setLinksAlerts(auth: User): boolean { + return auth.role === Role.ADMINISTRATOR + } +} diff --git a/src/lib/client/tools/authorization/policies/index.ts b/src/lib/client/tools/authorization/policies/index.ts new file mode 100644 index 0000000..4c5d302 --- /dev/null +++ b/src/lib/client/tools/authorization/policies/index.ts @@ -0,0 +1,7 @@ +import BrandPolicy from './BrandPolicy' +import UserPolicy from './UserPolicy' + +export { + BrandPolicy, + UserPolicy, +} diff --git a/src/lib/client/tools/index.ts b/src/lib/client/tools/index.ts new file mode 100644 index 0000000..ef3816d --- /dev/null +++ b/src/lib/client/tools/index.ts @@ -0,0 +1,4 @@ +import applyZ3Action from './applyZ3Action.tools' +import can from './authorization/can.tools' + +export { applyZ3Action, can } diff --git a/src/lib/components/HelpComponent.svelte b/src/lib/components/HelpComponent.svelte new file mode 100644 index 0000000..dde91d0 --- /dev/null +++ b/src/lib/components/HelpComponent.svelte @@ -0,0 +1,37 @@ + + +
+ +
diff --git a/src/lib/components/LucideIcon.svelte b/src/lib/components/LucideIcon.svelte new file mode 100644 index 0000000..a217af4 --- /dev/null +++ b/src/lib/components/LucideIcon.svelte @@ -0,0 +1,94 @@ + + + diff --git a/src/lib/components/app/charts/BarChart.svelte b/src/lib/components/app/charts/BarChart.svelte new file mode 100644 index 0000000..2c0593e --- /dev/null +++ b/src/lib/components/app/charts/BarChart.svelte @@ -0,0 +1,264 @@ + + +
+
+
+ {#if !onFullScreen} + + {:else} + + {/if} + +
+
+
+ + + +
+ +
+
+
+ +
+
diff --git a/src/lib/components/app/charts/LineChart.svelte b/src/lib/components/app/charts/LineChart.svelte new file mode 100644 index 0000000..fde5e5a --- /dev/null +++ b/src/lib/components/app/charts/LineChart.svelte @@ -0,0 +1,267 @@ + + +
+
+
+ {#if !onFullScreen} + + {:else} + + {/if} + +
+
+
+ + + +
+ +
+
+
+ +
+
diff --git a/src/lib/components/app/charts/index.ts b/src/lib/components/app/charts/index.ts new file mode 100644 index 0000000..fcdf951 --- /dev/null +++ b/src/lib/components/app/charts/index.ts @@ -0,0 +1,4 @@ +import BarChart from './BarChart.svelte' +import LineChart from './LineChart.svelte' + +export { BarChart, LineChart } diff --git a/src/lib/components/app/common/BadgeColor.svelte b/src/lib/components/app/common/BadgeColor.svelte new file mode 100644 index 0000000..00bfff6 --- /dev/null +++ b/src/lib/components/app/common/BadgeColor.svelte @@ -0,0 +1,40 @@ + + +{#if children !== undefined}{@render children()}{:else}{color}{/if} + + diff --git a/src/lib/components/app/common/FilterEntity.svelte b/src/lib/components/app/common/FilterEntity.svelte new file mode 100644 index 0000000..a910ec0 --- /dev/null +++ b/src/lib/components/app/common/FilterEntity.svelte @@ -0,0 +1,61 @@ + + + diff --git a/src/lib/components/app/common/FilterNoBrand.svelte b/src/lib/components/app/common/FilterNoBrand.svelte new file mode 100644 index 0000000..6bd79f5 --- /dev/null +++ b/src/lib/components/app/common/FilterNoBrand.svelte @@ -0,0 +1,53 @@ + + + diff --git a/src/lib/components/app/common/Fqdns.svelte b/src/lib/components/app/common/Fqdns.svelte new file mode 100644 index 0000000..cef54d6 --- /dev/null +++ b/src/lib/components/app/common/Fqdns.svelte @@ -0,0 +1,17 @@ + +
+ {#each fqdns as fqdn, index(index)} + + {fqdn} + + {/each} +
diff --git a/src/lib/components/app/common/GlobalStatistics.svelte b/src/lib/components/app/common/GlobalStatistics.svelte new file mode 100644 index 0000000..a310def --- /dev/null +++ b/src/lib/components/app/common/GlobalStatistics.svelte @@ -0,0 +1,49 @@ + + +
+
+ {#if forBrand} + {Str.toTitleCase($t('entities.list.brand'))} + {forBrand.name} + {:else} + Toutes enseignes confondues + {/if} + + +
+ +
+ {#each Object.entries(globalStatistics) as globalStatisticEntry, index(index)} +
+
+
+ {$t(`statistics.global.${globalStatisticEntry[0]}.title`)} +
+
+ {globalStatisticEntry[1]} +
+
+
+ {/each} +
+
diff --git a/src/lib/components/app/common/HelpButton.svelte b/src/lib/components/app/common/HelpButton.svelte new file mode 100644 index 0000000..d94e7e1 --- /dev/null +++ b/src/lib/components/app/common/HelpButton.svelte @@ -0,0 +1,33 @@ + + + diff --git a/src/lib/components/app/common/Modal.svelte b/src/lib/components/app/common/Modal.svelte new file mode 100644 index 0000000..7fc1b36 --- /dev/null +++ b/src/lib/components/app/common/Modal.svelte @@ -0,0 +1,95 @@ + + +{#if isOpen} + + + + + close()} + open +> + + +{/if} + + diff --git a/src/lib/components/app/common/Pagination.svelte b/src/lib/components/app/common/Pagination.svelte new file mode 100644 index 0000000..0c34d29 --- /dev/null +++ b/src/lib/components/app/common/Pagination.svelte @@ -0,0 +1,243 @@ + + + +{#snippet previous()} + {#if previousPage === false} + + {:else} + + {/if} +{/snippet} + + +{#snippet next()} + {#if nextPage === false} + + {:else} + + {/if} +{/snippet} + +{#snippet currentPage(index: number, currentPageNumber: number)} + {#if paginatedData.totalPages < 5} + + {index + 1} + {:else} + +
{index + 1}
+ + + {/if} + +{/snippet} + +
+ + +
+
+ + {@render previous()} + + {#each Array(paginatedData.totalPages) as _, index (index)} + {#if index + 1 === currentPageNumber} + {@render currentPage(index, currentPageNumber)} + {/if} + {/each} + {@render next()} + +
+ + + + +
+ + +
+ +
+
diff --git a/src/lib/components/app/common/PerPage.svelte b/src/lib/components/app/common/PerPage.svelte new file mode 100644 index 0000000..765c601 --- /dev/null +++ b/src/lib/components/app/common/PerPage.svelte @@ -0,0 +1,86 @@ + + + diff --git a/src/lib/components/app/common/Search.svelte b/src/lib/components/app/common/Search.svelte new file mode 100644 index 0000000..6538812 --- /dev/null +++ b/src/lib/components/app/common/Search.svelte @@ -0,0 +1,142 @@ + + +
+ + + + +
diff --git a/src/lib/components/app/common/SortBy.svelte b/src/lib/components/app/common/SortBy.svelte new file mode 100644 index 0000000..025eece --- /dev/null +++ b/src/lib/components/app/common/SortBy.svelte @@ -0,0 +1,81 @@ + + +
+ + + +
diff --git a/src/lib/components/app/common/UserRole.svelte b/src/lib/components/app/common/UserRole.svelte new file mode 100644 index 0000000..bb27674 --- /dev/null +++ b/src/lib/components/app/common/UserRole.svelte @@ -0,0 +1,22 @@ + +{displayableRole} diff --git a/src/lib/components/app/common/index.ts b/src/lib/components/app/common/index.ts new file mode 100644 index 0000000..eb41831 --- /dev/null +++ b/src/lib/components/app/common/index.ts @@ -0,0 +1,18 @@ +import BadgeColor from './BadgeColor.svelte' +import FilterEntity from './FilterEntity.svelte' +import FilterNoBrand from './FilterNoBrand.svelte' +import Fqdns from './Fqdns.svelte' +import GlobalStatistics from './GlobalStatistics.svelte' +import HelpButton from './HelpButton.svelte' +import Modal from './Modal.svelte' +import Pagination from './Pagination.svelte' +import PerPage from './PerPage.svelte' +import Search from './Search.svelte' +import SortBy from './SortBy.svelte' +import UserRole from './UserRole.svelte' + +export { + BadgeColor, FilterEntity, FilterNoBrand, Fqdns, + GlobalStatistics, HelpButton, Modal, + Pagination, PerPage, Search, SortBy, UserRole, +} diff --git a/src/lib/components/app/import/ImportErrors.svelte b/src/lib/components/app/import/ImportErrors.svelte new file mode 100644 index 0000000..76ea824 --- /dev/null +++ b/src/lib/components/app/import/ImportErrors.svelte @@ -0,0 +1,75 @@ + + +
+ Cliquez ici pour voir le détails des erreurs d'importation +
+
+ + + + + + + + + + + + {#each failedImports as failedImport, index(index)} + {@const importedLine = failedImportToCol(failedImport)} + + {#if importedLine.length === 1} + + {:else} + + + + + + {/if} + + + {#if importedLine.length === 1} + + {:else} + + {/if} + + {/each} + +
{importedLine[0]} +

ligne : {importedLine[0]}

+

message : {importedLine[1]}

+

source : {importedLine[2]}

+

url : {importedLine[3]}

+

externe : {importedLine[4]}

+
+
+
+
diff --git a/src/lib/components/app/import/index.ts b/src/lib/components/app/import/index.ts new file mode 100644 index 0000000..c0072bc --- /dev/null +++ b/src/lib/components/app/import/index.ts @@ -0,0 +1,3 @@ +import ImportErrors from './ImportErrors.svelte' + +export { ImportErrors } diff --git a/src/lib/components/app/inputs/index.ts b/src/lib/components/app/inputs/index.ts new file mode 100644 index 0000000..6acc42b --- /dev/null +++ b/src/lib/components/app/inputs/index.ts @@ -0,0 +1,15 @@ +import InputCheckbox from './input-checkbox.svelte' +import InputCollectionTable from './input-collection-table.svelte' +import InputColor from './input-color.svelte' +import InputFile from './input-file.svelte' +import InputPassword from './input-password.svelte' +import Input from './input.svelte' +import SelectEntity from './select-entity.svelte' +import SelectInput from './select-input.svelte' +import SelectMultipleEntities from './select-multiple-entities.svelte' + +export { + Input, InputCheckbox, InputCollectionTable, + InputColor, InputFile, InputPassword, + SelectEntity, SelectInput, SelectMultipleEntities, +} diff --git a/src/lib/components/app/inputs/input-checkbox.svelte b/src/lib/components/app/inputs/input-checkbox.svelte new file mode 100644 index 0000000..7ab3c54 --- /dev/null +++ b/src/lib/components/app/inputs/input-checkbox.svelte @@ -0,0 +1,108 @@ + +
+ {#if label} + + {/if} + + {#if helper} +
+ {helper} +
+ {/if} + {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/input-collection-table.svelte b/src/lib/components/app/inputs/input-collection-table.svelte new file mode 100644 index 0000000..234bce2 --- /dev/null +++ b/src/lib/components/app/inputs/input-collection-table.svelte @@ -0,0 +1,105 @@ + + +
+ + {label} + {#if required} + * + {/if} + +
+ + {#if required} + + {/if} + +
+
+ {#if value.length === 0} + {$t('common.inputs.input-collection.empty')} + {/if} + {#each value as element, index(index)} +
+ + {element} + + + +
+ {#if index < value.length - 1} +
+ {/if} + {/each} +
+ {#if errors?.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/input-color.svelte b/src/lib/components/app/inputs/input-color.svelte new file mode 100644 index 0000000..e2abdd1 --- /dev/null +++ b/src/lib/components/app/inputs/input-color.svelte @@ -0,0 +1,107 @@ + +
+ {#if label} + + {/if} +
+ + + {#if helper} +
+ {helper} +
+ {/if} +
+ {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/input-file.svelte b/src/lib/components/app/inputs/input-file.svelte new file mode 100644 index 0000000..5c3fe2f --- /dev/null +++ b/src/lib/components/app/inputs/input-file.svelte @@ -0,0 +1,131 @@ + +
+ {#if label} + + {/if} + + + {#if files} +
+ {#each files as file, index(index)} + {limitFileName(file.name)} - {Str.humanFileSize(file.size)} + {/each} +
+ {/if} + {#if helper} +
+ {helper} +
+ {/if} + {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/input-password.svelte b/src/lib/components/app/inputs/input-password.svelte new file mode 100644 index 0000000..5f8800c --- /dev/null +++ b/src/lib/components/app/inputs/input-password.svelte @@ -0,0 +1,112 @@ + +
+ {#if label} + + {/if} +
+ + +
+ {#if helper} +
+ {helper} +
+ {/if} + {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/input.svelte b/src/lib/components/app/inputs/input.svelte new file mode 100644 index 0000000..9867ad3 --- /dev/null +++ b/src/lib/components/app/inputs/input.svelte @@ -0,0 +1,107 @@ + +
+ {#if label} + + {/if} + + {#if helper} +
+ {helper} +
+ {/if} + {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/select-entity.svelte b/src/lib/components/app/inputs/select-entity.svelte new file mode 100644 index 0000000..67b89f1 --- /dev/null +++ b/src/lib/components/app/inputs/select-entity.svelte @@ -0,0 +1,107 @@ + + +
+ + {#if previousPage !== false} + + {/if} + {#if nextPage !== false} + + {/if} +
diff --git a/src/lib/components/app/inputs/select-input.svelte b/src/lib/components/app/inputs/select-input.svelte new file mode 100644 index 0000000..a7b7093 --- /dev/null +++ b/src/lib/components/app/inputs/select-input.svelte @@ -0,0 +1,125 @@ + +
+ {#if label} + + {/if} + + {#if helper} +
+ {helper} +
+ {/if} + {#if errors.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+ {/if} +
diff --git a/src/lib/components/app/inputs/select-multiple-entities.svelte b/src/lib/components/app/inputs/select-multiple-entities.svelte new file mode 100644 index 0000000..eef6bf7 --- /dev/null +++ b/src/lib/components/app/inputs/select-multiple-entities.svelte @@ -0,0 +1,109 @@ + + +
+ + {label} + {#if required}*{/if} + + {#each options as option, index(index)} +
+
+ +
+
+ {/each} + {#if errors?.length} +
+ {#each errors as error, index(index)} + {error} + {/each} +
+{/if} + {#if previousPage !== false} + + {/if} + {#if nextPage !== false} + + {/if} +
diff --git a/src/lib/components/app/layout/Footer.svelte b/src/lib/components/app/layout/Footer.svelte new file mode 100644 index 0000000..c6ec29b --- /dev/null +++ b/src/lib/components/app/layout/Footer.svelte @@ -0,0 +1,25 @@ + + +
+
+
+
    +
  • {appName}
  • +
  • -
  • +
  • version : {appVersion}
  • +
+
+
+
diff --git a/src/lib/components/app/layout/Header.svelte b/src/lib/components/app/layout/Header.svelte new file mode 100644 index 0000000..2a28b82 --- /dev/null +++ b/src/lib/components/app/layout/Header.svelte @@ -0,0 +1,187 @@ + + +
+ + + + + +
+ + + +
+ + +
+ + + + + +
+ +
diff --git a/src/lib/components/app/layout/Message.svelte b/src/lib/components/app/layout/Message.svelte new file mode 100644 index 0000000..c8e6e8d --- /dev/null +++ b/src/lib/components/app/layout/Message.svelte @@ -0,0 +1,78 @@ + + + diff --git a/src/lib/components/app/layout/index.ts b/src/lib/components/app/layout/index.ts new file mode 100644 index 0000000..2a6bc67 --- /dev/null +++ b/src/lib/components/app/layout/index.ts @@ -0,0 +1,7 @@ +import Footer from './Footer.svelte' +import Header from './Header.svelte' +import Message from './Message.svelte' + +export { + Footer, Header, Message, +} diff --git a/src/lib/components/app/layout/menu/BrandSelector.svelte b/src/lib/components/app/layout/menu/BrandSelector.svelte new file mode 100644 index 0000000..5fff9ec --- /dev/null +++ b/src/lib/components/app/layout/menu/BrandSelector.svelte @@ -0,0 +1,70 @@ + + +
{ + changingActiveBrand = true + return async ({ update }): Promise => { + await update() + changingActiveBrand = false + // eslint-disable-next-line svelte/no-goto-without-base + return goto(url) + } + }} +> + {#if brands.length} + { + return { + label: Str.toTitleCase(brand.name), + value: brand.slug, + } + })} + outerClassList="join-item w-40 md:w-52" + required + bind:value={selectedBrand} + /> + {/if} + diff --git a/src/lib/components/app/layout/menu/MainMenu.svelte b/src/lib/components/app/layout/menu/MainMenu.svelte new file mode 100644 index 0000000..530743c --- /dev/null +++ b/src/lib/components/app/layout/menu/MainMenu.svelte @@ -0,0 +1,125 @@ + + + diff --git a/src/lib/components/app/layout/menu/ThemeChanger.svelte b/src/lib/components/app/layout/menu/ThemeChanger.svelte new file mode 100644 index 0000000..eca6ebd --- /dev/null +++ b/src/lib/components/app/layout/menu/ThemeChanger.svelte @@ -0,0 +1,55 @@ + + + + + diff --git a/src/lib/components/app/layout/menu/UserMenu.svelte b/src/lib/components/app/layout/menu/UserMenu.svelte new file mode 100644 index 0000000..408b1d6 --- /dev/null +++ b/src/lib/components/app/layout/menu/UserMenu.svelte @@ -0,0 +1,88 @@ + + + + + diff --git a/src/lib/components/app/layout/menu/index.ts b/src/lib/components/app/layout/menu/index.ts new file mode 100644 index 0000000..835f1db --- /dev/null +++ b/src/lib/components/app/layout/menu/index.ts @@ -0,0 +1,8 @@ +import BrandSelector from './BrandSelector.svelte' +import MainMenu from './MainMenu.svelte' +import ThemeChanger from './ThemeChanger.svelte' +import UserMenu from './UserMenu.svelte' + +export { + BrandSelector, MainMenu, ThemeChanger, UserMenu, +} diff --git a/src/lib/components/app/layout/navigation/ActionBar.svelte b/src/lib/components/app/layout/navigation/ActionBar.svelte new file mode 100644 index 0000000..efbb66d --- /dev/null +++ b/src/lib/components/app/layout/navigation/ActionBar.svelte @@ -0,0 +1,46 @@ + + +
+
+ {#if previousUrl} + + + {/if} + {#if children} + + {@render children()} + + {/if} + +

+ {pageTitle} + {#if subTitle !== undefined} + {subTitle} + {/if} +

+
+
diff --git a/src/lib/components/app/layout/navigation/Breadcrumb.svelte b/src/lib/components/app/layout/navigation/Breadcrumb.svelte new file mode 100644 index 0000000..b3b6f29 --- /dev/null +++ b/src/lib/components/app/layout/navigation/Breadcrumb.svelte @@ -0,0 +1,93 @@ + + +{#key breadcrumbParts.length} + +{/key} diff --git a/src/lib/components/app/layout/navigation/index.ts b/src/lib/components/app/layout/navigation/index.ts new file mode 100644 index 0000000..902de1d --- /dev/null +++ b/src/lib/components/app/layout/navigation/index.ts @@ -0,0 +1,4 @@ +import ActionBar from './ActionBar.svelte' +import Breadcrumb from './Breadcrumb.svelte' + +export { ActionBar, Breadcrumb } diff --git a/src/lib/components/app/statistics/StatisticsShortCutCard.svelte b/src/lib/components/app/statistics/StatisticsShortCutCard.svelte new file mode 100644 index 0000000..d43ab75 --- /dev/null +++ b/src/lib/components/app/statistics/StatisticsShortCutCard.svelte @@ -0,0 +1,48 @@ + + +
+
+

{title}

+ + +

{description}

+
+ +
+
+
diff --git a/src/lib/components/app/statistics/index.ts b/src/lib/components/app/statistics/index.ts new file mode 100644 index 0000000..ee2d568 --- /dev/null +++ b/src/lib/components/app/statistics/index.ts @@ -0,0 +1,3 @@ +import StatisticsShortCutCard from "./StatisticsShortCutCard.svelte"; + +export { StatisticsShortCutCard }; diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts new file mode 100644 index 0000000..0531d6f --- /dev/null +++ b/src/lib/components/index.ts @@ -0,0 +1,7 @@ +import HelpComponent from './HelpComponent.svelte' +import LucideIcon from './LucideIcon.svelte' + +export { + HelpComponent, + LucideIcon, +} diff --git a/src/lib/components/screens/ErrorScreen.svelte b/src/lib/components/screens/ErrorScreen.svelte new file mode 100644 index 0000000..58305f6 --- /dev/null +++ b/src/lib/components/screens/ErrorScreen.svelte @@ -0,0 +1,40 @@ + + +

{pageTitle}

+ +
+ +
+ + diff --git a/src/lib/components/screens/NotFoundScreen.svelte b/src/lib/components/screens/NotFoundScreen.svelte new file mode 100644 index 0000000..7f1dd4a --- /dev/null +++ b/src/lib/components/screens/NotFoundScreen.svelte @@ -0,0 +1,20 @@ + + +

Bad Link!

+ +

La page demandée n'a pas pu être trouvée

+ + diff --git a/src/lib/components/screens/index.ts b/src/lib/components/screens/index.ts new file mode 100644 index 0000000..00dab12 --- /dev/null +++ b/src/lib/components/screens/index.ts @@ -0,0 +1,6 @@ +import ErrorScreen from './ErrorScreen.svelte' +import NotFoundScreen from './NotFoundScreen.svelte' + +export { + ErrorScreen, NotFoundScreen, +} diff --git a/src/lib/enums/direction.enum.ts b/src/lib/enums/direction.enum.ts new file mode 100644 index 0000000..5dd5186 --- /dev/null +++ b/src/lib/enums/direction.enum.ts @@ -0,0 +1,5 @@ +export enum Direction { + auto = 'auto', + asc = 'asc', + desc = 'desc', +} diff --git a/src/lib/enums/globalStatistic.enum.ts b/src/lib/enums/globalStatistic.enum.ts new file mode 100644 index 0000000..ed9fa08 --- /dev/null +++ b/src/lib/enums/globalStatistic.enum.ts @@ -0,0 +1,9 @@ +export enum GlobalStatistic { + total_invalid_links_all = 'total_invalid_links_for_all', + total_invalid_links_brand = 'total_invalid_links_for_brand', + total_visits_all = 'total_visits_for_all', + total_visits_brand = 'total_visits_for_brand', + total_external_visits_all = 'total_external_visits_for_all', + total_external_visits_brand = 'total_external_visits_for_brand', + total_brand_less_visits = 'total_brand_less_visits', +} diff --git a/src/lib/enums/index.ts b/src/lib/enums/index.ts new file mode 100644 index 0000000..5bc0e80 --- /dev/null +++ b/src/lib/enums/index.ts @@ -0,0 +1,13 @@ +import { Direction } from './direction.enum' +import { GlobalStatistic } from './globalStatistic.enum' +import { Level } from './level.enum' +import { Role } from './roles.enum' +import { Validity } from './validity.enum' + +export { + Direction, + GlobalStatistic, + Level, + Role, + Validity, +} diff --git a/src/lib/enums/level.enum.ts b/src/lib/enums/level.enum.ts new file mode 100644 index 0000000..29f9306 --- /dev/null +++ b/src/lib/enums/level.enum.ts @@ -0,0 +1,5 @@ +export enum Level { + info = 'information', + warn = 'avertissement', + critical = 'critique', +} diff --git a/src/lib/enums/roles.enum.ts b/src/lib/enums/roles.enum.ts new file mode 100644 index 0000000..90e3057 --- /dev/null +++ b/src/lib/enums/roles.enum.ts @@ -0,0 +1,4 @@ +export enum Role { + ADMINISTRATOR = 'ADMINISTRATOR', + PUBLISHER = 'PUBLISHER', +} diff --git a/src/lib/enums/validity.enum.ts b/src/lib/enums/validity.enum.ts new file mode 100644 index 0000000..6bc37f9 --- /dev/null +++ b/src/lib/enums/validity.enum.ts @@ -0,0 +1,5 @@ +export enum Validity { + unknown, + valid, + invalid, +} diff --git a/src/lib/models/Brand.type.ts b/src/lib/models/Brand.type.ts new file mode 100644 index 0000000..0c4e59f --- /dev/null +++ b/src/lib/models/Brand.type.ts @@ -0,0 +1,9 @@ +import type { Uuid } from '$customType/value-objects' +import type { Entity } from '.' + +export default interface Brand extends Entity { + uuid: Uuid + slug: string + name: string + fqdns: string[] +} diff --git a/src/lib/models/Catalog.type.ts b/src/lib/models/Catalog.type.ts new file mode 100644 index 0000000..d8d04e5 --- /dev/null +++ b/src/lib/models/Catalog.type.ts @@ -0,0 +1,9 @@ +import type { Uuid } from '$customType/value-objects' +import type { Entity } from '.' + +export default interface Catalog extends Entity { + uuid: Uuid + slug: string + name: string + brand: Uuid +} diff --git a/src/lib/models/Entity.type.ts b/src/lib/models/Entity.type.ts new file mode 100644 index 0000000..75ff41b --- /dev/null +++ b/src/lib/models/Entity.type.ts @@ -0,0 +1,5 @@ +import type { Uuid } from '$customType/value-objects' + +export default interface Entity { + uuid?: Uuid +} diff --git a/src/lib/models/GlobalStatisticLine.type.ts b/src/lib/models/GlobalStatisticLine.type.ts new file mode 100644 index 0000000..495f42a --- /dev/null +++ b/src/lib/models/GlobalStatisticLine.type.ts @@ -0,0 +1,5 @@ +export default interface GlobalStatisticLine { + key: string + statistic: string + value: number +} diff --git a/src/lib/models/JwtToken.type.ts b/src/lib/models/JwtToken.type.ts new file mode 100644 index 0000000..2d04214 --- /dev/null +++ b/src/lib/models/JwtToken.type.ts @@ -0,0 +1,16 @@ +import type { Uuid } from '$customType/value-objects' + +export default interface JwtToken { + /** Encoded JWT token from server */ + originalToken: string + /** Endpoint Issuer */ + iss: URL + /** User uuid */ + sub: Uuid + /** Unix timeout timestamp */ + exp: number + /** Unix issued at timestamp */ + iat: number + /** Token unique identifier (not used) */ + jti: Uuid +} diff --git a/src/lib/models/Link.type.ts b/src/lib/models/Link.type.ts new file mode 100644 index 0000000..8c038a6 --- /dev/null +++ b/src/lib/models/Link.type.ts @@ -0,0 +1,25 @@ +import type { Source, Url, Uuid } from '$customType/value-objects' +import type { Validity } from '$lib/enums' +import type { Entity } from '.' + +export default interface Link extends Entity { + sourceAbsolute: Url + source: Source + /** A complete url */ + target: string + isExternal: boolean + validity: Validity + visits: number + brand: Uuid | null + catalog: { + slug: string + name: string + } | null + tag: { + uuid: Uuid + name: string + color: string + } | null + createdAt: string + updatedAt: string | null +} diff --git a/src/lib/models/LinkValidity.type.ts b/src/lib/models/LinkValidity.type.ts new file mode 100644 index 0000000..c21316d --- /dev/null +++ b/src/lib/models/LinkValidity.type.ts @@ -0,0 +1,4 @@ +export default interface LinkValidity { + target: string + isValid: boolean +} diff --git a/src/lib/models/ListOf.type.ts b/src/lib/models/ListOf.type.ts new file mode 100644 index 0000000..8a3f687 --- /dev/null +++ b/src/lib/models/ListOf.type.ts @@ -0,0 +1,9 @@ +import type { Entity } from '.' + +export default interface ListOf { + list: T[] + page: number + perPage: number + totalPages: number + totalItems: number +} diff --git a/src/lib/models/Message.type.ts b/src/lib/models/Message.type.ts new file mode 100644 index 0000000..5223d96 --- /dev/null +++ b/src/lib/models/Message.type.ts @@ -0,0 +1,3 @@ +export default interface Message { + message: string +} diff --git a/src/lib/models/ReportingSubscription.type.ts b/src/lib/models/ReportingSubscription.type.ts new file mode 100644 index 0000000..7122390 --- /dev/null +++ b/src/lib/models/ReportingSubscription.type.ts @@ -0,0 +1,4 @@ +export default interface ReportingSubscription { + user: string + brand: string +} diff --git a/src/lib/models/StatisticLine.type.ts b/src/lib/models/StatisticLine.type.ts new file mode 100644 index 0000000..eb37a6d --- /dev/null +++ b/src/lib/models/StatisticLine.type.ts @@ -0,0 +1,7 @@ +import type { Uuid } from '$customType/value-objects' + +export default interface StatisticLine { + uuid: Uuid + name: string + totalVisits: number +} diff --git a/src/lib/models/Tag.type.ts b/src/lib/models/Tag.type.ts new file mode 100644 index 0000000..45c8fff --- /dev/null +++ b/src/lib/models/Tag.type.ts @@ -0,0 +1,8 @@ +import type { HexColor, Uuid } from '$customType/value-objects' +import type { Entity } from '.' + +export default interface Tag extends Entity { + uuid: Uuid + name: string + color: HexColor +} diff --git a/src/lib/models/User.type.ts b/src/lib/models/User.type.ts new file mode 100644 index 0000000..7b5eee2 --- /dev/null +++ b/src/lib/models/User.type.ts @@ -0,0 +1,12 @@ +import type { Uuid } from '$customType/value-objects' +import type { Role } from '$lib/enums' +import type Entity from './Entity.type' + +export default interface User extends Entity { + uuid: Uuid + firstName: string + lastName: string + email: string + role: Role + brands?: Uuid[] +} diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts new file mode 100644 index 0000000..78895fa --- /dev/null +++ b/src/lib/models/index.ts @@ -0,0 +1,29 @@ +import type Brand from './Brand.type' +import type Catalog from './Catalog.type' +import type Entity from './Entity.type' +import type GlobalStatisticLine from './GlobalStatisticLine.type' +import type JwtToken from './JwtToken.type' +import type Link from './Link.type' +import type LinkValidity from './LinkValidity.type' +import type ListOf from './ListOf.type' +import type Message from './Message.type' +import type ReportingSubscription from './ReportingSubscription.type' +import type StatisticLine from './StatisticLine.type' +import type Tag from './Tag.type' +import type User from './User.type' + +export type { + Brand, + Catalog, + Entity, + GlobalStatisticLine, + JwtToken, + Link, + LinkValidity, + ListOf, + Message, + ReportingSubscription, + StatisticLine, + Tag, + User, +} diff --git a/src/lib/server/classes/FlashErrors.ts b/src/lib/server/classes/FlashErrors.ts new file mode 100644 index 0000000..0be7ac5 --- /dev/null +++ b/src/lib/server/classes/FlashErrors.ts @@ -0,0 +1,19 @@ +import type { ServerFieldsErrors } from '$customType' + +export default class FlashErrors { + #errors: ServerFieldsErrors + + constructor() { + this.#errors = {} + } + + public setErrors(errors: ServerFieldsErrors): void { + this.#errors = { ...errors } + } + + public consumeErrors(): ServerFieldsErrors { + const errors = this.#errors + this.#errors = {} + return errors + } +} diff --git a/src/lib/server/classes/FlashMessages.ts b/src/lib/server/classes/FlashMessages.ts new file mode 100644 index 0000000..0aaaa11 --- /dev/null +++ b/src/lib/server/classes/FlashMessages.ts @@ -0,0 +1,39 @@ +import type { FlashMessage } from '$customType' + +export default class FlashMessages { + #flashList: FlashMessage[] + + constructor() { + this.#flashList = [] + } + + public flash(message: FlashMessage['message'], at?: FlashMessage['at']): void { + this.flashMessage({ type: 'default', message, at }) + } + + public flashInfo(message: FlashMessage['message'], at?: FlashMessage['at']): void { + this.flashMessage({ type: 'info', message, at }) + } + + public flashWarning(message: FlashMessage['message'], at?: FlashMessage['at']): void { + this.flashMessage({ type: 'warning', message, at }) + } + + public flashSuccess(message: FlashMessage['message'], at?: FlashMessage['at']): void { + this.flashMessage({ type: 'success', message, at }) + } + + public flashError(message: FlashMessage['message'], at?: FlashMessage['at']): void { + this.flashMessage({ type: 'error', message, at }) + } + + private flashMessage(message: FlashMessage): void { + this.#flashList.push({ ...message }) + } + + public consumeMessages(): FlashMessage[] { + const messages = this.#flashList + this.#flashList = [] + return messages + } +} diff --git a/src/lib/server/classes/FlashOlds.ts b/src/lib/server/classes/FlashOlds.ts new file mode 100644 index 0000000..85fb01c --- /dev/null +++ b/src/lib/server/classes/FlashOlds.ts @@ -0,0 +1,19 @@ +import type { FormFieldsOlds } from '$customType/forms' + +export default class FlashOlds { + #olds: FormFieldsOlds + + constructor() { + this.#olds = {} + } + + public setOlds(olds: FormFieldsOlds): void { + this.#olds = { ...olds } + } + + public consumeOlds(): FormFieldsOlds { + const olds = this.#olds + this.#olds = {} + return olds + } +} diff --git a/src/lib/server/classes/RequestInputs.ts b/src/lib/server/classes/RequestInputs.ts new file mode 100644 index 0000000..c2bb8bb --- /dev/null +++ b/src/lib/server/classes/RequestInputs.ts @@ -0,0 +1,68 @@ +export type RequestInput = string | string[] | null + +export default class RequestInputs implements Map { + readonly #inputs: Map + + constructor(inputs: Record) { + this.#inputs = new Map() + + Object.entries({ ...inputs }).map(entry => this.#inputs.set(entry[0], entry[1])) + } + + public get(fieldName: string): RequestInput | undefined { + return this.#inputs.get(fieldName) + } + + public set(key: string, value: RequestInput): this { + this.#inputs.set(key, value) + return this + } + + public clear(): void { + this.clear() + } + + public delete(key: string): boolean { + return this.#inputs.delete(key) + } + + public forEach(callbackfn: (value: RequestInput, key: string, map: Map) => void, thisArg?: unknown): void { + for (const [key, value] of this.#inputs.entries()) { + callbackfn.call(thisArg, value, key, this.#inputs) + } + } + + public has(key: string): boolean { + return this.#inputs.has(key) + } + + public get size(): number { + return this.#inputs.size + } + + get [Symbol.toStringTag](): string { + return this.constructor.name + } + + *[Symbol.iterator](): MapIterator<[string, RequestInput]> { + for (const entry of this.#inputs) { + yield entry + } + } + + *entries(): MapIterator<[string, RequestInput]> { + yield * this[Symbol.iterator]() + } + + *keys(): MapIterator { + for (const entry of this.#inputs) { + yield entry[0] + } + } + + *values(): MapIterator { + for (const entry of this.#inputs) { + yield entry[1] + } + } +} diff --git a/src/lib/server/classes/UserSession.ts b/src/lib/server/classes/UserSession.ts new file mode 100644 index 0000000..b3f280e --- /dev/null +++ b/src/lib/server/classes/UserSession.ts @@ -0,0 +1,29 @@ +import type { Brand, JwtToken, User } from '$lib/models' + +export default class UserSession { + #token: JwtToken + #user: User + #activeBrand: Brand | undefined + + constructor(token: JwtToken, user: User) { + this.#token = token + this.#user = user + this.#activeBrand = undefined + } + + public getToken(): JwtToken { + return { ...this.#token } + } + + public getUser(): User { + return { ...this.#user } + } + + public getActiveBrand(): Brand | undefined { + return this.#activeBrand + } + + public setActiveBrand(brand: Brand): void { + this.#activeBrand = brand + } +} diff --git a/src/lib/server/classes/index.ts b/src/lib/server/classes/index.ts new file mode 100644 index 0000000..4ed2860 --- /dev/null +++ b/src/lib/server/classes/index.ts @@ -0,0 +1,13 @@ +import FlashErrors from './FlashErrors' +import FlashMessages from './FlashMessages' +import FlashOlds from './FlashOlds' +import RequestInputs from './RequestInputs' +import UserSession from './UserSession' + +export { + FlashErrors, + FlashMessages, + FlashOlds, + RequestInputs, + UserSession, +} diff --git a/src/lib/server/clients/HttpClient.ts b/src/lib/server/clients/HttpClient.ts new file mode 100644 index 0000000..b30ba09 --- /dev/null +++ b/src/lib/server/clients/HttpClient.ts @@ -0,0 +1,176 @@ +import { get } from 'svelte/store' + +import type { PinoLogger } from '$customType' +import type { JwtToken } from '$lib/models' +import { AuthenticationError, ServerError } from '../errors' +import { pinoLoggerStore } from '../stores' + +export enum HttpMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', +}; + +export type RequestParameters = Record +export type MultiPartRequestParameters = FormData +export type CallResponse = Promise + +export class HttpClient { + static readonly #pinoLogger: PinoLogger = get(pinoLoggerStore) + readonly #defautlHeaders: HeadersInit = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Accept-Language': 'fr', + } + + readonly #endpointHost: URL + readonly #bearerToken: string | null = null + + constructor(endpointHost: URL, bearerToken: string | null = null) { + if (endpointHost.host.length === 0) { + throw new Error('Host is required') + } + this.#endpointHost = endpointHost + this.#bearerToken = bearerToken + } + + /** + * Get an url to make calls + * @param path Url path append + * @returns a new URL with to make calls within HTTP client + */ + public pathUrl(path: string): URL { + return new URL(path, this.#endpointHost) + } + + public asAuthenticated(jwtToken: JwtToken | null | undefined = null): HttpClient { + if (!jwtToken) { + throw new AuthenticationError('Cannot run authenticated call without bearer token') + } + return new HttpClient(this.#endpointHost, jwtToken.originalToken) + } + + public sendMultiPartFile(url: URL, args: MultiPartRequestParameters, headers: HeadersInit = {}): CallResponse { + const init: RequestInit = { + method: HttpMethod.POST, + }, initHeaders = this.setupHeaders(headers) + + delete initHeaders['Content-Type' as keyof typeof initHeaders] + + url.host = this.#endpointHost.host + url.protocol = this.#endpointHost.protocol + + init.headers = initHeaders + init.body = args + + HttpClient.#pinoLogger.debug(new URLSearchParams( + args as unknown as Record, + ).toString()) + HttpClient.#pinoLogger.debug(url.toString()) + HttpClient.#pinoLogger.debug(init) + + return this.fetch(url, init) + } + + public stream(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.GET, url, args, headers) + } + + public get(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.GET, url, args, headers) + } + + public post(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.POST, url, args, headers) + } + + public patch(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.PATCH, url, args, headers) + } + + public put(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.PUT, url, args, headers) + } + + public delete(url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + return this.call(HttpMethod.DELETE, url, args, headers) + } + + public call(method: HttpMethod, url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): CallResponse { + const init = this.makeCallInit(method, url, args, headers) + return this.fetch(url, init) + } + + private makeCallInit(method: HttpMethod, url: URL, args: RequestParameters = {}, headers: HeadersInit = {}): RequestInit { + const init: RequestInit = { + method: method.toString(), + } + + init.headers = this.setupHeaders(headers) + + url.host = this.#endpointHost.host + url.protocol = this.#endpointHost.protocol + + this.setupParameters(init, method, url, args) + + return init + } + + private setupHeaders(headers: HeadersInit = {}): HeadersInit { + let httpHeaders: HeadersInit = { ...this.#defautlHeaders, ...headers } + + if (this.#bearerToken) { + httpHeaders = { ...httpHeaders, ...{ Authorization: `Bearer ${this.#bearerToken}` } } + } + + return httpHeaders + } + + private setupParameters(init: RequestInit, method: HttpMethod, url: URL, args: RequestParameters = {}): void { + if (method === HttpMethod.GET) { + const searchParams = new URLSearchParams(url.search); + [...Object.keys(args)].forEach(key => searchParams.set(key, String(args[key]))) + url.search = searchParams.toString() + } + else { + init.body = JSON.stringify(args) + } + HttpClient.#pinoLogger.debug(new URLSearchParams( + args as unknown as Record, + ).toString()) + HttpClient.#pinoLogger.debug(url.toString()) + HttpClient.#pinoLogger.debug(init) + } + + /** + * Make a fetch call + * @throws ServerError In case of a communication error + */ + private fetch(url: URL, init: RequestInit): CallResponse { + return fetch(url, init).then(this.checkReponseIs200) + .catch(this.handleResponseIs500).then() + } + + private checkReponseIs200(response: Response): Response { + // ? Between 200 and 299 status, ignore 4xx ? + if (!response.ok && !(response.status >= 400 && response.status < 499)) { + const message = `Server Response was not ok '${response.status}'` + HttpClient.#pinoLogger.error(message) + HttpClient.#pinoLogger.debug(response.statusText) + throw new ServerError(message) + } + return response + } + + private handleResponseIs500(cause: unknown): void { + if (cause instanceof Error) { + const message = `Call failed '${cause.message}'` + HttpClient.#pinoLogger.error(message) + throw new ServerError(message, { cause }) + } + HttpClient.#pinoLogger.error(cause) + throw new ServerError('Unknown error when calling server') + } +} diff --git a/src/lib/server/clients/Z3/Z3Client.ts b/src/lib/server/clients/Z3/Z3Client.ts new file mode 100644 index 0000000..66882c6 --- /dev/null +++ b/src/lib/server/clients/Z3/Z3Client.ts @@ -0,0 +1,178 @@ +import { get } from 'svelte/store' +import { z, ZodObject, type ZodTypeAny } from 'zod' + +import type { JsonValue, PinoLogger } from '$customType' +import type { JwtToken, Message, User } from '$lib/models' +import { + AuthorizationError, JwtAuthenticationError, + NotFoundError, ServerError, ValidationError, +} from '$lib/server/errors' +import { authorizationErrorSchema, userSchema, validationErrors } from '$lib/server/schemas' +import { loginSchema } from '$lib/server/schemas/auth' +import { JwtDecodeService } from '$lib/server/services' +import { pinoLoggerStore } from '$lib/server/stores' +import { Str } from '$lib/tools' +import type { HttpClient } from '../HttpClient' + +export default class Z3Client { + readonly #apiPrefix: string = '/api/v1' + readonly #httpClient: HttpClient + readonly #jwtDecodeService: JwtDecodeService + static readonly #pinoLogger: PinoLogger = get(pinoLoggerStore) + + constructor(httpClient: HttpClient, jwtDecodeService: JwtDecodeService) { + this.#httpClient = httpClient + this.#jwtDecodeService = jwtDecodeService + } + + public getHttpClient(): HttpClient { + return this.#httpClient + } + + async authenticate(login: string, password: string): Promise { + const response: Response = await this.#httpClient.post( + this.#httpClient.pathUrl(`${this.#apiPrefix}/auth/login`), + { login, password }, + ) + + // ? Authentication failed ? + if (response.status === 401) { + Z3Client.#pinoLogger.debug('Failed authentication') + return false + } + + const data = await Z3Client.reponseToData(response, loginSchema) + const jwtToken = this.#jwtDecodeService.decodePayload(data.token) + + return jwtToken + } + + async authenticatedUser(jwtToken: JwtToken | undefined): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/auth/me`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, userSchema) + } + + async changePassword(jwtToken: JwtToken | undefined, password: string, newPassword: string): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/auth/password`), { + password, + 'new-password': newPassword, + }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, z.object({ + message: z.string(), + })) + } + + /** + * ? Authentication failed ? + */ + public static assertWasAuthenticated(response: Response): void { + if (response.status === 401) { + const message = 'Not authenticated with JWT' + Z3Client.#pinoLogger.debug(message) + throw new JwtAuthenticationError(message) + } + } + + public static assertResponseIsOk(response: Response): void { + if ( + response.status !== 200 + && response.status !== 403 + && response.status !== 404 + && response.status !== 422 + ) { + response.text().then(text => + this.#pinoLogger.error(text)) + this.#pinoLogger.error(`Error on backend call code '${response.status}'`) + throw new ServerError(`Unhandled server response ${response.status}`) + } + } + + /** + * Get and validate data from the server response + * + * @param response The fetch http Response + * @param schema ZodSchema to validate the reponse + * @returns Reponse data + * @throws Error if the response could not be validated + */ + public static async reponseToData(response: Response, schema: T): Promise> { + const dataResponse = await this.reponseToJson(response) + const result = schema.safeParse(dataResponse) + + if (response.status === 403) { + const message = 'Not authorized, CODE ' + response.status + Z3Client.#pinoLogger.debug(message) + const authorizationError = authorizationErrorSchema.safeParse(dataResponse) + throw new AuthorizationError(authorizationError.data?.reason ?? 'Unauthorized', message) + } + + if (response.status === 404) { + const message = 'Not found, CODE ' + response.status + Z3Client.#pinoLogger.debug(message) + throw new NotFoundError(message) + } + + if (response.status === 422) { + const message = 'Invalid request, CODE ' + response.status + + if (schema instanceof ZodObject) { + Z3Client.#pinoLogger.debug(message) + + // Try to map response error field to our schema + const propertyNames = Object.keys(schema.shape) + const jsonErrors = validationErrors.safeParse(dataResponse) + const errors: Record = jsonErrors.data?.errors ?? {} + const mappedErrors: Record = {} + + for (const [key, value] of Object.entries(errors)) { + const matchingPropertyName = propertyNames.find(property => property === key) + ?? propertyNames.find(property => property === Str.toCamelCase(key)) + ?? propertyNames.find(property => property === Str.toSnakeCase(key)) + ?? Str.toKebabCase(key) + mappedErrors[matchingPropertyName] = value + } + throw new ValidationError(mappedErrors ?? {}, message) + } + + throw new ValidationError(dataResponse?.['errors' as keyof typeof dataResponse] ?? {}, message) + } + + if (!result.success) { + const message = 'Failed to validate server response, CODE ' + response.status + Z3Client.#pinoLogger.error(message) + Z3Client.#pinoLogger.debug(result.error) + throw new ServerError(message) + } + + return result.data + } + + /** + * Decode html response from json + * @throws Error if decoding from json fails + */ + public static async reponseToJson(response: Response): Promise { + let body = '' + try { + body = await response.text() + return JSON.parse(body) + } + catch (cause) { + const message = 'Failed to decode reponse as json' + this.#pinoLogger.debug(body.slice(0, 100)) + throw new Error(message, { cause }) + } + } +} diff --git a/src/lib/server/clients/Z3/index.ts b/src/lib/server/clients/Z3/index.ts new file mode 100644 index 0000000..775bd33 --- /dev/null +++ b/src/lib/server/clients/Z3/index.ts @@ -0,0 +1,5 @@ +import Z3Client from './Z3Client' + +export { + Z3Client, +} diff --git a/src/lib/server/clients/Z3/repositories/BrandRepository.ts b/src/lib/server/clients/Z3/repositories/BrandRepository.ts new file mode 100644 index 0000000..a39642b --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/BrandRepository.ts @@ -0,0 +1,106 @@ +import type { Slug } from '$customType/value-objects' +import type { Brand, JwtToken, ListOf, Message } from '$lib/models' +import { NotFoundError } from '$lib/server/errors' +import { brandSchema, listOfEntitySchema, messageSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface CreateBrand { + name: string + fqdns: string[] +} + +export interface UpdateBrand { + slug: Slug + name: string +} + +export class BrandRepository { + readonly #apiPrefix: string = '/api/v1/brands' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async listBrands( + jwtToken: JwtToken | undefined, + page: number, + perPage = 5, + ): Promise> { + if (page < 1) { + throw new Error('Page cannot be less than 1') + } + + if (perPage < 1) { + throw new Error('PerPage cannot be less than 1') + } + + if (perPage > 50) { + throw new Error('PerPage cannot be more than 50') + } + + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(this.#apiPrefix), { page, perPage }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, listOfEntitySchema(brandSchema)) + } + + /** @throws {NotFoundError} */ + async getBrand(jwtToken: JwtToken | undefined, slug: Slug): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${slug}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + try { + return await Z3Client.reponseToData(response, brandSchema) + } + catch (e) { + if (e instanceof NotFoundError) { + return null + } + throw e + } + } + + async createBrand(jwtToken: JwtToken | undefined, { name, fqdns }: CreateBrand): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(this.#apiPrefix), { name, fqdns }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, brandSchema) + } + + /** @throws {NotFoundError} */ + async updateBrand(jwtToken: JwtToken | undefined, updateBrand: UpdateBrand): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${updateBrand.slug}`), { ...updateBrand }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, brandSchema) + } + + async deleteBrand(jwtToken: JwtToken | undefined, slug: Slug): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).delete( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${slug}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/CatalogRepository.ts b/src/lib/server/clients/Z3/repositories/CatalogRepository.ts new file mode 100644 index 0000000..322759a --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/CatalogRepository.ts @@ -0,0 +1,106 @@ +import type { Slug, Uuid } from '$customType/value-objects' +import type { Catalog, JwtToken, ListOf, Message } from '$lib/models' +import { NotFoundError } from '$lib/server/errors' +import { catalogSchema, listOfEntitySchema, messageSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface CreateCatalog { + brand: Uuid + name: string +} + +export interface UpdateCatalog { + slug: Slug + name: string +} + +export class CatalogRepository { + readonly #apiPrefix: string = '/api/v1/catalogs' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async listCatalogs( + jwtToken: JwtToken | undefined, + page: number, perPage = 5, + brand: Uuid | undefined = undefined, + ): Promise> { + if (page < 1) { + throw new Error('Page cannot be less than 1') + } + + if (perPage < 1) { + throw new Error('PerPage cannot be less than 1') + } + + if (perPage > 50) { + throw new Error('PerPage cannot be more than 50') + } + + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(this.#apiPrefix), { page, perPage, ...(brand ? { brand } : {}) }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, listOfEntitySchema(catalogSchema)) + } + + /** @throws {NotFoundError} */ + async getCatalog(jwtToken: JwtToken | undefined, slug: Slug): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${slug}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + try { + return await Z3Client.reponseToData(response, catalogSchema) + } + catch (e) { + if (e instanceof NotFoundError) { + return null + } + throw e + } + } + + async createCatalog(jwtToken: JwtToken | undefined, { brand, name }: CreateCatalog): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(this.#apiPrefix), { brand, name }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, catalogSchema) + } + + /** @throws {NotFoundError} */ + async updateCatalog(jwtToken: JwtToken | undefined, updateCatalog: UpdateCatalog): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${updateCatalog.slug}`), { ...updateCatalog }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, catalogSchema) + } + + async deleteCatalog(jwtToken: JwtToken | undefined, slug: Slug): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).delete( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${slug}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/LinkRepository.ts b/src/lib/server/clients/Z3/repositories/LinkRepository.ts new file mode 100644 index 0000000..05856f9 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/LinkRepository.ts @@ -0,0 +1,256 @@ +import type { ImportResponse } from '$customType' +import type { Slug, Uuid } from '$customType/value-objects' +import type { Direction } from '$lib/enums' +import type { JwtToken, Link, LinkValidity, ListOf, Message } from '$lib/models' +import { ServerError } from '$lib/server/errors' +import { + linkImportFailuresSchema, linkSchema, linkValiditySchema, + listOfEntitySchema, messageSchema, +} from '$lib/server/schemas' +import type { HttpClient, RequestParameters } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface CreateLink { + source: string | null + target: string + is_external: boolean + brand?: Uuid + catalog?: Slug + tag?: Uuid +} + +export interface UpdateLink { + source: string + target: string + is_external: boolean + catalog?: Slug + tag?: Uuid +} + +export class LinkRepository { + readonly #apiPrefix: string = '/api/v1/links' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async listLinks( + jwtToken: JwtToken | undefined, + page: number, perPage = 5, + filter: { brand: Uuid | undefined, catalog: Slug | undefined, tag: Uuid | undefined }, + sorter: { sortBy: string, direction: Exclude } | null, + searcher: { searchOn: string, keywords: string } | null, + ): Promise> { + let parameters: RequestParameters = {} + + if (page < 1) { + throw new Error('Page cannot be less than 1') + } + + if (perPage < 1) { + throw new Error('PerPage cannot be less than 1') + } + + if (perPage > 50) { + throw new Error('PerPage cannot be more than 50') + } + if (sorter !== null && sorter.sortBy.length < 1) { + throw new Error('sortBy cannot be empty') + } + + parameters = { + page, perPage, + } + + if (sorter) { + parameters['orderBy'] = sorter.sortBy + parameters['direction'] = sorter.direction + } + if (searcher) { + parameters['searchOn'] = searcher.searchOn + parameters['keywords'] = searcher.keywords + } + + if (filter.brand) { + parameters['brand'] = filter.brand + } + if (filter.catalog) { + parameters['catalog'] = filter.catalog + } + if (filter.tag) { + parameters['tag'] = filter.tag + } + + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(this.#apiPrefix), parameters, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, listOfEntitySchema(linkSchema)) + } + + async getLink(jwtToken: JwtToken | undefined, source: string): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${source}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkSchema) + } + + async createLink(jwtToken: JwtToken | undefined, { source, target, is_external, brand, catalog, tag }: CreateLink): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(this.#apiPrefix), { + source, target, is_external, + ...(brand ? { brand } : { }), + ...(catalog ? { catalog } : { }), + ...(tag ? { tag } : { }), + }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkSchema) + } + + async updateLink(jwtToken: JwtToken | undefined, updateLink: UpdateLink): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${updateLink.source}`), { ...updateLink }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkSchema) + } + + async deleteLink(jwtToken: JwtToken | undefined, source: string): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).delete( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${source}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } + + async verifyLink(jwtToken: JwtToken | undefined, source: string): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${source}/verify`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkValiditySchema) + } + + async exportLinks(jwtToken: JwtToken | undefined): Promise>> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}-export`), + {}, + { + Accept: 'application/octet-stream', + }, + ) + let stream: ReadableStream> | null = null + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + stream = response.body + + if (stream === null) { + throw new ServerError('Empty stream on export links') + } + + return stream + } + + async exportBrandLinks(jwtToken: JwtToken | undefined, brandSlug: Slug): Promise>> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}-export/brands/${brandSlug}`), + {}, + { + Accept: 'application/octet-stream', + }, + ) + let stream: ReadableStream> | null = null + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + stream = response.body + + if (stream === null) { + throw new ServerError('Empty stream on export links') + } + + return stream + } + + async exportCatalogLinks(jwtToken: JwtToken | undefined, catalogSlug: Slug): Promise>> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}-export/catalogs/${catalogSlug}`), + {}, + { + Accept: 'application/octet-stream', + }, + ) + let stream: ReadableStream> | null = null + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + stream = response.body + + if (stream === null) { + throw new ServerError('Empty stream on export links') + } + + return stream + } + + async importBrandLinks(jwtToken: JwtToken | undefined, brandUuid: Uuid, tagUuid: Uuid | undefined, file: File): Promise { + const body = new FormData() + if (tagUuid) { + body.set('tag', tagUuid) + } + body.set('file', file) + + const response = await this.#httpClient.asAuthenticated(jwtToken).sendMultiPartFile( + this.#httpClient.pathUrl(`${this.#apiPrefix}-import/brands/${brandUuid}`), + body, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkImportFailuresSchema) + } + + async importCatalogLinks(jwtToken: JwtToken | undefined, catalogUuid: Uuid, tagUuid: Uuid | undefined, file: File): Promise { + const body = new FormData() + if (tagUuid) { + body.set('tag', tagUuid) + } + body.set('file', file) + + const response = await this.#httpClient.asAuthenticated(jwtToken).sendMultiPartFile( + this.#httpClient.pathUrl(`${this.#apiPrefix}-import/catalogs/${catalogUuid}`), + body, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkImportFailuresSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/LinkStatisticsRepository.ts b/src/lib/server/clients/Z3/repositories/LinkStatisticsRepository.ts new file mode 100644 index 0000000..d1872a1 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/LinkStatisticsRepository.ts @@ -0,0 +1,71 @@ +import type { Uuid } from '$customType/value-objects' +import type { GlobalStatisticLine, JwtToken, StatisticLine } from '$lib/models' +import { globalStatisticsSchema, linkStatisticsSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export class LinkStatisticsRepository { + readonly #apiPrefix: string = '/api/v1/links/statistics' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async globalStatistics(jwtToken: JwtToken | undefined): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, globalStatisticsSchema) + } + + async globalStatisticsForBrand(jwtToken: JwtToken | undefined, brand: Uuid): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${brand}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, globalStatisticsSchema) + } + + async linkStatisticsForBrands(jwtToken: JwtToken | undefined): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/brands`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkStatisticsSchema) + } + + async linkCatalogStatisticsFor(jwtToken: JwtToken | undefined, brandId: Uuid): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/catalog`), + { brand: brandId }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkStatisticsSchema) + } + + async linkTagStatisticsFor(jwtToken: JwtToken | undefined, brandId: Uuid): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/tag`), + { brand: brandId }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, linkStatisticsSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/ReportingSubscriptionRepository.ts b/src/lib/server/clients/Z3/repositories/ReportingSubscriptionRepository.ts new file mode 100644 index 0000000..8ba9746 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/ReportingSubscriptionRepository.ts @@ -0,0 +1,36 @@ +import type { Uuid } from '$customType/value-objects' +import type { JwtToken, Message, ReportingSubscription } from '$lib/models' +import { messageSchema, reportingSubscriptionsSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export class ReportingSubscriptionRepository { + readonly #apiPrefix: string = '/api/v1/reporting/subscriptions' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async getReportingSubscriptionsFor(jwtToken: JwtToken | undefined, user: Uuid): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${user}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, reportingSubscriptionsSchema) + } + + async updateUserReportingSubscriptions(jwtToken: JwtToken | undefined, user: Uuid, brands: Uuid[]): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${user}`), { brands }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/StatisticsRepository.ts b/src/lib/server/clients/Z3/repositories/StatisticsRepository.ts new file mode 100644 index 0000000..6871fe7 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/StatisticsRepository.ts @@ -0,0 +1,90 @@ +import type { JwtToken } from '$lib/models' +import { invalidLinksGroupedBySchema, topReferrers as topReferrersSchema, topSources, uniqueVisitorsGroupedBySchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface TopSource { + source: string, + brand: string, + count: number +} + +export interface TopReferrer { + referrer: string, + brand: string, + count: number +} + +export interface CountByDay { + dayOfYear: Date + count: number +} + +export interface CountByWeek { + weekOfYear: Date + count: number +} + +export class StatisticsRepository { + readonly #apiPrefix: string = '/api/v1/statistics' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async mostVisitedSources(jwtToken: JwtToken | undefined): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/top/sources`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, topSources) + } + + async mostFrequentReferrers(jwtToken: JwtToken | undefined): Promise { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/top/referrers`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, topReferrersSchema) + } + + async uniqueVisitorsByBrands(jwtToken: JwtToken | undefined): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/unique-visitors/brands`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, uniqueVisitorsGroupedBySchema) + } + + async uniqueVisitorsByCatalogs(jwtToken: JwtToken | undefined): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/unique-visitors/catalogs`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, uniqueVisitorsGroupedBySchema) + } + + async invalidLinksPerWeekPerBrand(jwtToken: JwtToken | undefined): Promise> { + const response = await this.#httpClient.asAuthenticated(jwtToken).stream( + this.#httpClient.pathUrl(`${this.#apiPrefix}/invalid-links/per-week-per-brand`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, invalidLinksGroupedBySchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/TagRepository.ts b/src/lib/server/clients/Z3/repositories/TagRepository.ts new file mode 100644 index 0000000..bef41a1 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/TagRepository.ts @@ -0,0 +1,103 @@ +import type { HexColor, Slug, Uuid } from '$customType/value-objects' +import type { JwtToken, ListOf, Message, Tag } from '$lib/models' +import { NotFoundError } from '$lib/server/errors' +import { listOfEntitySchema, messageSchema, tagSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface CreateTag { + name: string + color: HexColor +} + +export interface UpdateTag { + uuid: Uuid + name: string + color: HexColor +} + +export class TagRepository { + readonly #apiPrefix: string = '/api/v1/tags' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async listTags(jwtToken: JwtToken | undefined, page: number, perPage = 5): Promise> { + if (page < 1) { + throw new Error('Page cannot be less than 1') + } + + if (perPage < 1) { + throw new Error('PerPage cannot be less than 1') + } + + if (perPage > 50) { + throw new Error('PerPage cannot be more than 50') + } + + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(this.#apiPrefix), { page, perPage }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, listOfEntitySchema(tagSchema)) + } + + /** @throws {NotFoundError} */ + async getTag(jwtToken: JwtToken | undefined, id: Uuid): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${id}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + try { + return await Z3Client.reponseToData(response, tagSchema) + } + catch (e) { + if (e instanceof NotFoundError) { + return null + } + throw e + } + } + + async createTag(jwtToken: JwtToken | undefined, { name, color }: CreateTag): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(this.#apiPrefix), { name, color }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, tagSchema) + } + + /** @throws {NotFoundError} */ + async updateTag(jwtToken: JwtToken | undefined, updateTag: UpdateTag): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${updateTag.uuid}`), { ...updateTag }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, tagSchema) + } + + async deleteTag(jwtToken: JwtToken | undefined, slug: Slug): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).delete( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${slug}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/UserRepository.ts b/src/lib/server/clients/Z3/repositories/UserRepository.ts new file mode 100644 index 0000000..9bcf9a0 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/UserRepository.ts @@ -0,0 +1,110 @@ +import type { Uuid } from '$customType/value-objects' +import type { Role } from '$lib/enums' +import type { JwtToken, ListOf, Message, User } from '$lib/models' +import { listOfEntitySchema, messageSchema, userSchema } from '$lib/server/schemas' +import type { HttpClient } from '../../HttpClient' +import Z3Client from '../Z3Client' + +export interface CreateUser { + firstName: string + lastName: string + email: string + role: Role + login: string + brands: Uuid[] +} + +export interface UpdateUser { + uuid: Uuid + firstName: string + lastName: string + email: string + brands: Uuid[] +} + +export class UserRepository { + readonly #apiPrefix: string = '/api/v1/users' + readonly #httpClient: HttpClient + + constructor(httpClient: HttpClient) { + this.#httpClient = httpClient + } + + async listUsers(jwtToken: JwtToken | undefined, page: number, perPage = 5): Promise> { + if (page < 1) { + throw new Error('Page cannot be less than 1') + } + + if (perPage < 1) { + throw new Error('PerPage cannot be less than 1') + } + + if (perPage > 50) { + throw new Error('PerPage cannot be more than 50') + } + + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(this.#apiPrefix), { page, perPage }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, listOfEntitySchema(userSchema)) + } + + async getUser(jwtToken: JwtToken | undefined, uuid: Uuid): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).get( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${uuid}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, userSchema) + } + + async resetUserPassword(jwtToken: JwtToken | undefined, uuid: Uuid): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${uuid}/reset`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await response.status === 200 + } + + async createUser(jwtToken: JwtToken | undefined, { firstName, lastName, email, role, login, brands }: CreateUser): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).post( + this.#httpClient.pathUrl(this.#apiPrefix), { firstName, lastName, email, role, login, brands }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, userSchema) + } + + async updateUser(jwtToken: JwtToken | undefined, updateUser: UpdateUser): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).put( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${updateUser.uuid}`), { ...updateUser }, + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, userSchema) + } + + async deleteUser(jwtToken: JwtToken | undefined, uuid: Uuid): Promise { + const response: Response = await this.#httpClient.asAuthenticated(jwtToken).delete( + this.#httpClient.pathUrl(`${this.#apiPrefix}/${uuid}`), + ) + + Z3Client.assertWasAuthenticated(response) + Z3Client.assertResponseIsOk(response) + + return await Z3Client.reponseToData(response, messageSchema) + } +} diff --git a/src/lib/server/clients/Z3/repositories/index.ts b/src/lib/server/clients/Z3/repositories/index.ts new file mode 100644 index 0000000..7311193 --- /dev/null +++ b/src/lib/server/clients/Z3/repositories/index.ts @@ -0,0 +1,51 @@ +import { BrandRepository, type CreateBrand, type UpdateBrand } from './BrandRepository' +import { CatalogRepository, type CreateCatalog, type UpdateCatalog } from './CatalogRepository' +import { LinkRepository, type CreateLink, type UpdateLink } from './LinkRepository' +import { LinkStatisticsRepository } from './LinkStatisticsRepository' +import { ReportingSubscriptionRepository } from './ReportingSubscriptionRepository' +import { StatisticsRepository, type CountByDay, type CountByWeek } from './StatisticsRepository' +import { TagRepository, type CreateTag, type UpdateTag } from './TagRepository' +import { UserRepository, type CreateUser, type UpdateUser } from './UserRepository' + +export { + BrandRepository, + type CreateBrand, + type UpdateBrand +} + +export { + CatalogRepository, + type CreateCatalog, + type UpdateCatalog +} + +export { + LinkRepository, + type CreateLink, + type UpdateLink +} + +export { + LinkStatisticsRepository +} + +export { + ReportingSubscriptionRepository +} + +export { + TagRepository, + type CreateTag, + type UpdateTag +} + +export { + UserRepository, + type CreateUser, + type UpdateUser +} + +export { + StatisticsRepository, type CountByDay, type CountByWeek +} + diff --git a/src/lib/server/clients/index.ts b/src/lib/server/clients/index.ts new file mode 100644 index 0000000..b6ba8e7 --- /dev/null +++ b/src/lib/server/clients/index.ts @@ -0,0 +1,13 @@ +import type { CallResponse, MultiPartRequestParameters, RequestParameters } from './HttpClient' +import { HttpClient, HttpMethod } from './HttpClient' + +export { + HttpClient, + HttpMethod, +} + +export type { + CallResponse, + MultiPartRequestParameters, + RequestParameters, +} diff --git a/src/lib/server/errors/AuthenticationError.ts b/src/lib/server/errors/AuthenticationError.ts new file mode 100644 index 0000000..edccab6 --- /dev/null +++ b/src/lib/server/errors/AuthenticationError.ts @@ -0,0 +1 @@ +export default class AuthenticationError extends Error { } diff --git a/src/lib/server/errors/AuthorizationError.ts b/src/lib/server/errors/AuthorizationError.ts new file mode 100644 index 0000000..1b40e12 --- /dev/null +++ b/src/lib/server/errors/AuthorizationError.ts @@ -0,0 +1,12 @@ +export default class AuthorizationError extends Error { + #reason: string + + constructor(reason = '', message?: string, options?: ErrorOptions) { + super(message, options) + this.#reason = reason + } + + public getReason(): string { + return this.#reason + } +} diff --git a/src/lib/server/errors/JwtAuthenticationError.ts b/src/lib/server/errors/JwtAuthenticationError.ts new file mode 100644 index 0000000..c8e63eb --- /dev/null +++ b/src/lib/server/errors/JwtAuthenticationError.ts @@ -0,0 +1 @@ +export default class JwtAuthenticationError extends Error { } diff --git a/src/lib/server/errors/NotFoundError.ts b/src/lib/server/errors/NotFoundError.ts new file mode 100644 index 0000000..94f19bf --- /dev/null +++ b/src/lib/server/errors/NotFoundError.ts @@ -0,0 +1 @@ +export default class NotFoundError extends Error { } diff --git a/src/lib/server/errors/ServerError.ts b/src/lib/server/errors/ServerError.ts new file mode 100644 index 0000000..4d504ee --- /dev/null +++ b/src/lib/server/errors/ServerError.ts @@ -0,0 +1 @@ +export default class ServerError extends Error { } diff --git a/src/lib/server/errors/ValidationError.ts b/src/lib/server/errors/ValidationError.ts new file mode 100644 index 0000000..ab1daaa --- /dev/null +++ b/src/lib/server/errors/ValidationError.ts @@ -0,0 +1,14 @@ +import type { ServerFieldsErrors } from '$customType' + +export default class ValidationError extends Error { + #fieldsErrors: ServerFieldsErrors + + constructor(fieldsErrors: ServerFieldsErrors, message?: string, options?: ErrorOptions) { + super(message, options) + this.#fieldsErrors = fieldsErrors + } + + public getFieldErrors(): ServerFieldsErrors { + return this.#fieldsErrors + } +} diff --git a/src/lib/server/errors/index.ts b/src/lib/server/errors/index.ts new file mode 100644 index 0000000..25cbb89 --- /dev/null +++ b/src/lib/server/errors/index.ts @@ -0,0 +1,15 @@ +import AuthenticationError from './AuthenticationError' +import AuthorizationError from './AuthorizationError' +import JwtAuthenticationError from './JwtAuthenticationError' +import NotFoundError from './NotFoundError' +import ServerError from './ServerError' +import ValidationError from './ValidationError' + +export { + AuthenticationError, + AuthorizationError, + JwtAuthenticationError, + NotFoundError, + ServerError, + ValidationError, +} diff --git a/src/lib/server/memory/FlashErrorsManager.ts b/src/lib/server/memory/FlashErrorsManager.ts new file mode 100644 index 0000000..dffcad9 --- /dev/null +++ b/src/lib/server/memory/FlashErrorsManager.ts @@ -0,0 +1,19 @@ +// Import types + +import type { UserSessionId } from '$customType' +import { FlashErrors } from '../classes' + +export default class FlashErrorsManager { + readonly #flashErrorsMap: Map + + constructor() { + this.#flashErrorsMap = new Map() + } + + get(sessionId: UserSessionId): FlashErrors { + const flashErrors = this.#flashErrorsMap.get(sessionId.toString()) ?? new FlashErrors() + this.#flashErrorsMap.set(sessionId.toString(), flashErrors) + + return flashErrors + } +} diff --git a/src/lib/server/memory/FlashMessagesManager.ts b/src/lib/server/memory/FlashMessagesManager.ts new file mode 100644 index 0000000..65a9a29 --- /dev/null +++ b/src/lib/server/memory/FlashMessagesManager.ts @@ -0,0 +1,17 @@ +import type { UserSessionId } from '$customType' +import { FlashMessages } from '../classes' + +export default class FlashMessagesManager { + readonly #flashMessagesMap: Map + + constructor() { + this.#flashMessagesMap = new Map() + } + + get(sessionId: UserSessionId): FlashMessages { + const flashMessages = this.#flashMessagesMap.get(sessionId.toString()) ?? new FlashMessages() + this.#flashMessagesMap.set(sessionId.toString(), flashMessages) + + return flashMessages + } +} diff --git a/src/lib/server/memory/FlashOldsManager.ts b/src/lib/server/memory/FlashOldsManager.ts new file mode 100644 index 0000000..d82a0a8 --- /dev/null +++ b/src/lib/server/memory/FlashOldsManager.ts @@ -0,0 +1,17 @@ +import type { UserSessionId } from '$customType' +import { FlashOlds } from '../classes' + +export default class FlashOldsManager { + readonly #flashOldsMap: Map + + constructor() { + this.#flashOldsMap = new Map() + } + + get(sessionId: UserSessionId): FlashOlds { + const flashErrors = this.#flashOldsMap.get(sessionId.toString()) ?? new FlashOlds() + this.#flashOldsMap.set(sessionId.toString(), flashErrors) + + return flashErrors + } +} diff --git a/src/lib/server/memory/SessionManager.ts b/src/lib/server/memory/SessionManager.ts new file mode 100644 index 0000000..868ecb1 --- /dev/null +++ b/src/lib/server/memory/SessionManager.ts @@ -0,0 +1,28 @@ +// Import types +import type { UserSessionId } from '$customType' +import type { JwtToken, User } from '$lib/models' +import { UserSession } from '../classes' + +export default class SessionManager { + readonly #sessions: Map + + constructor() { + this.#sessions = new Map() + } + + add(sessionId: UserSessionId, token: JwtToken, user: User): void { + this.#sessions.set(sessionId.toString(), new UserSession(token, user)) + } + + get(sessionId: UserSessionId): UserSession | undefined { + return this.#sessions.get(sessionId.toString()) + } + + update(sessionId: UserSessionId, session: UserSession): void { + this.#sessions.set(sessionId.toString(), session) + } + + remove(sessionId: UserSessionId): void { + this.#sessions.delete(sessionId.toString()) + } +} diff --git a/src/lib/server/memory/index.ts b/src/lib/server/memory/index.ts new file mode 100644 index 0000000..e58e65d --- /dev/null +++ b/src/lib/server/memory/index.ts @@ -0,0 +1,11 @@ +import FlashErrorsManager from './FlashErrorsManager' +import FlashMessagesManager from './FlashMessagesManager' +import FlashOldsManager from './FlashOldsManager' +import SessionManager from './SessionManager' + +export { + FlashErrorsManager, + FlashMessagesManager, + FlashOldsManager, + SessionManager, +} diff --git a/src/lib/server/schemas/auth/index.ts b/src/lib/server/schemas/auth/index.ts new file mode 100644 index 0000000..556de1e --- /dev/null +++ b/src/lib/server/schemas/auth/index.ts @@ -0,0 +1,7 @@ +import { jwtSchema } from './jwt.schema' +import { loginSchema } from './login.schema' + +export { + jwtSchema, + loginSchema, +} diff --git a/src/lib/server/schemas/auth/jwt.schema.ts b/src/lib/server/schemas/auth/jwt.schema.ts new file mode 100644 index 0000000..4210ec8 --- /dev/null +++ b/src/lib/server/schemas/auth/jwt.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +export const jwtSchema = z.object({ + /** Endpoint Issuer */ + iss: z.string().url(), + /** User api token {UUID} */ + sub: z.string().uuid(), + /** Unix timeout timestamp */ + exp: z.number().int().min(0), + /** Unix issued at timestamp */ + iat: z.number().int().min(0), + /** Token unique identifier (UUID, not used) */ + jti: z.string().uuid(), +}) diff --git a/src/lib/server/schemas/auth/login.schema.ts b/src/lib/server/schemas/auth/login.schema.ts new file mode 100644 index 0000000..69728e9 --- /dev/null +++ b/src/lib/server/schemas/auth/login.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const loginSchema = z.object({ + token: z.string(), +}) diff --git a/src/lib/server/schemas/authorizationError.schema.ts b/src/lib/server/schemas/authorizationError.schema.ts new file mode 100644 index 0000000..d59d938 --- /dev/null +++ b/src/lib/server/schemas/authorizationError.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const authorizationErrorSchema = z.object({ + reason: z.string(), +}) diff --git a/src/lib/server/schemas/brand.schema.ts b/src/lib/server/schemas/brand.schema.ts new file mode 100644 index 0000000..97132c2 --- /dev/null +++ b/src/lib/server/schemas/brand.schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const brandSchema = z.object({ + uuid: z.string().min(1).uuid(), + slug: z.string().min(1).max(255), + name: z.string().min(1).max(255), + fqdns: z.array(z.string().min(1).max(255)).min(0), +}) diff --git a/src/lib/server/schemas/catalog.schema.ts b/src/lib/server/schemas/catalog.schema.ts new file mode 100644 index 0000000..d4cd9cc --- /dev/null +++ b/src/lib/server/schemas/catalog.schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const catalogSchema = z.object({ + uuid: z.string().min(1).uuid(), + slug: z.string().min(1).max(255), + name: z.string().min(1).max(255), + brand: z.string().min(1).uuid(), +}) diff --git a/src/lib/server/schemas/fileImport.schema.ts b/src/lib/server/schemas/fileImport.schema.ts new file mode 100644 index 0000000..569f235 --- /dev/null +++ b/src/lib/server/schemas/fileImport.schema.ts @@ -0,0 +1,11 @@ +import { BODY_SIZE_LIMIT } from '$env/static/private' + +import { Str, zod } from '$lib/tools' + +const MIN_FILE_SIZE = 256 +const MAX_FILE_SIZE = Math.max(Number.parseInt(BODY_SIZE_LIMIT) * 1024, MIN_FILE_SIZE) - MIN_FILE_SIZE + +export const fileImportSchema + = zod.any() + .refine((file: File) => file?.size !== 0, 'Le fichier est requis') + .refine(file => file?.size < MAX_FILE_SIZE, `La taille du fichier ne peut exceder ${Str.humanFileSize(MAX_FILE_SIZE)}`) diff --git a/src/lib/server/schemas/globalStatistics.schema.ts b/src/lib/server/schemas/globalStatistics.schema.ts new file mode 100644 index 0000000..c3c6daa --- /dev/null +++ b/src/lib/server/schemas/globalStatistics.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +import { GlobalStatistic } from '$lib/enums' + +export const globalStatisticsSchema = z.array( + z.object({ + key: z.nativeEnum(GlobalStatistic) + .transform(v => Object.keys(GlobalStatistic)[Object.values(GlobalStatistic).indexOf(v)]), + statistic: z.string().min(1), + value: z.number().min(0), + }), +) diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts new file mode 100644 index 0000000..d0d5a19 --- /dev/null +++ b/src/lib/server/schemas/index.ts @@ -0,0 +1,33 @@ +import { authorizationErrorSchema } from './authorizationError.schema' +import { brandSchema } from './brand.schema' +import { catalogSchema } from './catalog.schema' +import { fileImportSchema } from './fileImport.schema' +import { globalStatisticsSchema } from './globalStatistics.schema' +import { linkSchema } from './link.schema' +import { linkImportFailuresSchema } from './linkImportFailures.schema' +import { linkStatisticsSchema } from './linkStatistics.schema' +import { linkValiditySchema } from './linkValidity.schema' +import { listOfEntitySchema } from './listOfEntity.schema' +import { messageSchema } from './message.schema' +import { reportingSubscriptionsSchema } from './reportingSubscriptions.schema' +import { invalidLinksGroupedBySchema } from './statistics/invalidLinksGroupedBy.schema' +import { topReferrersSchema } from './statistics/topReferrers.schema' +import { topSources } from './statistics/topSources.schema' +import { uniqueVisitorsGroupedBySchema } from './statistics/uniqueVisitorsGroupedBy.schema' +import { tagSchema } from './tag.schema' +import { userSchema } from './user.schema' +import { validationErrors } from './validationErrors.schema' + +export { + authorizationErrorSchema, + brandSchema, + catalogSchema, + fileImportSchema, + globalStatisticsSchema, invalidLinksGroupedBySchema, linkImportFailuresSchema, linkSchema, linkStatisticsSchema, + linkValiditySchema, + listOfEntitySchema, + messageSchema, reportingSubscriptionsSchema, + tagSchema, topReferrersSchema as topReferrers, topSources, uniqueVisitorsGroupedBySchema, userSchema, + validationErrors +} + diff --git a/src/lib/server/schemas/link.schema.ts b/src/lib/server/schemas/link.schema.ts new file mode 100644 index 0000000..8fdf8af --- /dev/null +++ b/src/lib/server/schemas/link.schema.ts @@ -0,0 +1,30 @@ +import { z } from 'zod' + +import { hexColorSchema } from './utils' + +const LinkValidity = { + unknown: 0, + valid: 1, + invalid: 2, +} + +export const linkSchema = z.object({ + sourceAbsolute: z.coerce.string().url(), + source: z.string().min(1).max(255), + target: z.string().url(), + isExternal: z.boolean(), + validity: z.nativeEnum(LinkValidity), + visits: z.number().min(0), + brand: z.string().min(1).uuid().nullable(), + catalog: z.object({ + slug: z.string().min(1), + name: z.string().min(1), + }).nullable(), + tag: z.object({ + uuid: z.string().uuid(), + name: z.string().min(1), + color: hexColorSchema, + }).nullable(), + createdAt: z.string().datetime({ offset: true }), + updatedAt: z.string().datetime({ offset: true }).nullable(), +}) diff --git a/src/lib/server/schemas/linkImportFailures.schema.ts b/src/lib/server/schemas/linkImportFailures.schema.ts new file mode 100644 index 0000000..92989a8 --- /dev/null +++ b/src/lib/server/schemas/linkImportFailures.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod' + +export const linkImportFailuresSchema = z.object({ + failedImports: z.array(z.string()), + importedLinks: z.number(), +}) diff --git a/src/lib/server/schemas/linkStatistics.schema.ts b/src/lib/server/schemas/linkStatistics.schema.ts new file mode 100644 index 0000000..7657171 --- /dev/null +++ b/src/lib/server/schemas/linkStatistics.schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +export const linkStatisticsSchema = z.record( + z.string().min(1).uuid(), + z.object({ + uuid: z.string().min(1).uuid(), + name: z.string().min(1), + totalVisits: z.number(), + }), +) diff --git a/src/lib/server/schemas/linkValidity.schema.ts b/src/lib/server/schemas/linkValidity.schema.ts new file mode 100644 index 0000000..2830369 --- /dev/null +++ b/src/lib/server/schemas/linkValidity.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod' + +export const linkValiditySchema = z.object({ + target: z.string().url(), + isValid: z.boolean(), +}) diff --git a/src/lib/server/schemas/listOfEntity.schema.ts b/src/lib/server/schemas/listOfEntity.schema.ts new file mode 100644 index 0000000..df93f53 --- /dev/null +++ b/src/lib/server/schemas/listOfEntity.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +import type { ZodSchema } from '$customType' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function listOfEntitySchema(schema: T) { + return z.object({ + list: z.array(schema), + page: z.number().min(1), + perPage: z.number().min(1).max(50), + totalPages: z.number().min(1), + totalItems: z.number().min(0), + }) +} diff --git a/src/lib/server/schemas/message.schema.ts b/src/lib/server/schemas/message.schema.ts new file mode 100644 index 0000000..ee49e73 --- /dev/null +++ b/src/lib/server/schemas/message.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const messageSchema = z.object({ + message: z.string(), +}) diff --git a/src/lib/server/schemas/reportingSubscriptions.schema.ts b/src/lib/server/schemas/reportingSubscriptions.schema.ts new file mode 100644 index 0000000..e15ce9f --- /dev/null +++ b/src/lib/server/schemas/reportingSubscriptions.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod' + +export const reportingSubscriptionsSchema = z.array(z.object({ + user: z.string().min(1).uuid(), + brand: z.string().min(1).uuid(), +})) diff --git a/src/lib/server/schemas/statistics/invalidLinksGroupedBy.schema.ts b/src/lib/server/schemas/statistics/invalidLinksGroupedBy.schema.ts new file mode 100644 index 0000000..3306c49 --- /dev/null +++ b/src/lib/server/schemas/statistics/invalidLinksGroupedBy.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const invalidLinksGroupedBySchema = + z.record( + z.string(), + z.array( + z.object({ + weekOfYear: z.date({ coerce: true }), + count: z.number().min(1), + }) + ) + ) diff --git a/src/lib/server/schemas/statistics/topReferrers.schema.ts b/src/lib/server/schemas/statistics/topReferrers.schema.ts new file mode 100644 index 0000000..9116478 --- /dev/null +++ b/src/lib/server/schemas/statistics/topReferrers.schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const topReferrersSchema = + z.array( + z.object({ + referrer: z.string().nonempty(), + brand: z.string().nonempty(), + count: z.number().min(1) + }) + ) diff --git a/src/lib/server/schemas/statistics/topSources.schema.ts b/src/lib/server/schemas/statistics/topSources.schema.ts new file mode 100644 index 0000000..58fe1b5 --- /dev/null +++ b/src/lib/server/schemas/statistics/topSources.schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const topSources = + z.array( + z.object({ + source: z.string().nonempty(), + brand: z.string().nonempty(), + count: z.number().min(1) + }) + ) diff --git a/src/lib/server/schemas/statistics/uniqueVisitorsGroupedBy.schema.ts b/src/lib/server/schemas/statistics/uniqueVisitorsGroupedBy.schema.ts new file mode 100644 index 0000000..17aaf9b --- /dev/null +++ b/src/lib/server/schemas/statistics/uniqueVisitorsGroupedBy.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const uniqueVisitorsGroupedBySchema = + z.record( + z.string(), + z.array( + z.object({ + dayOfYear: z.date({ coerce: true }), + count: z.number().min(1), + }) + ) + ) diff --git a/src/lib/server/schemas/tag.schema.ts b/src/lib/server/schemas/tag.schema.ts new file mode 100644 index 0000000..5c89818 --- /dev/null +++ b/src/lib/server/schemas/tag.schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' + +import { hexColorSchema } from './utils' + +export const tagSchema = z.object({ + uuid: z.string().min(1).uuid(), + name: z.string().min(1).max(255), + color: hexColorSchema, +}) diff --git a/src/lib/server/schemas/user.schema.ts b/src/lib/server/schemas/user.schema.ts new file mode 100644 index 0000000..dfe4fbe --- /dev/null +++ b/src/lib/server/schemas/user.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +import { Role } from '$lib/enums' + +export const userSchema = z.object({ + uuid: z.string().min(1).uuid(), + firstName: z.string().min(1).max(255), + lastName: z.string().min(1).max(255), + email: z.string().min(1).max(255).email(), + role: z.nativeEnum(Role), + brands: z.optional(z.array(z.string().min(1).uuid())), +}) diff --git a/src/lib/server/schemas/utils/hex-color.schema.ts b/src/lib/server/schemas/utils/hex-color.schema.ts new file mode 100644 index 0000000..a802bd8 --- /dev/null +++ b/src/lib/server/schemas/utils/hex-color.schema.ts @@ -0,0 +1,13 @@ +import color from 'color-string' +import { z } from 'zod' + +function colorValidator(val: string): boolean { + try { + return color.get(val) != null + } + catch { + return false + } +} + +export default z.string().refine(colorValidator) diff --git a/src/lib/server/schemas/utils/index.ts b/src/lib/server/schemas/utils/index.ts new file mode 100644 index 0000000..a07be79 --- /dev/null +++ b/src/lib/server/schemas/utils/index.ts @@ -0,0 +1,5 @@ +import hexColorSchema from './hex-color.schema' + +export { + hexColorSchema, +} diff --git a/src/lib/server/schemas/validationErrors.schema.ts b/src/lib/server/schemas/validationErrors.schema.ts new file mode 100644 index 0000000..df02222 --- /dev/null +++ b/src/lib/server/schemas/validationErrors.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const validationErrors = z.object({ + errors: z.record(z.string(), z.string()), +}) diff --git a/src/lib/server/services/AuthService.ts b/src/lib/server/services/AuthService.ts new file mode 100644 index 0000000..120e92f --- /dev/null +++ b/src/lib/server/services/AuthService.ts @@ -0,0 +1,40 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { UserSessionId } from '$customType' +import type { Message } from '$lib/models' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService, Z3ClientService } from './Z3ClientService' + +export class AuthService { + readonly #client: Z3ClientService + readonly #sessionService: SessionService + + constructor(client: Z3ClientService, sessionService: SessionService) { + this.#client = client + this.#sessionService = sessionService + } + + async authenticate(cookies: Cookies, email: string, password: string): Promise { + const jwtToken = await this.#client.authenticate(email, password) + + if (jwtToken === false) return false + + const user = await this.#client.authenticatedUser(jwtToken) + + return this.#sessionService.createSession(cookies, jwtToken, user) + } + + changePassword(cookies: Cookies, password: string, newPassword: string): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#client.changePassword(userToken, password, newPassword) + } + + disconnect(sessionId: UserSessionId): void { + this.#sessionService.removeSession(sessionId) + } +} + +export const authService = new AuthService( + z3ClientService, + sessionService, +) diff --git a/src/lib/server/services/BrandService.ts b/src/lib/server/services/BrandService.ts new file mode 100644 index 0000000..c6b473b --- /dev/null +++ b/src/lib/server/services/BrandService.ts @@ -0,0 +1,71 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { Slug } from '$customType/value-objects/Slug.type' +import type Brand from '$lib/models/Brand.type' +import type ListOf from '$lib/models/ListOf.type' +import type Message from '$lib/models/Message.type' +import { BrandRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class BrandService { + readonly #brandRepository: BrandRepository + readonly #sessionService: SessionService + + constructor(brandRepository: BrandRepository, sessionService: SessionService) { + this.#brandRepository = brandRepository + this.#sessionService = sessionService + } + + async getAllBrands(cookies: Cookies): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const perPage = 50 + let brands: Brand[] = [] + let page = 1 + let paginatedBrands: ListOf + do { + paginatedBrands = await this.#brandRepository.listBrands(userToken, page, perPage) + brands = [...brands, ...paginatedBrands.list] + page++ + } while (paginatedBrands.list.length === perPage) + + return brands + } + + listBrands(cookies: Cookies, page: number, perPage: number): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#brandRepository.listBrands(userToken, page, perPage) + } + + getBrand(cookies: Cookies, slug: Slug): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#brandRepository.getBrand(userToken, slug) + } + + deleteBrand(cookies: Cookies, slug: Slug): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#brandRepository.deleteBrand(userToken, slug) + } + + createBrand( + cookies: Cookies, + name: string, + fqdns: string[], + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#brandRepository.createBrand(userToken, { + name, + fqdns, + }) + } + + updateBrand(cookies: Cookies, brand: Brand): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#brandRepository.updateBrand(userToken, brand) + } +} + +export const brandService = new BrandService( + new BrandRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/CatalogService.ts b/src/lib/server/services/CatalogService.ts new file mode 100644 index 0000000..9264e28 --- /dev/null +++ b/src/lib/server/services/CatalogService.ts @@ -0,0 +1,62 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { Slug, Uuid } from '$customType/value-objects' +import type { Catalog, ListOf, Message } from '$lib/models' +import { CatalogRepository } from '../clients/Z3/repositories' +import { sessionService, type SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class CatalogService { + readonly #catalogRepository: CatalogRepository + readonly #sessionService: SessionService + + constructor(catalogRepository: CatalogRepository, sessionService: SessionService) { + this.#catalogRepository = catalogRepository + this.#sessionService = sessionService + } + + listCatalogs( + cookies: Cookies, + page: number, + perPage: number, + brand: Uuid | undefined = undefined, + ): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#catalogRepository.listCatalogs(userToken, page, perPage, brand) + } + + getCatalog(cookies: Cookies, slug: Slug): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#catalogRepository.getCatalog(userToken, slug) + } + + deleteCatalog(cookies: Cookies, slug: Slug): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#catalogRepository.deleteCatalog(userToken, slug) + } + + createCatalog( + cookies: Cookies, + brand: Uuid, + name: string, + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#catalogRepository.createCatalog(userToken, { + brand, + name, + }) + } + + updateCatalog(cookies: Cookies, catalog: Catalog): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#catalogRepository.updateCatalog(userToken, catalog) + } +} + +const httpClient = z3ClientService.getHttpClient() +const catalogRepository = new CatalogRepository(httpClient) + +export const catalogService = new CatalogService( + catalogRepository, + sessionService, +) diff --git a/src/lib/server/services/JwtDecodeService.ts b/src/lib/server/services/JwtDecodeService.ts new file mode 100644 index 0000000..52cb396 --- /dev/null +++ b/src/lib/server/services/JwtDecodeService.ts @@ -0,0 +1,30 @@ +// Import types +import type JwtToken from '$lib/models/JwtToken.type' + +// Import modules +import { jwtDecode } from 'jwt-decode' + +export class JwtDecodeService { + /** + * Decode Z3 JWT token + * @throws Error If missing 'iss' or 'exp' or 'iat' or 'jti' or 'sub' fields + */ + decodePayload(token: string): JwtToken { + const decoded = jwtDecode(token) + if (!decoded.iss) throw new Error('Missing eissxp on token') + if (!decoded.exp) throw new Error('Missing exp on token') + if (!decoded.iat) throw new Error('Missing iat on token') + if (!decoded.jti) throw new Error('Missing jti on token') + if (!decoded.sub) throw new Error('Missing sub on token') + return { + originalToken: token, + iss: new URL(decoded.iss), + exp: decoded.exp, + iat: decoded.iat, + jti: decoded.jti, + sub: decoded.sub, + } + } +} + +export const jwtDecodeService = new JwtDecodeService() diff --git a/src/lib/server/services/LinkService.ts b/src/lib/server/services/LinkService.ts new file mode 100644 index 0000000..5ed3460 --- /dev/null +++ b/src/lib/server/services/LinkService.ts @@ -0,0 +1,196 @@ +import type { Cookies } from '@sveltejs/kit' +import { get } from 'svelte/store' + +import type { ImportResponse, PinoLogger } from '$customType' +import type { Source, Uuid } from '$customType/value-objects' +import { Direction } from '$lib/enums' +import type { Brand, Catalog, Link, LinkValidity, ListOf, Message } from '$lib/models' +import { LinkRepository } from '../clients/Z3/repositories' +import { pinoLoggerStore } from '../stores' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class LinkService { + readonly #linkRepository: LinkRepository + readonly #sessionService: SessionService + readonly #logger: PinoLogger + + constructor( + linkRepository: LinkRepository, + sessionService: SessionService, + logger: PinoLogger, + ) { + this.#linkRepository = linkRepository + this.#sessionService = sessionService + this.#logger = logger + } + + listLinks( + cookies: Cookies, + page: number, + perPage: number, + filters: { brand: string | undefined, tag: string | undefined, catalog: string | undefined }, + sortBy: string | null, + direction: Direction, + searchOn: string, + keywords: string | null, + ): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + // * Omit auto when calling to server + const repoSorter = (sortBy !== null && direction !== null && direction !== Direction.auto) + ? { sortBy, direction } + : null + const repoSearcher = (keywords && keywords.length) + ? { + searchOn, + keywords, + } + : null + + return this.#linkRepository.listLinks(userToken, page, perPage, filters, repoSorter, repoSearcher) + } + + getLink(cookies: Cookies, source: Source): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.getLink(userToken, source) + } + + deleteLink(cookies: Cookies, source: Source): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.deleteLink(userToken, source) + } + + createLink( + cookies: Cookies, + source: string | null, + target: string, + isExternal: boolean, + brand: string | null, + catalog: string | null, + tag: string | null, + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.createLink(userToken, { + source, + target, + is_external: isExternal, + brand: brand ?? undefined, + catalog: catalog ?? undefined, + tag: tag ?? undefined, + }) + } + + updateLink(cookies: Cookies, { source, target, isExternal, catalog, tag }: { + source: string + target: string + isExternal: boolean + catalog: string | null + tag: string | null + }): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.updateLink(userToken, { + ...{ source, target, is_external: isExternal }, + ...(catalog ? { catalog } : {}), + ...(tag ? { tag } : {}), + }) + } + + verifyLink(cookies: Cookies, source: Source): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.verifyLink(userToken, source) + } + + async exportLinks(cookies: Cookies): Promise>> { + const logger = this.#logger + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const exportLinks = await this.#linkRepository.exportLinks(userToken) + const linksReader = exportLinks.getReader() + + return new ReadableStream({ + async start(controller): Promise { + let done = false, value: Uint8Array | undefined = undefined + while (!done) { + ({ value, done } = await linksReader.read()) + if (done) { + logger.debug('Export is complete') + linksReader.releaseLock() + return controller.close() + } + controller.enqueue(value) + } + }, + cancel(): void { + logger.debug('Cancelling links file download') + linksReader.cancel() + }, + }) + } + + async exportBrandLinks(cookies: Cookies, brand: Brand): Promise>> { + const logger = this.#logger + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const exportLinks = await this.#linkRepository.exportBrandLinks(userToken, brand.slug) + const linksReader = exportLinks.getReader() + + return new ReadableStream({ + async start(controller): Promise { + let done = false, value: Uint8Array | undefined = undefined + while (!done) { + ({ value, done } = await linksReader.read()) + if (done) { + logger.debug('Export is complete') + linksReader.releaseLock() + return controller.close() + } + controller.enqueue(value) + } + }, + cancel(): void { + logger.debug('Cancelling links file download') + linksReader.cancel() + }, + }) + } + + async exportCatalogLinks(cookies: Cookies, catalog: Catalog): Promise>> { + const logger = this.#logger + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const exportLinks = await this.#linkRepository.exportCatalogLinks(userToken, catalog.slug) + const linksReader = exportLinks.getReader() + + return new ReadableStream({ + async start(controller): Promise { + let done = false, value: Uint8Array | undefined = undefined + while (!done) { + ({ value, done } = await linksReader.read()) + if (done) { + logger.debug('Export is complete') + linksReader.releaseLock() + return controller.close() + } + controller.enqueue(value) + } + }, + cancel(): void { + logger.debug('Cancelling links file download') + linksReader.cancel() + }, + }) + } + + importBrandLinks(cookies: Cookies, brand: Uuid, tag: Uuid | undefined, data: File): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.importBrandLinks(userToken, brand, tag, data) + } + + importCatalogLinks(cookies: Cookies, catalog: Uuid, tag: Uuid | undefined, data: File): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkRepository.importCatalogLinks(userToken, catalog, tag, data) + } +} + +export const linkService = new LinkService( + new LinkRepository(z3ClientService.getHttpClient()), + sessionService, + get(pinoLoggerStore), +) diff --git a/src/lib/server/services/LinkStatisticsService.ts b/src/lib/server/services/LinkStatisticsService.ts new file mode 100644 index 0000000..3162134 --- /dev/null +++ b/src/lib/server/services/LinkStatisticsService.ts @@ -0,0 +1,62 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects/Uuid.type' +import type { StatisticLine } from '$lib/models' +import { LinkStatisticsRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export interface TypeObjectStats { + uuid: Uuid + name: string + count: number +} + +export class LinkStatisticsService { + readonly #linkStatisticsRepository: LinkStatisticsRepository + readonly #sessionService: SessionService + + constructor( + linkStatisticsRepository: LinkStatisticsRepository, + sessionService: SessionService, + ) { + this.#linkStatisticsRepository = linkStatisticsRepository + this.#sessionService = sessionService + } + + async globalStatistics(cookies: Cookies): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const output: Record = {}, + statistics = await this.#linkStatisticsRepository.globalStatistics(userToken) + statistics.forEach(statisticLine => output[statisticLine.key] = statisticLine.value) + return output + } + + async globalStatisticsForBrand(cookies: Cookies, brand: Uuid): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + const output: Record = {}, + statistics = await this.#linkStatisticsRepository.globalStatisticsForBrand(userToken, brand) + statistics.forEach(statisticLine => output[statisticLine.key] = statisticLine.value) + return output + } + + linkStatisticsForBrands(cookies: Cookies): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkStatisticsRepository.linkStatisticsForBrands(userToken) + } + + linkCatalogStatisticsFor(cookies: Cookies, catalog: Uuid): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkStatisticsRepository.linkCatalogStatisticsFor(userToken, catalog) + } + + linkTagStatisticsFor(cookies: Cookies, tag: Uuid): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#linkStatisticsRepository.linkTagStatisticsFor(userToken, tag) + } +} + +export const linkStatisticsService = new LinkStatisticsService( + new LinkStatisticsRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/ReportingSubscriptionService.ts b/src/lib/server/services/ReportingSubscriptionService.ts new file mode 100644 index 0000000..b17cf3c --- /dev/null +++ b/src/lib/server/services/ReportingSubscriptionService.ts @@ -0,0 +1,42 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { Message, ReportingSubscription } from '$lib/models' +import { ReportingSubscriptionRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class ReportingSubscriptionService { + readonly #reportingSubscriptionRepository: ReportingSubscriptionRepository + readonly #sessionService: SessionService + + constructor( + reportingSubscriptionRepository: ReportingSubscriptionRepository, + sessionService: SessionService, + ) { + this.#reportingSubscriptionRepository = reportingSubscriptionRepository + this.#sessionService = sessionService + } + + getReportingSubscriptions( + cookies: Cookies, + user: Uuid, + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#reportingSubscriptionRepository.getReportingSubscriptionsFor(userToken, user) + } + + updateUserReportingSubscriptions( + cookies: Cookies, + user: Uuid, + brands: Uuid[], + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#reportingSubscriptionRepository.updateUserReportingSubscriptions(userToken, user, brands) + } +} + +export const reportingSubscriptionService = new ReportingSubscriptionService( + new ReportingSubscriptionRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/SessionService.ts b/src/lib/server/services/SessionService.ts new file mode 100644 index 0000000..2ae886c --- /dev/null +++ b/src/lib/server/services/SessionService.ts @@ -0,0 +1,117 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { UserSessionId } from '$customType' +import type { JwtToken, User } from '$lib/models' +import { uniquId } from '$lib/tools' +import type { FlashErrors, FlashMessages, FlashOlds, UserSession } from '../classes' +import { ServerError } from '../errors' +import { + FlashErrorsManager, FlashMessagesManager, + FlashOldsManager, SessionManager, +} from '../memory' + +export interface SessionData { + sessionId: UserSessionId + session: UserSession +} + +export class SessionService { + readonly #sessionManager: SessionManager + readonly #flashMessagesManager: FlashMessagesManager + readonly #flashErrorsManager: FlashErrorsManager + readonly #flashOldsManager: FlashOldsManager + + constructor( + sessions: SessionManager, + flashMessageManager: FlashMessagesManager, + flashErrorsManager: FlashErrorsManager, + flashOldsManager: FlashOldsManager, + ) { + this.#sessionManager = sessions + this.#flashMessagesManager = flashMessageManager + this.#flashErrorsManager = flashErrorsManager + this.#flashOldsManager = flashOldsManager + } + + createSession(cookies: Cookies, jwtToken: JwtToken, user: User): UserSessionId { + this.#sessionManager.add(jwtToken.jti, jwtToken, user) + + cookies.set('sessionId', jwtToken.jti.toString(), { path: '/' }) + + return jwtToken.jti + } + + getSessionData(cookies: Cookies): SessionData | undefined { + const sessionId: string | undefined = cookies.get('sessionId') + + if (sessionId === undefined) return undefined + + const session = this.#sessionManager.get(sessionId) + const currentTime = Math.round(new Date().getTime() / 1000) + + if (session === undefined) return session + + if (session.getToken().exp < currentTime) { + this.#sessionManager.remove(sessionId) + return + } + + return { sessionId, session } + } + + getSessionUser(cookies: Cookies): UserSession | undefined { + return this.getSessionData(cookies)?.session + } + + updateSession(cookies: Cookies, session: UserSession): void { + const sessionId = this.getSessionId(cookies) + const actualSession = this.getSessionData(cookies) ?? { sessionId, session } + + this.#sessionManager.update(actualSession.sessionId, session) + } + + removeSession(sessionId: UserSessionId): void { + this.#sessionManager.remove(sessionId) + } + + getFlash(cookies: Cookies): FlashMessages { + const flashSessionId = this.getFlashSessionId(cookies) + return this.#flashMessagesManager.get(flashSessionId) + } + + getFlashOlds(cookies: Cookies): FlashOlds { + const flashSessionId = this.getFlashSessionId(cookies) + return this.#flashOldsManager.get(flashSessionId) + } + + getFlashErrors(cookies: Cookies): FlashErrors { + const flashSessionId = this.getFlashSessionId(cookies) + return this.#flashErrorsManager.get(flashSessionId) + } + + private getFlashSessionId(cookies: Cookies): string { + let flashSessionId = cookies.get('flashSessionId') + if (!flashSessionId) { + flashSessionId = String(uniquId()) + cookies.set('flashSessionId', flashSessionId, { path: '/' }) + } + return flashSessionId + } + + public getSessionId(cookies: Cookies): UserSessionId { + const sessionId: string | undefined = cookies.get('sessionId') + + if (sessionId === undefined) { + throw new ServerError('No cookie for session Id') + } + + return sessionId + } +} + +export const sessionService = new SessionService( + new SessionManager(), + new FlashMessagesManager(), + new FlashErrorsManager(), + new FlashOldsManager(), +) diff --git a/src/lib/server/services/StatisticsService.ts b/src/lib/server/services/StatisticsService.ts new file mode 100644 index 0000000..229c365 --- /dev/null +++ b/src/lib/server/services/StatisticsService.ts @@ -0,0 +1,48 @@ +import type { Cookies } from '@sveltejs/kit' + +import { StatisticsRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class StatisticsService { + readonly #statisticsRepository: StatisticsRepository + readonly #sessionService: SessionService + + constructor( + statisticsRepository: StatisticsRepository, + sessionService: SessionService, + ) { + this.#statisticsRepository = statisticsRepository + this.#sessionService = sessionService + } + + async getUniqueVisitorsByDayAndByCatalog(cookies: Cookies): Promise>> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#statisticsRepository.uniqueVisitorsByCatalogs(userToken) + } + + async getInvalidLinksByWeekAndByCatalog(cookies: Cookies): Promise>> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#statisticsRepository.invalidLinksPerWeekPerBrand(userToken) + } + + async getUniqueVisitorsByDayAndByBrand(cookies: Cookies): Promise>> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#statisticsRepository.uniqueVisitorsByBrands(userToken) + } + + async topMostVisitedSources(cookies: Cookies): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#statisticsRepository.mostVisitedSources(userToken) + } + + async topMostFrequentReferrers(cookies: Cookies): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#statisticsRepository.mostFrequentReferrers(userToken) + } +} + +export const statisticsService = new StatisticsService( + new StatisticsRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/TagService.ts b/src/lib/server/services/TagService.ts new file mode 100644 index 0000000..746ce3b --- /dev/null +++ b/src/lib/server/services/TagService.ts @@ -0,0 +1,54 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { HexColor, Uuid } from '$customType/value-objects' +import type { ListOf, Message, Tag } from '$lib/models' +import { TagRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class TagService { + readonly #tagRepository: TagRepository + readonly #sessionService: SessionService + + constructor(tagRepository: TagRepository, sessionService: SessionService) { + this.#tagRepository = tagRepository + this.#sessionService = sessionService + } + + listTags(cookies: Cookies, page: number, perPage: number): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#tagRepository.listTags(userToken, page, perPage) + } + + getTag(cookies: Cookies, uuid: Uuid): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#tagRepository.getTag(userToken, uuid) + } + + deleteTag(cookies: Cookies, uuid: Uuid): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#tagRepository.deleteTag(userToken, uuid) + } + + createTag( + cookies: Cookies, + name: string, + color: HexColor, + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#tagRepository.createTag(userToken, { + name, + color, + }) + } + + updateTag(cookies: Cookies, tag: Tag): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#tagRepository.updateTag(userToken, tag) + } +} + +export const tagService = new TagService( + new TagRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/UserService.ts b/src/lib/server/services/UserService.ts new file mode 100644 index 0000000..19c321a --- /dev/null +++ b/src/lib/server/services/UserService.ts @@ -0,0 +1,71 @@ +import type { Cookies } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { Role } from '$lib/enums' +import type { ListOf, Message, User } from '$lib/models' +import { UserRepository } from '../clients/Z3/repositories' +import { sessionService, SessionService } from './SessionService' +import { z3ClientService } from './Z3ClientService' + +export class UserService { + readonly #userRepository: UserRepository + readonly #sessionService: SessionService + + constructor(userRepository: UserRepository, sessionService: SessionService) { + this.#userRepository = userRepository + this.#sessionService = sessionService + } + + listUsers(cookies: Cookies, page: number, perPage: number): Promise> { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.listUsers(userToken, page, perPage) + } + + getUser(cookies: Cookies, uuid: Uuid): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.getUser(userToken, uuid) + } + + resetUserPassword(cookies: Cookies, uuid: Uuid): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.resetUserPassword(userToken, uuid) + } + + deleteUser(cookies: Cookies, uuid: Uuid): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.deleteUser(userToken, uuid) + } + + createUser( + cookies: Cookies, + firstName: string, + lastName: string, + email: string, + role: Role, + login: string, + brands: Uuid[], + ): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.createUser(userToken, { + firstName, + lastName, + email, + role, + login, + brands, + }) + } + + updateUser(cookies: Cookies, user: User): Promise { + const userToken = this.#sessionService.getSessionUser(cookies)?.getToken() + return this.#userRepository.updateUser(userToken, { + ...user, + ...{ brands: user.brands ?? [] }, + }) + } +} + +export const userService = new UserService( + new UserRepository(z3ClientService.getHttpClient()), + sessionService, +) diff --git a/src/lib/server/services/Z3ClientService.ts b/src/lib/server/services/Z3ClientService.ts new file mode 100644 index 0000000..338fb46 --- /dev/null +++ b/src/lib/server/services/Z3ClientService.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private' +import { HttpClient } from '../clients' +import { Z3Client } from '../clients/Z3' +import { jwtDecodeService } from './JwtDecodeService' + +const backendUrl = new URL(env.BACKEND_URL) +const httpClient = new HttpClient(backendUrl) + +export const z3ClientService = new Z3Client( + httpClient, + jwtDecodeService, +) + +export { Z3Client as Z3ClientService } diff --git a/src/lib/server/services/index.ts b/src/lib/server/services/index.ts new file mode 100644 index 0000000..6a2a1f9 --- /dev/null +++ b/src/lib/server/services/index.ts @@ -0,0 +1,57 @@ +import { authService, AuthService } from './AuthService' +import { brandService, BrandService } from './BrandService' +import { catalogService, CatalogService } from './CatalogService' +import { jwtDecodeService, JwtDecodeService } from './JwtDecodeService' +import { linkService, LinkService } from './LinkService' +import { linkStatisticsService, LinkStatisticsService } from './LinkStatisticsService' +import { reportingSubscriptionService, ReportingSubscriptionService } from './ReportingSubscriptionService' +import { SessionService, sessionService } from './SessionService' +import { statisticsService, StatisticsService } from './StatisticsService' +import { tagService, TagService } from './TagService' +import { userService, UserService } from './UserService' +import { z3ClientService, Z3ClientService } from './Z3ClientService' + +export { + authService, AuthService +} + +export { + brandService, BrandService, catalogService, CatalogService +} + +export { + jwtDecodeService, JwtDecodeService +} + +export { + linkService, LinkService +} + +export { + linkStatisticsService, LinkStatisticsService +} + +export { + reportingSubscriptionService, ReportingSubscriptionService +} + +export { + sessionService, SessionService +} + +export { + tagService, TagService +} + +export { + userService, UserService +} + +export { + statisticsService, StatisticsService +} + +export { + z3ClientService, Z3ClientService +} + diff --git a/src/lib/server/stores/index.ts b/src/lib/server/stores/index.ts new file mode 100644 index 0000000..46699a9 --- /dev/null +++ b/src/lib/server/stores/index.ts @@ -0,0 +1,5 @@ +import pinoLoggerStore from './pinoLoggerStore.svelte' + +export { + pinoLoggerStore, +} diff --git a/src/lib/server/stores/pinoLoggerStore.svelte.ts b/src/lib/server/stores/pinoLoggerStore.svelte.ts new file mode 100644 index 0000000..edacb31 --- /dev/null +++ b/src/lib/server/stores/pinoLoggerStore.svelte.ts @@ -0,0 +1,76 @@ +import pino, { type LoggerOptions } from 'pino' +import { readable } from 'svelte/store' + +import { browser, dev } from '$app/environment' +import type { PinoLogger } from '$customType' +import { LogLevel, ServerEnvironment } from '$customType/PinoLogger.type' + +const defaultLogLevel = LogLevel.INFO + +const pinoLogger: PinoLogger = ((): PinoLogger => { + let pinoOptions: LoggerOptions + + if (dev) { + pinoOptions = { + browser: { asObject: false }, + level: LogLevel.DEBUG, + // format the level in the log to be uppercase. + formatters: { + level: (label): { level: string } => { + return { level: label.toUpperCase() } + }, + }, + transport: { + target: 'pino-pretty', + options: { + colorize: true, // show colors in log + levelFirst: true, // show levels first in log + translateTime: true, // translate the time in human readable format + }, + }, + } + } + else { + pinoOptions = { + level: defaultLogLevel, + formatters: { + level: (label): { level: string } => { + return { level: label.toUpperCase() } + }, + }, + } + } + + return pino(pinoOptions) +})() + +pinoLogger.setLogLevel = (NODE_ENV: ServerEnvironment): string => { + let logLevel: LoggerOptions['level'] = defaultLogLevel + + switch (NODE_ENV) { + case ServerEnvironment.DEV: + case ServerEnvironment.PREV: + logLevel = LogLevel.TRACE + break + case ServerEnvironment.STG: + logLevel = LogLevel.INFO + break + case ServerEnvironment.PROD: + if (browser) { + logLevel = LogLevel.SILENT + } + else { + logLevel = LogLevel.INFO + } + break + } + + pinoLogger.info(`Logger log level will be set to "${logLevel}".`) + pinoLogger.level = logLevel + + return logLevel +} + +const pinoLoggerStore = readable(pinoLogger) + +export default pinoLoggerStore diff --git a/src/lib/tools/colors.tools.ts b/src/lib/tools/colors.tools.ts new file mode 100644 index 0000000..42eff0d --- /dev/null +++ b/src/lib/tools/colors.tools.ts @@ -0,0 +1,54 @@ +import type { HexColor } from '$customType/value-objects' +import colorParser from 'color-string' + +interface RGB { + r: number + g: number + b: number +} + +export abstract class Color { + /** Calculate the optimal text color */ + public static getTextColor(color: HexColor): HexColor { + if (colorParser.get(color) === null) { + throw new Error(`Invalid color '${color}'`) + } + + return this.calculateTextColor(color) + } + + private static calculateTextColor(color: HexColor): HexColor { + const rgbColor = this.toRgb(color) + const cieY = this.luminance(rgbColor) + + return (Math.pow(cieY, 0.678) < 0.5) ? '#ffffff' : '#000000' + } + + /** + Andy's Easy Standard Luminance for sRGB. + @url https://www.andysomers.com/ + @url https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color + */ + private static luminance(color: RGB): number { + return Math.pow(color.r / 255.0, 2.2) * 0.2126 + + Math.pow(color.g / 255.0, 2.2) * 0.7152 + + Math.pow(color.b / 255.0, 2.2) * 0.0722 + } + + private static toRgb(color: HexColor): RGB { + const longHexColor = this.expandHexColor(color) + const rgbColors = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(longHexColor) + return { + r: parseInt(rgbColors![1], 16), + g: parseInt(rgbColors![2], 16), + b: parseInt(rgbColors![3], 16), + } + } + + private static expandHexColor(color: HexColor): HexColor { + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i + return color.replace(shorthandRegex, (m, r, g, b) => { + return `${r}${r}${g}${g}${b}${b}` + }) + } +} diff --git a/src/lib/tools/dates.tools.ts b/src/lib/tools/dates.tools.ts new file mode 100644 index 0000000..d9e1f12 --- /dev/null +++ b/src/lib/tools/dates.tools.ts @@ -0,0 +1,32 @@ +import { locale } from './translations.tools' + +export abstract class Dates { + public static niceDate(value: number | string | Date): string { + return new Date(value).toLocaleDateString(locale.get()) + } + + public static niceDateTime(value: number | string | Date): string { + return new Date(value).toLocaleDateString(locale.get(), { + day: '2-digit', + month: '2-digit', + year: 'numeric', + weekday: 'short', + hour: '2-digit', + minute: '2-digit', + }) + } + + public static dateTime(value: number | string | Date): string { + return new Date(value).toLocaleDateString(locale.get(), { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + public static niceDateWithOptions(value: number | string | Date, options?: Intl.DateTimeFormatOptions): string { + return new Date(value).toLocaleDateString(locale.get(), options) + } +} diff --git a/src/lib/tools/debounce.tools.ts b/src/lib/tools/debounce.tools.ts new file mode 100644 index 0000000..16920bf --- /dev/null +++ b/src/lib/tools/debounce.tools.ts @@ -0,0 +1,13 @@ +export class Debounce { + #timeout: ReturnType|null = null + + debounce(callback: (...args: I[]) => O, wait = 300): ((...args: I[]) => void) { + + return (...args: I[]): void => { + if (this.#timeout !== null) { + clearTimeout(this.#timeout) + } + this.#timeout = setTimeout(() => callback(...args), wait) + } + } +} diff --git a/src/lib/tools/index.ts b/src/lib/tools/index.ts new file mode 100644 index 0000000..e6c3008 --- /dev/null +++ b/src/lib/tools/index.ts @@ -0,0 +1,38 @@ +import { Color } from './colors.tools' +import { Dates } from './dates.tools' +import { Debounce } from './debounce.tools' +import { Roles } from './roles.tools' +import { Str } from './strings.tools' +import { uniquId } from './uniquId.tools' +import { isValidUuid } from './uuidValidator.tools' +import { zod } from './zod.tools' + +import { + addTranslations, + defaultLocale, + loading, + loadTranslations, + locale, + locales, + setLocale, setRoute, + t, + translations, +} from './translations.tools' + +export { + Color, + Dates, + Debounce, + isValidUuid, + Roles, + Str, + uniquId, + zod +} + +export { + addTranslations, defaultLocale, loading, + loadTranslations, locale, locales, + setLocale, setRoute, t, translations +} + diff --git a/src/lib/tools/request.tools.ts b/src/lib/tools/request.tools.ts new file mode 100644 index 0000000..dab5635 --- /dev/null +++ b/src/lib/tools/request.tools.ts @@ -0,0 +1,89 @@ +import type { FormFieldsError } from '$customType/forms/FormFieldsError.type' +import type { ZodSchema } from '$customType/ZodSchema.type' +import type Entity from '$lib/models/Entity.type' +import RequestInputs from '$lib/server/classes/RequestInputs' +import { type ZodError } from 'zod' +import type { zod } from './zod.tools' + +export class Z3Request { + readonly #request: Request + readonly #schema: SchemaT + + constructor(request: Request, schema: SchemaT) { + this.#request = request + this.#schema = schema + } + + public async formInputs(): Promise { + const inputs = await this.#getInputs() + return new RequestInputs(inputs) + } + + public async validated(): Promise<{ + validated: zod.infer + errors: FormFieldsError + }> { + const inputs = await this.#getInputs() + return this.#validateRequest(inputs) + } + + async #getInputs(): Promise> { + const output: Record = {} + + const formData = (await this.#request.clone().formData()) + formData.forEach((fieldValue, fieldName) => { + const isAnArray = fieldName.endsWith('[]') + const value = (fieldValue instanceof File) ? null : fieldValue + const key = isAnArray ? fieldName.substring(0, fieldName.length - 2) : fieldName + + // ? The current value is an array + if (isAnArray) { + output[key] = output[key] ?? [] + output[key] = (value && value.length) ? [value, ...output[key]] : output[key] + } + else { + output[key] = (value && value.length) ? value : null + } + }) + + return output + } + + #validateRequest(requestData: Record): { + validated: zod.infer + errors: FormFieldsError + } { + const validated = this.#schema.safeParse(requestData) + return { + validated: validated?.data ?? {}, + errors: validated?.error ? this.#zErrorToFieldsError(validated?.error) : {}, + } + } + + #zErrorToFieldsError(zError: ZodError>): FormFieldsError { + const errors = Object.entries(zError!.flatten().fieldErrors) + const output: FormFieldsError = {} + + errors.forEach((entry) => { + const errorMessages = entry[1] instanceof Array ? entry[1].join(', ') : '' + if (errorMessages.length) { + output[entry[0]] = errorMessages + } + }) + + return output + } + + public static of(request: Request, zodFormSchema: SchemaT): Z3Request { + return new Z3Request(request, zodFormSchema) + } + + public static assignEntityProps( + model: M, + properties: Record, + ): void { + Object.keys(properties).forEach((key) => { + model[key as keyof typeof model] = properties[key] as never + }) + } +} diff --git a/src/lib/tools/roles.tools.ts b/src/lib/tools/roles.tools.ts new file mode 100644 index 0000000..92f1f1a --- /dev/null +++ b/src/lib/tools/roles.tools.ts @@ -0,0 +1,9 @@ +import { Role } from '$lib/enums' + +export abstract class Roles { + public static getRoleValue(role: Role): number { + const roleValues = Object.keys(Role).filter(r => isNaN(Number(r))) + + return roleValues.indexOf(role) + } +} diff --git a/src/lib/tools/strings.tools.ts b/src/lib/tools/strings.tools.ts new file mode 100644 index 0000000..633a130 --- /dev/null +++ b/src/lib/tools/strings.tools.ts @@ -0,0 +1,68 @@ +export abstract class Str { + // * CONSTANTS + static #UNITS = [ + 'byte', + 'kilobyte', + 'megabyte', + 'gigabyte', + 'terabyte', + 'petabyte', + ] + + static #BYTES_PER_KB = 1024 + + public static ucFirst(value: string): string { + return value.at(0)?.toUpperCase() + value.slice(1) + } + + public static toCamelCase(value: string): string { + return value.replace(/(?:^\p{L}*|[A-Z]|\b\p{L}*)/gu, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase() + }).replace(/\s+/g, '') + } + + public static toSnakeCase(value: string): string { + return value.replace(/\W+/g, ' ') + .split(/ |\B(?=[A-Z])/) + .map(word => word.toLowerCase()) + .join('_').replace(/^(_+)?(.*)/, '$2').replace(/_*$/, '') + } + + public static toTitleCase(value: string): string { + return value.replace( + /\p{L}*\S*/gu, + text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), + ) + } + + public static toKebabCase(value: string): string { + return value.replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase() + } + + public static limit(value: string, limit = 25, ellipsis = '...'): string { + if (limit < 1) { + throw new Error('Limit cannot be less than 1') + } + return value.slice(0, limit) + (value.length > limit ? ellipsis : '') + } + + /** Human file size change. */ + public static humanFileSize(sizeBytes: number): string { + let size = Math.abs(Number(sizeBytes)) + + let u = 0 + while (size >= this.#BYTES_PER_KB && u < this.#UNITS.length - 1) { + size /= this.#BYTES_PER_KB + ++u + } + + return new Intl.NumberFormat([], { + style: 'unit', + unit: this.#UNITS[u], + unitDisplay: 'short', + maximumFractionDigits: 1, + }).format(size) + } +} diff --git a/src/lib/tools/translations.tools.ts b/src/lib/tools/translations.tools.ts new file mode 100644 index 0000000..496cc2c --- /dev/null +++ b/src/lib/tools/translations.tools.ts @@ -0,0 +1,93 @@ +import type { Parser } from '@sveltekit-i18n/base' +import type { Readable } from 'svelte/store' +import i18n, { type Config } from 'sveltekit-i18n' +import lang from '../../locales/lang.json' + +export const defaultLocale = 'fr' + +// Hacky for now https://github.com/sveltekit-i18n/lib/issues/60 +interface Params { + pageName?: string + pageNumber?: number + entity?: string + resource?: string + user?: string + sort?: string +} + +const config: Config = ({ + translations: { + fr: { lang }, + }, + loaders: [ + { + locale: 'fr', + key: 'help', + loader: async () => ( + await import('../../locales/fr/help.json') + ).default, + }, + { + locale: 'fr', + key: 'common', + loader: async () => ( + await import('../../locales/fr/common.json') + ).default, + }, + { + locale: 'fr', + key: 'icons', + loader: async () => ( + await import('../../locales/fr/icons.json') + ).default, + }, + { + locale: 'fr', + key: 'entities', + loader: async () => ( + await import('../../locales/fr/entities.json') + ).default, + }, + { + locale: 'fr', + key: 'pagination', + loader: async () => ( + await import('../../locales/fr/pagination.json') + ).default, + }, + { + locale: 'fr', + key: 'enums', + loader: async () => ( + await import('../../locales/fr/enums.json') + ).default, + }, + { + locale: 'fr', + key: 'statistics', + loader: async () => ( + await import('../../locales/fr/statistics.json') + ).default, + }, + { + locale: 'fr', + key: 'crud', + loader: async () => ( + await import('../../locales/fr/crud.json') + ).default, + }, + ], +}) + +const i18N = new i18n(config) + +// Ignore the any ouput from translate function +type ExtendedStore T, Store = Readable> = Store & { + get: Get +} +type TranslationFunction

= (key: string, ...restParams: P) => string +export const t = i18N.t as ExtendedStore, TranslationFunction> + +export const { loading, locales, locale, loadTranslations, addTranslations, translations, setLocale, setRoute } = i18N + +loading.subscribe($loading => $loading) diff --git a/src/lib/tools/uniquId.tools.ts b/src/lib/tools/uniquId.tools.ts new file mode 100644 index 0000000..9644f23 --- /dev/null +++ b/src/lib/tools/uniquId.tools.ts @@ -0,0 +1,3 @@ +export function uniquId(length = 16): number { + return parseInt(Math.ceil(Math.random() * Date.now()).toPrecision(length).toString().replace('.', '')) +} diff --git a/src/lib/tools/uuidValidator.tools.ts b/src/lib/tools/uuidValidator.tools.ts new file mode 100644 index 0000000..69138d5 --- /dev/null +++ b/src/lib/tools/uuidValidator.tools.ts @@ -0,0 +1,3 @@ +export function isValidUuid(value: string): boolean { + return (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/.exec(value)) !== null +} diff --git a/src/lib/tools/zod.tools.ts b/src/lib/tools/zod.tools.ts new file mode 100644 index 0000000..c7b055d --- /dev/null +++ b/src/lib/tools/zod.tools.ts @@ -0,0 +1,19 @@ +import i18next from 'i18next' +import { z } from 'zod' +// Fix for https://stackoverflow.com/a/77108389/4355295 +// https://github.com/aiji42/zod-i18n/issues/204 +// @ts-ignore +import { zodI18nMap } from 'zod-i18n-map/dist/index.mjs' +// Import your language translation files +import translation from 'zod-i18n-map/locales/fr/zod.json' + +// lng and resources key depend on your locale. +i18next.init({ + lng: 'fr', + resources: { + fr: { zod: translation }, + }, +}) +z.setErrorMap(zodI18nMap) + +export { z as zod } diff --git a/src/locales/fr/common.json b/src/locales/fr/common.json new file mode 100644 index 0000000..0b96546 --- /dev/null +++ b/src/locales/fr/common.json @@ -0,0 +1,67 @@ +{ + "pages": { + "login": "connexion", + "forgotten": "mot de passe oublié", + "account": "mon compte", + "security": "sécurité", + "profile": "profil", + "export": "export de liens", + "import": "import de liens", + "dashboard": "accueil", + "links": "liens", + "links_of_entity": "liens {{entity}}", + "link-statistics": "visites des liens", + "link-statistics-brands": "visites par enseigne", + "link-statistics-catalogs": "visites par catalogue", + "link-statistics-tags": "visites par étiquette", + "catalogs": "catalogues", + "users": "utilisateurs", + "tags": "étiquettes", + "brands": "enseignes", + "help": "aide", + "statistics": "statistiques" + }, + "action": { + "copy_resource": "Copier {{resource}}", + "confirm": "Confirmer", + "actions": "actions", + "gotoHelp": "Consulter l'aide pour cet élement", + "previous": "Revenir en arrière", + "goto": "Aller sur la page {{pageName}}", + "export": "Exporter", + "export_graphic": "Exporter le graphique actuel", + "view_statistics": "Voir les statistiques", + "expand": "Plein écran", + "zoom_in": "Zoomer en avant", + "zoom_out": "Zoomer en arrière", + "zoom_reset": "Réinitialiser le zoom", + "pan_left": "Déplacer à gauche", + "pan_right": "Déplacer à droite", + "pan_up": "Déplacer en haut", + "pan_down": "Déplacer en bas", + "pan_reset": "Réinitialiser le panoramique", + "close": "Fermer", + "download_element_picture": "Télécharger une image de cet élément" + }, + "statement": { + "welcome": "Bienvenue {{user}}", + "error_occured": "une erreur est survenue", + "no_entity": "aucun {{entity}}", + "contextual_help": "aide contextuelle", + "ask_for_delete": "Voulez-vous supprimer cet élément ?", + "resource_copied": "{{resource}} copié" + }, + "sort": { + "sortBy": "Trier par ordre {{sort}}", + "asc": "croissant", + "desc": "décroissant", + "auto": "naturel" + }, + "inputs": { + "input-collection": { + "empty": "Aucun élément", + "add": "Ajouter un élément", + "remove": "Supprimer cet élément" + } + } +} \ No newline at end of file diff --git a/src/locales/fr/crud.json b/src/locales/fr/crud.json new file mode 100644 index 0000000..0113ecb --- /dev/null +++ b/src/locales/fr/crud.json @@ -0,0 +1,26 @@ +{ + "common": { + "list_of": "Liste {{resource}}", + "create": "Créer {{resource}}", + "edit": "Modifier {{resource}}", + "show": "Voir {{resource}}", + "delete": "Supprimer {{resource}}", + "save": "Enregistrer" + }, + "inflection": { + "of": "de", + "of_many": "des", + "of_p": "d'", + "of_the": "du", + "of_the_f": "de la", + "of_the_p": "de l'", + "the": "le", + "the_p": "l'", + "a": "un", + "a_f": "une", + "a_p": "d'un", + "a_p_f": "d'une", + "an": "un", + "an_p": "d'un" + } +} \ No newline at end of file diff --git a/src/locales/fr/entities.json b/src/locales/fr/entities.json new file mode 100644 index 0000000..f2e8159 --- /dev/null +++ b/src/locales/fr/entities.json @@ -0,0 +1,9 @@ +{ + "list": { + "brand": "enseigne", + "catalog": "catalogue", + "user": "utilisateur", + "tag": "étiquette", + "link": "lien" + } +} diff --git a/src/locales/fr/enums.json b/src/locales/fr/enums.json new file mode 100644 index 0000000..91dd357 --- /dev/null +++ b/src/locales/fr/enums.json @@ -0,0 +1,7 @@ +{ + "roles": { + "ADMINISTRATOR": "Administrateur", + "MANAGER": "Manager", + "PUBLISHER": "Éditeur" + } +} diff --git a/src/locales/fr/help.json b/src/locales/fr/help.json new file mode 100644 index 0000000..5f75b86 --- /dev/null +++ b/src/locales/fr/help.json @@ -0,0 +1,14 @@ +{ + "pages": { + "help": "aide", + "import": "import de fichiers", + "brands": "enseignes", + "resolve": "résolution de liens", + "visits": "visites", + "statistics": "statistiques" + }, + "statement": { + "loading": "chargement en cours...", + "loading_error": "une erreur est survenue lors du chargement de la page" + } +} \ No newline at end of file diff --git a/src/locales/fr/icons.json b/src/locales/fr/icons.json new file mode 100644 index 0000000..02d8b73 --- /dev/null +++ b/src/locales/fr/icons.json @@ -0,0 +1,38 @@ +{ + "HelpCircleIcon": { + "title": "Aide" + }, + "Eye": { + "title": "Un oeil ouvert pour voir quelque chose" + }, + "ZoomIn": { + "title": "Une loupe avec un signe plus à l'intérieur" + }, + "ZoomOut": { + "title": "Une loupe avec un signe moins à l'intérieur" + }, + "ImageDown": { + "title": "Un téléchargement d'image" + }, + "MoveLeft": { + "title": "Une flèche pointant vers la gauche" + }, + "MoveRight": { + "title": "Une flèche pointant vers la droite" + }, + "MoveUp": { + "title": "Une flèche pointant vers le haut" + }, + "MoveDown": { + "title": "Une flèche pointant vers le bas" + }, + "Shrink": { + "title": "Des flèches pointant vers le centre" + }, + "SearchX": { + "title": "Une loupe avec un signe de croix à l'intérieur" + }, + "X": { + "title": "Une croix" + } +} \ No newline at end of file diff --git a/src/locales/fr/pagination.json b/src/locales/fr/pagination.json new file mode 100644 index 0000000..102cf39 --- /dev/null +++ b/src/locales/fr/pagination.json @@ -0,0 +1,9 @@ +{ + "previous": "précedent", + "next": "suivant", + "gotoPage": "aller à la page {{pageNumber}}", + "showing": "affichage", + "to": "à", + "of": "de", + "results": "résultats" +} \ No newline at end of file diff --git a/src/locales/fr/statistics.json b/src/locales/fr/statistics.json new file mode 100644 index 0000000..e4d7c26 --- /dev/null +++ b/src/locales/fr/statistics.json @@ -0,0 +1,70 @@ +{ + "global": { + "total_invalid_links_all": { + "title": "Liens invalides" + }, + "total_invalid_links_brand": { + "title": "Liens invalides" + }, + "total_visits_all": { + "title": "Visites sur tous les liens" + }, + "total_visits_brand": { + "title": "Visites sur l'enseigne" + }, + "total_external_visits_all": { + "title": "Visites sur des liens externes" + }, + "total_external_visits_brand": { + "title": "Visites sur des liens externes" + }, + "total_brand_less_visits": { + "title": "Visites sur les liens sans enseignes" + } + }, + "statement": { + "no_entity_stats": "Aucune statistique {{entity}}", + "link_visits_by_brand": "visites des liens par enseigne", + "link_visits_by_catalog": "visites des liens par catalogue", + "link_visits_by_tag": "visites des liens par étiquette", + "no_brand": "sans enseigne" + }, + "application_stats": { + "categories": { + "invalid_links": { + "breadcrumb": "liens invalides" + }, + "top_list": { + "breadcrumb": "classements" + }, + "unique_visitors": { + "breadcrumb": "visites uniques" + } + }, + "unique_visitors_per_brand": { + "breadcrumb": "visites uniques par enseigne", + "title": "Visites uniques par enseigne", + "description": "Nombre de visiteurs uniques par enseigne et par jour" + }, + "unique_visitors_per_catalog": { + "breadcrumb": "visites uniques par catalogue", + "title": "Visites uniques par catalogue", + "description": "Nombre de visiteurs uniques par catalogue et par jour" + }, + "invalid_link_per_brand_per_week": { + "breadcrumb": "par enseigne", + "title": "Liens invalides par enseigne", + "description": "Le nombre de liens invalides par enseigne et par semaine" + }, + "most_frequent_referrer_by_brand": { + "breadcrumb": "meilleurs référents par enseigne", + "title": "Provenance des visites par enseigne les plus fréquentes", + "description": "Un classement des provenances les plus courantes des visites par enseigne" + }, + "most_frequent_source_by_brand": { + "breadcrumb": "meilleurs liens par enseigne", + "title": "Les liens les plus utilisés par enseigne", + "description": "Un classement des liens les plus utilisés par enseigne" + } + } +} \ No newline at end of file diff --git a/src/locales/lang.json b/src/locales/lang.json new file mode 100644 index 0000000..da249e2 --- /dev/null +++ b/src/locales/lang.json @@ -0,0 +1,4 @@ +{ + "en": "English", + "fr": "Francais" +} \ No newline at end of file diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 0000000..cf6f722 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1 @@ +import 'wicg-inert' diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts new file mode 100644 index 0000000..1a2d6fd --- /dev/null +++ b/src/routes/(app)/+layout.server.ts @@ -0,0 +1,50 @@ +import { redirect } from '@sveltejs/kit' + +import { sessionService } from '$lib/server/services' +import { defaultLocale, loadTranslations, locales, translations } from '$lib/tools' +import type { LayoutServerLoad } from './$types' + +export const load: LayoutServerLoad = async (event) => { + const { url, cookies, request, locals } = event + const { pathname } = url + + // CHECK SESSION + if (!locals.session) return redirect(301, '/login') + + // SETUP LOCALE + // Try to get the locale from cookie + let locale = (cookies.get('lang') || '').toLowerCase() + + // Get user preferred locale + if (!locale) { + locale = `${/[a-zA-Z]+?(?=-|_|,|;)/.exec((`${request.headers.get('accept-language')}`))}`.toLowerCase() + } + + // Get defined locales + const supportedLocales = locales.get().map(l => l.toLowerCase()) + + // Use default locale if current locale is not supported + if (!supportedLocales.includes(locale)) { + locale = defaultLocale + } + + await loadTranslations(locale, pathname) + + // Consume flash messages + const messages = [ + ...(locals.flashMessages ?? []), + ...sessionService.getFlash(cookies).consumeMessages(), + ] + locals.flashMessages = [] + + return { + activeBrand: locals.activeBrand, + brands: locals.brands, + olds: request.method === 'GET' ? sessionService.getFlashOlds(cookies).consumeOlds() : {}, + errors: sessionService.getFlashErrors(cookies).consumeErrors(), + messages: messages, + auth: locals.session?.user ?? null, + i18n: { locale, route: pathname }, + translations: translations.get(), + } +} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..2d82068 --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,109 @@ + + + + {$title} + + +

+
+ +
+ {#if flashMessagesList.length} +
+
+ {#each flashMessagesList as message, index(index)} + onClosedFlashMessage(index)} + /> + {/each} +
+
+ {/if} +
+
+ +
+
+
+ {@render children()} +
+ +
+ +
+ +
+ +
+ + diff --git a/src/routes/(app)/+layout.ts b/src/routes/(app)/+layout.ts new file mode 100644 index 0000000..2052d4c --- /dev/null +++ b/src/routes/(app)/+layout.ts @@ -0,0 +1,18 @@ +import type { LayoutLoad } from './$types' + +import { inputErrors } from '$lib/client/stores' +import { addTranslations, setLocale, setRoute } from '$lib/tools' + +export const load: LayoutLoad = ({ data }) => { + const { i18n, auth, translations, errors, olds, activeBrand, brands, messages } = data + const { locale, route } = i18n + + addTranslations(translations) + + setRoute(route) + setLocale(locale) + + inputErrors.set(errors) + + return { i18n, auth, olds, activeBrand, brands, errors, messages } +} diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts new file mode 100644 index 0000000..f432c97 --- /dev/null +++ b/src/routes/(app)/+page.server.ts @@ -0,0 +1,27 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import { Role } from '$lib/enums' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' + +export const load: ServerLoad = async ({ locals, cookies }) => { + const { brands, session } = locals + + const availableBrandUuids = brands.map(b => b.uuid) + let globalStatistics: Record = {} + const linkStatistics: Record = {} + + // Keep only available brands + Object.entries(await linkStatisticsService.linkStatisticsForBrands(cookies)).forEach((statLineEntry) => { + if (availableBrandUuids.indexOf(statLineEntry[0]) !== -1) { + linkStatistics[statLineEntry[0]] = statLineEntry[1] + } + }) + + if (session?.user.role === Role.ADMINISTRATOR) { + globalStatistics = await linkStatisticsService.globalStatistics(cookies) + } + + return { linkStatistics, globalStatistics } +} diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte new file mode 100644 index 0000000..480b3e2 --- /dev/null +++ b/src/routes/(app)/+page.svelte @@ -0,0 +1,79 @@ + + + + {Str.toTitleCase($t('common.pages.link-statistics'))} + + +
+
+ {#if chartDataSet.length > 1} + + {:else} +
+ + + + + + + + + + {#each chartDataSet as statisticLine, index(index)} + + + + + {/each} + +
{Str.toTitleCase($t('common.pages.brands'))}Visites
{statisticLine.name}{statisticLine.totalVisits}
+
+ {/if} +
+ {#if Object.values(globalStatistics).length} +
+ +
+ {/if} +
diff --git a/src/routes/(app)/account/+page.svelte b/src/routes/(app)/account/+page.svelte new file mode 100644 index 0000000..1c70810 --- /dev/null +++ b/src/routes/(app)/account/+page.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/src/routes/(app)/account/profile/+page.svelte b/src/routes/(app)/account/profile/+page.svelte new file mode 100644 index 0000000..9fef11b --- /dev/null +++ b/src/routes/(app)/account/profile/+page.svelte @@ -0,0 +1,49 @@ + + + + +
+
+
+
+
UUID
+
{auth.uuid}
+
+
+
Nom
+
{auth.lastName}
+
+
+
Prénom
+
{auth.firstName}
+
+
+
Email
+
{auth.email}
+
+
+
Role
+
{Str.ucFirst(auth.role.toLocaleLowerCase())}
+
+
+
+
diff --git a/src/routes/(app)/account/security/+page.server.ts b/src/routes/(app)/account/security/+page.server.ts new file mode 100644 index 0000000..838c8cb --- /dev/null +++ b/src/routes/(app)/account/security/+page.server.ts @@ -0,0 +1,52 @@ +import { error, fail, type ActionFailure, type Actions } from '@sveltejs/kit' +import { RateLimiter } from 'sveltekit-rate-limiter/server' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import { authService } from '$lib/server/services' +import { Z3Request } from '$lib/tools/request.tools' +import updatePasswordRequestSchema from './updatePassword.request.schema' + +const limiter = new RateLimiter({ + // A rate is defined as [number, unit] + IP: [10, 'h'], // IP address limiter + IPUA: [5, 'm'], // IP + User Agent limiter +}) + +export const actions = { + default: async (event): Promise> => { + if (await limiter.isLimited(event)) error(429) + + const { request, locals, cookies } = event + + const z3Request = Z3Request.of(request, updatePasswordRequestSchema) + const requestData = await z3Request.formInputs() + const validation = await z3Request.validated() + + if (requestData.get('new-password') !== undefined + && requestData.get('new-password') !== requestData.get('new-password-confirm')) { + validation.errors['new-password-confirm'] = 'Les mot de passe doivent être identiques' + } + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { message, errors: validation.errors }) + } + + const result = await authService.changePassword( + cookies, + validation.validated.password, + validation.validated['new-password'], + ) + + if (!result) { + const message = 'Echec du changement de mot de passe' + locals.flashMessages = [{ message: message, type: 'error' }] + return fail(500, { message }) + } + + const message = 'Le mot de passe à bien été mis à jour' + locals.flashMessages = [{ message: message, type: 'success' }] + return { message } + }, +} satisfies Actions diff --git a/src/routes/(app)/account/security/+page.svelte b/src/routes/(app)/account/security/+page.svelte new file mode 100644 index 0000000..a29610d --- /dev/null +++ b/src/routes/(app)/account/security/+page.svelte @@ -0,0 +1,87 @@ + + + + +
+
+
+
+ Utilisez le formulaire ci-dessous pour modifier votre mot de passe. + + + +
+ +
+
+
+
+
diff --git a/src/routes/(app)/account/security/updatePassword.request.schema.ts b/src/routes/(app)/account/security/updatePassword.request.schema.ts new file mode 100644 index 0000000..219c9f3 --- /dev/null +++ b/src/routes/(app)/account/security/updatePassword.request.schema.ts @@ -0,0 +1,7 @@ +import { zod } from '$lib/tools/zod.tools' + +export default zod.object({ + 'password': zod.string().min(1), + 'new-password': zod.string().min(1), + 'new-password-confirm': zod.string().min(1), +}) diff --git a/src/routes/(app)/api/active-brand/+page.server.ts b/src/routes/(app)/api/active-brand/+page.server.ts new file mode 100644 index 0000000..cad7a23 --- /dev/null +++ b/src/routes/(app)/api/active-brand/+page.server.ts @@ -0,0 +1,30 @@ +import { error, fail, type ActionFailure, type ActionResult, type Actions } from '@sveltejs/kit' + +import { brandService, sessionService } from '$lib/server/services' + +export const actions = { + default: async ({ request, cookies, locals }): Promise => { + // CHECK SESSION + if (!locals.session) return error(401) + + const sessionUser = await sessionService.getSessionUser(cookies), + requestData = await request.formData(), + brandSlug = requestData.get('active-brand'), + brand = typeof brandSlug === 'string' ? await brandService.getBrand(cookies, brandSlug) : null + + if (sessionUser === undefined) { + return fail(401) + } + + if (brand == null) { + return fail(422) + } + + sessionUser.setActiveBrand(brand) + + return { + type: 'success', + status: 200, + } + }, +} satisfies Actions diff --git a/src/routes/(app)/api/brands/+server.ts b/src/routes/(app)/api/brands/+server.ts new file mode 100644 index 0000000..309f464 --- /dev/null +++ b/src/routes/(app)/api/brands/+server.ts @@ -0,0 +1,14 @@ +import { error, json, type RequestHandler } from '@sveltejs/kit' + +import { brandService } from '$lib/server/services' + +export const GET: RequestHandler = async ({ url, locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 1)), 1), 50) + const paginatedBrands = await brandService.listBrands(cookies, page, perPage) + + return json(paginatedBrands) +} diff --git a/src/routes/(app)/api/catalogs/+server.ts b/src/routes/(app)/api/catalogs/+server.ts new file mode 100644 index 0000000..90abe48 --- /dev/null +++ b/src/routes/(app)/api/catalogs/+server.ts @@ -0,0 +1,16 @@ +import { error, json, type RequestHandler } from '@sveltejs/kit' + +import { catalogService } from '$lib/server/services' + +export const GET: RequestHandler = async ({ url, locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const activeBrand = locals.activeBrand + + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 1)), 1), 50) + const paginatedCatalogs = await catalogService.listCatalogs(cookies, page, perPage, activeBrand?.uuid) + + return json(paginatedCatalogs) +} diff --git a/src/routes/(app)/api/flash-messages/+server.ts b/src/routes/(app)/api/flash-messages/+server.ts new file mode 100644 index 0000000..dd64eea --- /dev/null +++ b/src/routes/(app)/api/flash-messages/+server.ts @@ -0,0 +1,12 @@ +import { error, json, type RequestHandler } from '@sveltejs/kit' + +import { sessionService } from '$lib/server/services' + +export const GET: RequestHandler = async ({ cookies, locals }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const flashMessages = sessionService.getFlash(cookies).consumeMessages() + + return json(flashMessages) +} diff --git a/src/routes/(app)/api/links/[source]/verify/+server.ts b/src/routes/(app)/api/links/[source]/verify/+server.ts new file mode 100644 index 0000000..80659f0 --- /dev/null +++ b/src/routes/(app)/api/links/[source]/verify/+server.ts @@ -0,0 +1,24 @@ +import { error, json, type Cookies, type RequestHandler } from '@sveltejs/kit' + +import type { Link } from '$lib/models' +import { linkService } from '$lib/server/services' + +export const PUT: RequestHandler = async ({ params, locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const link = await getRequestEntity(params, cookies) + + const linkVerification = await linkService.verifyLink(cookies, link.source) + + return json(linkVerification) +} + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const source = params['source'], + user = source ? await linkService.getLink(cookies, source) : null + + if (!user) error(404) + + return new Promise(resolve => resolve(user)) +} diff --git a/src/routes/(app)/api/links/export/brands/+server.ts b/src/routes/(app)/api/links/export/brands/+server.ts new file mode 100644 index 0000000..529a4a1 --- /dev/null +++ b/src/routes/(app)/api/links/export/brands/+server.ts @@ -0,0 +1,30 @@ +import { error, type Cookies, type RequestHandler } from '@sveltejs/kit' + +import type { Brand } from '$lib/models' +import { brandService, linkService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ url, locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const brand = await getRequestsBrandModel(url, cookies) + const exportStream = await linkService.exportBrandLinks(cookies, brand) + const filename = `z3-links-export-all-${Str.toKebabCase(brand.name)}-links-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} + +async function getRequestsBrandModel(url: URL, cookies: Cookies): Promise { + const slug = url.searchParams.get('brand'), + brand = slug ? await brandService.getBrand(cookies, slug) : null + + if (!brand) error(404) + + return new Promise(resolve => resolve(brand)) +} diff --git a/src/routes/(app)/api/links/export/catalogs/+server.ts b/src/routes/(app)/api/links/export/catalogs/+server.ts new file mode 100644 index 0000000..319362d --- /dev/null +++ b/src/routes/(app)/api/links/export/catalogs/+server.ts @@ -0,0 +1,30 @@ +import { error, type Cookies, type RequestHandler } from '@sveltejs/kit' + +import type { Catalog } from '$lib/models' +import { catalogService, linkService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ url, locals, cookies }): Promise => { + // CHECK SESSION + if (!locals.session) return error(401) + + const catalog = await getRequestsCatalogModel(url, cookies) + const exportStream = await linkService.exportCatalogLinks(cookies, catalog) + const filename = `z3-links-export-all-${Str.toKebabCase(catalog.name)}-links-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} + +async function getRequestsCatalogModel(url: URL, cookies: Cookies): Promise { + const slug = url.searchParams.get('catalog'), + catalog = slug ? await catalogService.getCatalog(cookies, slug) : null + + if (!catalog) error(404) + + return new Promise(resolve => resolve(catalog)) +} diff --git a/src/routes/(app)/api/links/export/nobrand/+server.ts b/src/routes/(app)/api/links/export/nobrand/+server.ts new file mode 100644 index 0000000..2436aa9 --- /dev/null +++ b/src/routes/(app)/api/links/export/nobrand/+server.ts @@ -0,0 +1,19 @@ +import { error, type RequestHandler } from '@sveltejs/kit' + +import { linkService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(401) + + const exportStream = await linkService.exportLinks(cookies) + const filename = `z3-links-export-all-links-with-no-brands-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} diff --git a/src/routes/(app)/api/links/statistics/brands/+server.ts b/src/routes/(app)/api/links/statistics/brands/+server.ts new file mode 100644 index 0000000..4698f6d --- /dev/null +++ b/src/routes/(app)/api/links/statistics/brands/+server.ts @@ -0,0 +1,33 @@ +import { type RequestHandler } from '@sveltejs/kit' + +import { linkStatisticsService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ locals, cookies }) => { + const { brands } = locals + + const availableBrandUuids = brands.map(b => b.uuid) + + const filename = `z3-links-statistics-brands-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + const exportStream = new ReadableStream({ + async start(controller): Promise { + controller.enqueue(`"Identifiant","Nom","Nombre de visites"\n`) + + Object.entries(await linkStatisticsService.linkStatisticsForBrands(cookies)).forEach((statLineEntry) => { + const statLine = statLineEntry[1] + if (availableBrandUuids.indexOf(statLineEntry[0]) !== -1) { + controller.enqueue(`"${statLine.uuid}","${statLine.name}","${statLine.totalVisits}"\n`) + } + }) + + controller.close() + }, + }) + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} diff --git a/src/routes/(app)/api/links/statistics/catalogs/+server.ts b/src/routes/(app)/api/links/statistics/catalogs/+server.ts new file mode 100644 index 0000000..7a4df00 --- /dev/null +++ b/src/routes/(app)/api/links/statistics/catalogs/+server.ts @@ -0,0 +1,36 @@ +import { type RequestHandler } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ locals, cookies }) => { + const { activeBrand } = locals + + const filename = `z3-links-statistics-catalogs-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + const exportStream = new ReadableStream({ + async start(controller): Promise { + let linkStatistics: Record = {} + if (activeBrand) { + linkStatistics = await linkStatisticsService.linkCatalogStatisticsFor(cookies, activeBrand.uuid) + } + + controller.enqueue(`"Identifiant","Nom","Nombre de visites"\n`) + + Object.entries(linkStatistics).forEach((statLineEntry) => { + const statLine = statLineEntry[1] + controller.enqueue(`"${statLine.uuid}","${statLine.name}","${statLine.totalVisits}"\n`) + }) + + controller.close() + }, + }) + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} diff --git a/src/routes/(app)/api/links/statistics/tags/+server.ts b/src/routes/(app)/api/links/statistics/tags/+server.ts new file mode 100644 index 0000000..ac0c774 --- /dev/null +++ b/src/routes/(app)/api/links/statistics/tags/+server.ts @@ -0,0 +1,36 @@ +import { type RequestHandler } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' +import { Dates, Str } from '$lib/tools' + +export const GET: RequestHandler = async ({ locals, cookies }) => { + const { activeBrand } = locals + + const filename = `z3-links-statistics-tags-${Str.toKebabCase(Dates.dateTime(Date.now()))}` + const exportStream = new ReadableStream({ + async start(controller): Promise { + let linkStatistics: Record = {} + if (activeBrand) { + linkStatistics = await linkStatisticsService.linkTagStatisticsFor(cookies, activeBrand.uuid) + } + + controller.enqueue(`"Identifiant","Nom","Nombre de visites"\n`) + + Object.entries(linkStatistics).forEach((statLineEntry) => { + const statLine = statLineEntry[1] + controller.enqueue(`"${statLine.uuid}","${statLine.name}","${statLine.totalVisits}"\n`) + }) + + controller.close() + }, + }) + + return new Response(exportStream, { + headers: { + 'Content-type': 'text/csv', + 'Content-Disposition': `attachment; filename=${filename}.csv`, + }, + }) +} diff --git a/src/routes/(app)/api/tags/+server.ts b/src/routes/(app)/api/tags/+server.ts new file mode 100644 index 0000000..ea714ed --- /dev/null +++ b/src/routes/(app)/api/tags/+server.ts @@ -0,0 +1,14 @@ +import { error, json, type RequestHandler } from '@sveltejs/kit' + +import { tagService } from '$lib/server/services' + +export const GET: RequestHandler = async ({ url, locals, cookies }) => { + // CHECK SESSION + if (!locals.session) return error(301) + + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 1)), 1), 50) + const paginatedTags = await tagService.listTags(cookies, page, perPage) + + return json(paginatedTags) +} diff --git a/src/routes/(app)/brands/+page.server.ts b/src/routes/(app)/brands/+page.server.ts new file mode 100644 index 0000000..da5dcf9 --- /dev/null +++ b/src/routes/(app)/brands/+page.server.ts @@ -0,0 +1,11 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { brandService } from '$lib/server/services' + +export const load: ServerLoad = async ({ url, cookies }) => { + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 15)), 1), 50) + const paginatedBrands = await brandService.listBrands(cookies, page, perPage) + + return { paginatedBrands } +} diff --git a/src/routes/(app)/brands/+page.svelte b/src/routes/(app)/brands/+page.svelte new file mode 100644 index 0000000..8178c22 --- /dev/null +++ b/src/routes/(app)/brands/+page.svelte @@ -0,0 +1,108 @@ + + + + + + + + + +{#if data.paginatedBrands.list.length > 5} +
+
+ +
+
+{/if} + +
+
+ {#if data.paginatedBrands.list.length} +
+ + + + + + + + + + {#each data.paginatedBrands.list as brand (brand.uuid)} + + + + + + {/each} + +
+ Nom + + Nom de domaines + {Str.toTitleCase($t('common.action.actions'))}
{brand.name} +
+ {#if can('brand:view', data.auth)} + + + {/if} + {#if can('brand:update', data.auth)} + + + {/if} +
+
+
+ {:else} + {Str.ucFirst($t('common.statement.no_entity', { entity: $t('common.pages.brands') }))} + {/if} +
+
+ +
+
+
+ +
+
diff --git a/src/routes/(app)/brands/[slug]/+page.server.ts b/src/routes/(app)/brands/[slug]/+page.server.ts new file mode 100644 index 0000000..3d0b0e2 --- /dev/null +++ b/src/routes/(app)/brands/[slug]/+page.server.ts @@ -0,0 +1,19 @@ +import { error, type Cookies, type ServerLoad } from '@sveltejs/kit' + +import type { Brand } from '$lib/models' +import { brandService } from '$lib/server/services' + +export const load: ServerLoad = async ({ params, cookies }) => { + const brand = await getRequestEntity(params, cookies) + + return { brand } +} + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + brand = slug ? await brandService.getBrand(cookies, slug) : null + + if (!brand) error(404) + + return new Promise(resolve => resolve(brand)) +} diff --git a/src/routes/(app)/brands/[slug]/+page.svelte b/src/routes/(app)/brands/[slug]/+page.svelte new file mode 100644 index 0000000..e616a3b --- /dev/null +++ b/src/routes/(app)/brands/[slug]/+page.svelte @@ -0,0 +1,56 @@ + + + + + {#if can('brand:update', data.auth)} + + + {/if} + + + + +
+
+
+
+
UUID
+
{data.brand.uuid}
+
+
+
Nom
+
{data.brand.name}
+
+
+
Nom de domaines
+
+
+
+
+
diff --git a/src/routes/(app)/brands/[slug]/edit/+page.server.ts b/src/routes/(app)/brands/[slug]/edit/+page.server.ts new file mode 100644 index 0000000..95ec19d --- /dev/null +++ b/src/routes/(app)/brands/[slug]/edit/+page.server.ts @@ -0,0 +1,51 @@ +import type { ActionFailure, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { Brand } from '$lib/models' +import { brandService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import updateBrandRequestSchema from './updateBrand.request.schema' + +export const load: ServerLoad = async ({ params, cookies }) => { + const brand = await getRequestsBrandModel(params, cookies) + + return { brand } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + const brand = await getRequestsBrandModel(params, cookies) + + const z3Request = Z3Request.of(request, updateBrandRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + Z3Request.assignEntityProps(brand, validation.validated) + + await brandService.updateBrand(cookies, brand) + + const message = `${t.get('entities.list.brand')} à bien été mis à jour` + locals.flashMessages = [{ message: message, type: 'success' }] + return { message } + }, +} satisfies Actions + +async function getRequestsBrandModel(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + brand = slug ? await brandService.getBrand(cookies, slug) : null + + if (!brand) error(404) + + return new Promise(resolve => resolve(brand)) +} diff --git a/src/routes/(app)/brands/[slug]/edit/+page.svelte b/src/routes/(app)/brands/[slug]/edit/+page.svelte new file mode 100644 index 0000000..bd29901 --- /dev/null +++ b/src/routes/(app)/brands/[slug]/edit/+page.svelte @@ -0,0 +1,133 @@ + + + + + {#if can('brand:view', data.auth)} + + + {/if} + + + + +
+
+
+ Identifiants + +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/brands/[slug]/edit/updateBrand.request.schema.ts b/src/routes/(app)/brands/[slug]/edit/updateBrand.request.schema.ts new file mode 100644 index 0000000..fccada8 --- /dev/null +++ b/src/routes/(app)/brands/[slug]/edit/updateBrand.request.schema.ts @@ -0,0 +1,9 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), + fqdns: zod.preprocess( + value => value !== undefined ? value : [], + zod.array(zod.string().min(1)), + ), +}) diff --git a/src/routes/(app)/brands/create/+page.server.ts b/src/routes/(app)/brands/create/+page.server.ts new file mode 100644 index 0000000..f54c25f --- /dev/null +++ b/src/routes/(app)/brands/create/+page.server.ts @@ -0,0 +1,34 @@ +import type { ActionFailure } from '@sveltejs/kit' +import { fail, redirect } from '@sveltejs/kit' + +import type FormFailure from '$customType/forms/FormFailure.type' +import { brandService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import createBrandRequestSchema from './createBrand.request.schema' + +export const actions = { + default: async ({ request, cookies, locals }): Promise> => { + const z3Request = Z3Request.of(request, createBrandRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const brand = await brandService.createBrand( + cookies, + validation.validated.name, + validation.validated.fqdns, + ) + + sessionService.getFlash(cookies).flashSuccess(`${t.get('entities.list.brand')} à bien été crée`) + redirect(302, `/brands/${brand.slug}/edit`) + }, +} satisfies Actions diff --git a/src/routes/(app)/brands/create/+page.svelte b/src/routes/(app)/brands/create/+page.svelte new file mode 100644 index 0000000..6cf034c --- /dev/null +++ b/src/routes/(app)/brands/create/+page.svelte @@ -0,0 +1,104 @@ + + + + + + +
+
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/brands/create/createBrand.request.schema.ts b/src/routes/(app)/brands/create/createBrand.request.schema.ts new file mode 100644 index 0000000..fccada8 --- /dev/null +++ b/src/routes/(app)/brands/create/createBrand.request.schema.ts @@ -0,0 +1,9 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), + fqdns: zod.preprocess( + value => value !== undefined ? value : [], + zod.array(zod.string().min(1)), + ), +}) diff --git a/src/routes/(app)/catalogs/+page.server.ts b/src/routes/(app)/catalogs/+page.server.ts new file mode 100644 index 0000000..20622ba --- /dev/null +++ b/src/routes/(app)/catalogs/+page.server.ts @@ -0,0 +1,13 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { catalogService } from '$lib/server/services' + +export const load: ServerLoad = async ({ url, locals, cookies }) => { + const activeBrand = locals.activeBrand + + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 15)), 1), 50) + const paginatedCatalogs = await catalogService.listCatalogs(cookies, page, perPage, activeBrand?.uuid) + + return { paginatedCatalogs } +} diff --git a/src/routes/(app)/catalogs/+page.svelte b/src/routes/(app)/catalogs/+page.svelte new file mode 100644 index 0000000..5068431 --- /dev/null +++ b/src/routes/(app)/catalogs/+page.svelte @@ -0,0 +1,126 @@ + + + + + + +{#if data.paginatedCatalogs.list.length > 5} +
+
+ +
+
+{/if} + +
+
+ {#if data.paginatedCatalogs.list.length} +
+ + + + + + + + + {#each data.paginatedCatalogs.list as catalog (catalog.uuid)} + + + + + {/each} + +
+ Nom + {Str.toTitleCase($t('common.action.actions'))}
{catalog.name} +
+ + + +
{ + if (!await modals.open(Modal, { + title: Str.toTitleCase($t('common.action.confirm')), + message: Str.ucFirst($t('common.statement.ask_for_delete')), + confirm: true, + })) { + cancel() + } + return async ({ result }): Promise => { + applyAction(result) + } + }} + > + + +
+
+
+ {:else} + {Str.ucFirst($t('common.statement.no_entity', { entity: $t('common.pages.catalogs') }))} + {/if} +
+
+ +
+
+
+ +
+
diff --git a/src/routes/(app)/catalogs/[slug]/+page.server.ts b/src/routes/(app)/catalogs/[slug]/+page.server.ts new file mode 100644 index 0000000..e02205a --- /dev/null +++ b/src/routes/(app)/catalogs/[slug]/+page.server.ts @@ -0,0 +1,35 @@ +import type { Actions, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, redirect } from '@sveltejs/kit' + +import type { Catalog } from '$lib/models' +import { catalogService, sessionService } from '$lib/server/services' +import { Str, t } from '$lib/tools' + +export const load: ServerLoad = async ({ params, cookies }) => { + const catalog = await getRequestEntity(params, cookies) + + return { catalog } +} + +export const actions = { + delete: async ({ cookies, params }): Promise => { + const catalog = await getRequestEntity(params, cookies) + + const message = await catalogService.deleteCatalog(cookies, catalog.slug) + + sessionService.getFlash(cookies).flashWarning( + Str.ucFirst(`${t.get('entities.list.catalog')} ${message.message}`.toLowerCase()), + ) + + redirect(302, '/catalogs') + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + catalog = slug ? await catalogService.getCatalog(cookies, slug) : null + + if (!catalog) error(404) + + return new Promise(resolve => resolve(catalog)) +} diff --git a/src/routes/(app)/catalogs/[slug]/+page.svelte b/src/routes/(app)/catalogs/[slug]/+page.svelte new file mode 100644 index 0000000..e8c4475 --- /dev/null +++ b/src/routes/(app)/catalogs/[slug]/+page.svelte @@ -0,0 +1,80 @@ + + + + + +
+ +
+
+
+ +
+
+
+
+
UUID
+
{data.catalog.uuid}
+
+
+
Nom
+
{data.catalog.name}
+
+
+
+
diff --git a/src/routes/(app)/catalogs/[slug]/edit/+page.server.ts b/src/routes/(app)/catalogs/[slug]/edit/+page.server.ts new file mode 100644 index 0000000..1c22ebb --- /dev/null +++ b/src/routes/(app)/catalogs/[slug]/edit/+page.server.ts @@ -0,0 +1,51 @@ +import type { ActionFailure, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail, redirect } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { Catalog } from '$lib/models' +import { catalogService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import updateCatalogRequestSchema from './updateCatalog.request.schema' + +export const load: ServerLoad = async ({ params, cookies }) => { + const catalog = await getRequestEntity(params, cookies) + + return { catalog } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + let catalog = await getRequestEntity(params, cookies) + + const z3Request = Z3Request.of(request, updateCatalogRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + Z3Request.assignEntityProps(catalog, validation.validated) + + catalog = await catalogService.updateCatalog(cookies, catalog) + + const message = `${t.get('entities.list.catalog')} à bien été mis à jour` + sessionService.getFlash(cookies).flashSuccess(message) + redirect(302, `/catalogs/${catalog.slug}/edit`) + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + catalog = slug ? await catalogService.getCatalog(cookies, slug) : null + + if (!catalog) error(404) + + return new Promise(resolve => resolve(catalog)) +} diff --git a/src/routes/(app)/catalogs/[slug]/edit/+page.svelte b/src/routes/(app)/catalogs/[slug]/edit/+page.svelte new file mode 100644 index 0000000..f76b21d --- /dev/null +++ b/src/routes/(app)/catalogs/[slug]/edit/+page.svelte @@ -0,0 +1,145 @@ + + + + + + +
+ +
+
+
+ +
+
+
+ Identifiants + +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/catalogs/[slug]/edit/updateCatalog.request.schema.ts b/src/routes/(app)/catalogs/[slug]/edit/updateCatalog.request.schema.ts new file mode 100644 index 0000000..f2b04ef --- /dev/null +++ b/src/routes/(app)/catalogs/[slug]/edit/updateCatalog.request.schema.ts @@ -0,0 +1,5 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), +}) diff --git a/src/routes/(app)/catalogs/create/+page.server.ts b/src/routes/(app)/catalogs/create/+page.server.ts new file mode 100644 index 0000000..35dff32 --- /dev/null +++ b/src/routes/(app)/catalogs/create/+page.server.ts @@ -0,0 +1,34 @@ +import type { ActionFailure } from '@sveltejs/kit' +import { fail, redirect } from '@sveltejs/kit' + +import type { FormFailure } from '$customType/forms' +import { catalogService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import createCatalogRequestSchema from './createCatalog.request.schema' + +export const actions = { + default: async ({ request, cookies, locals }): Promise> => { + const z3Request = Z3Request.of(request, createCatalogRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const catalog = await catalogService.createCatalog( + cookies, + locals.activeBrand!.uuid, + validation.validated.name, + ) + + sessionService.getFlash(cookies).flashSuccess(`${t.get('entities.list.catalog')} à bien été crée`) + redirect(302, `/catalogs/${catalog.slug}/edit`) + }, +} satisfies Actions diff --git a/src/routes/(app)/catalogs/create/+page.svelte b/src/routes/(app)/catalogs/create/+page.svelte new file mode 100644 index 0000000..2b524a1 --- /dev/null +++ b/src/routes/(app)/catalogs/create/+page.svelte @@ -0,0 +1,86 @@ + + + + +
+
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/catalogs/create/createCatalog.request.schema.ts b/src/routes/(app)/catalogs/create/createCatalog.request.schema.ts new file mode 100644 index 0000000..f2b04ef --- /dev/null +++ b/src/routes/(app)/catalogs/create/createCatalog.request.schema.ts @@ -0,0 +1,5 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), +}) diff --git a/src/routes/(app)/help/+page.svelte b/src/routes/(app)/help/+page.svelte new file mode 100644 index 0000000..4bd4f31 --- /dev/null +++ b/src/routes/(app)/help/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/help/brands/+page.svelte b/src/routes/(app)/help/brands/+page.svelte new file mode 100644 index 0000000..aa43a53 --- /dev/null +++ b/src/routes/(app)/help/brands/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/help/import/+page.svelte b/src/routes/(app)/help/import/+page.svelte new file mode 100644 index 0000000..5f5762d --- /dev/null +++ b/src/routes/(app)/help/import/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/help/resolve/+page.svelte b/src/routes/(app)/help/resolve/+page.svelte new file mode 100644 index 0000000..5b32d38 --- /dev/null +++ b/src/routes/(app)/help/resolve/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/help/statistics/+page.svelte b/src/routes/(app)/help/statistics/+page.svelte new file mode 100644 index 0000000..5dc5ac6 --- /dev/null +++ b/src/routes/(app)/help/statistics/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/help/visits/+page.svelte b/src/routes/(app)/help/visits/+page.svelte new file mode 100644 index 0000000..7bb804e --- /dev/null +++ b/src/routes/(app)/help/visits/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(app)/links/+page.server.ts b/src/routes/(app)/links/+page.server.ts new file mode 100644 index 0000000..14e8c09 --- /dev/null +++ b/src/routes/(app)/links/+page.server.ts @@ -0,0 +1,28 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { Direction } from '$lib/enums' +import { linkService } from '$lib/server/services' + +export const load: ServerLoad = async ({ url, locals, cookies }) => { + const activeBrand = locals.activeBrand + const directions = Object.keys(Direction).filter((item) => { + return isNaN(Number(item)) + }) + + const nobrand = url.searchParams.get('nobrand') ? true : false + const searchOn = url.searchParams.get('search-on') ?? 'source' + const search = url.searchParams.get('search') + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 15)), 1), 50) + const direction = (Object.values(directions).find(dir => dir === url.searchParams.get('direction')) ?? Direction.auto) as Direction + const sortBy = url.searchParams.get('sortBy') + const filters = { + brand: !nobrand ? activeBrand?.uuid : undefined, + tag: url.searchParams.get('tag') ?? undefined, + catalog: url.searchParams.get('catalog') ?? undefined, + } + + const paginatedLinks = await linkService.listLinks(cookies, page, perPage, filters, sortBy, direction, searchOn, search) + + return { paginatedLinks } +} diff --git a/src/routes/(app)/links/+page.svelte b/src/routes/(app)/links/+page.svelte new file mode 100644 index 0000000..bdba9ef --- /dev/null +++ b/src/routes/(app)/links/+page.svelte @@ -0,0 +1,309 @@ + + + + + + + + + + +
+
+
+ +
+
+
+ + + + +
+
+ + { loadCatalogs(nextPage) }} + onClickPrevious={(previousPage): void => { loadCatalogs(previousPage) }} + paginatedEntityList={$paginatedCatalogs} + value={url.searchParams.get('catalog') ?? ''} + /> +
+
+ + { loadTags(nextPage) }} + onClickPrevious={(previousPage): void => { loadTags(previousPage) }} + paginatedEntityList={$paginatedTags} + value={url.searchParams.get('tag') ?? ''} + /> +
+
+
+
+ +{#if data.paginatedLinks.list.length > 5} +
+
+ +
+
+{/if} + +
+
+ {#if data.paginatedLinks.list.length} +
+ + + + + + + + + + + + + + + + {#each data.paginatedLinks.list as link (link.source)} + + + + + + + + + + + + {/each} + +
+ Source + + + Redirection + + {Str.ucFirst($t('entities.list.catalog'))} + + {Str.ucFirst($t('entities.list.tag'))} + + Visites + + + Externe + + + Est valide + + + Crée le + + {Str.toTitleCase($t('common.action.actions'))}
{link.source} + {link.target} + + {#if link.catalog} + {Str.toTitleCase(link.catalog.name)} + {:else} + Aucun + {/if} + + {#if link.tag} + + {link.tag.name} + + {:else} + Aucun + {/if} + {link.visits} + {link.isExternal ? 'oui' : 'non'} + + {link.validity === Validity.unknown + ? 'non testé' + : ( + link.validity === Validity.valid ? 'oui' : 'non' + )} + {Dates.niceDateTime(link.createdAt)} +
+ + + + + + +
+
+
+ {:else} + {Str.ucFirst($t('common.statement.no_entity', { entity: $t('common.pages.links') }))} + {/if} +
+
+ +
+
+
+ +
+
diff --git a/src/routes/(app)/links/+page.ts b/src/routes/(app)/links/+page.ts new file mode 100644 index 0000000..9e34b25 --- /dev/null +++ b/src/routes/(app)/links/+page.ts @@ -0,0 +1,14 @@ +import { loadPaginatedCatalogs, loadPaginatedTags } from '$lib/client/stores' +import type { PageLoad } from './$types.js' + +export const load: PageLoad = async ({ url, fetch, data }) => { + const { paginatedLinks } = data + + const catalogPage = Math.max(Number.parseInt(String(url.searchParams.get('catalog-page') ?? 1)), 1) + const tagPage = Math.max(Number.parseInt(String(url.searchParams.get('tag-page') ?? 1)), 1) + + const paginatedCatalogs = await loadPaginatedCatalogs(catalogPage, 15, fetch) + const paginatedTags = await loadPaginatedTags(tagPage, 15, fetch) + + return { paginatedCatalogs, paginatedTags, paginatedLinks } +} diff --git a/src/routes/(app)/links/[source]/+page.server.ts b/src/routes/(app)/links/[source]/+page.server.ts new file mode 100644 index 0000000..1f82665 --- /dev/null +++ b/src/routes/(app)/links/[source]/+page.server.ts @@ -0,0 +1,35 @@ +import type { Actions, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, redirect } from '@sveltejs/kit' + +import type { Link } from '$lib/models' +import { linkService, sessionService } from '$lib/server/services' +import { Str, t } from '$lib/tools' + +export const load: ServerLoad = async ({ params, cookies }) => { + const link = await getRequestEntity(params, cookies) + + return { link } +} + +export const actions = { + delete: async ({ cookies, params }): Promise => { + const link = await getRequestEntity(params, cookies) + + const message = await linkService.deleteLink(cookies, link.source) + + sessionService.getFlash(cookies).flashWarning( + Str.ucFirst(`${t.get('entities.list.link')} ${message.message}`.toLowerCase()), + ) + + redirect(302, '/links') + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const source = params['source'], + link = source ? await linkService.getLink(cookies, source) : null + + if (!link) error(404) + + return new Promise(resolve => resolve(link)) +} diff --git a/src/routes/(app)/links/[source]/+page.svelte b/src/routes/(app)/links/[source]/+page.svelte new file mode 100644 index 0000000..d81507c --- /dev/null +++ b/src/routes/(app)/links/[source]/+page.svelte @@ -0,0 +1,152 @@ + + + + + + + + + + +
+
+
+
+
Source
+
+
+ + {data.link.sourceAbsolute} +
+ {data.link.source} +
+
+
+
Cible
+
+ {data.link.target} +
+
+
+
Nombre de visites
+
{data.link.visits}
+
+
+
Est externe
+
{data.link.isExternal ? 'oui' : 'non'}
+
+
+
Est valide
+
{data.link.validity === Validity.unknown + ? 'non testé' + : ( + data.link.validity === Validity.valid ? 'oui' : 'non' + )}
+
+ {#if data.link.tag} +
+
{Str.ucFirst($t('entities.list.tag'))}
+
+ + + {data.link.tag.name} + + +
+
+ {/if} + {#if data.link.catalog} +
+
{Str.ucFirst($t('entities.list.catalog'))}
+
+ {data.link.catalog.name} +
+
+ {/if} +
+
Crée le
+
{Dates.niceDateTime(data.link.createdAt)}
+
+ {#if data.link.updatedAt} +
+
Mis à jour le
+
{Dates.niceDateTime(data.link.updatedAt)}
+
+ {/if} +
+
+
diff --git a/src/routes/(app)/links/[source]/edit/+page.server.ts b/src/routes/(app)/links/[source]/edit/+page.server.ts new file mode 100644 index 0000000..a42f988 --- /dev/null +++ b/src/routes/(app)/links/[source]/edit/+page.server.ts @@ -0,0 +1,54 @@ +import type { ActionFailure, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { Link } from '$lib/models' +import { linkService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions, RouteParams } from './$types' +import updateLinkRequestSchema from './updateLink.request.schema' + +export const load: ServerLoad = async ({ params, cookies }) => { + const source = params['source'], + link = source ? await linkService.getLink(cookies, source) : null + + if (!link) error(404) + + return { link } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + const link = await getRequestEntity(params, cookies) + + const z3Request = Z3Request.of(request, updateLinkRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + await linkService.updateLink(cookies, { + ...validation.validated, + ...{ source: link.source }, + }) + + const message = `${t.get('entities.list.link')} à bien été mis à jour` + locals.flashMessages = [{ message: message, type: 'success' }] + return { message } + }, +} satisfies Actions + +async function getRequestEntity(params: RouteParams, cookies: Cookies): Promise { + const source = params['source'], + link = source ? await linkService.getLink(cookies, source) : null + + if (!link) error(404) + return new Promise(resolve => resolve(link)) +} diff --git a/src/routes/(app)/links/[source]/edit/+page.svelte b/src/routes/(app)/links/[source]/edit/+page.svelte new file mode 100644 index 0000000..caa70e7 --- /dev/null +++ b/src/routes/(app)/links/[source]/edit/+page.svelte @@ -0,0 +1,193 @@ + + + + + + + + + + +
+
+
+
+ Redirection + +
+
+ Source + {data.link.source} +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ { loadCatalogs(nextPage) }} + onClickPrevious={(previousPage): void => { loadCatalogs(previousPage) }} + outerClassList={['md:w-64']} + paginatedEntityList={$paginatedCatalogs} + value={olds['catalog'] ?? data.link.catalog?.slug} + /> +
+
+ { loadTags(nextPage) }} + onClickPrevious={(previousPage): void => { loadTags(previousPage) }} + outerClassList={['md:w-64']} + externalClassList={['grow']} + paginatedEntityList={$paginatedTags} + value={olds['tag'] ?? data.link.tag?.uuid} + /> +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/links/[source]/edit/+page.ts b/src/routes/(app)/links/[source]/edit/+page.ts new file mode 100644 index 0000000..0ca3f18 --- /dev/null +++ b/src/routes/(app)/links/[source]/edit/+page.ts @@ -0,0 +1,14 @@ +import { loadPaginatedCatalogs, loadPaginatedTags } from '$lib/client/stores' +import type { PageLoad } from './$types.js' + +export const load: PageLoad = async ({ url, fetch, data }) => { + const { link } = data + + const catalogPage = Math.max(Number.parseInt(String(url.searchParams.get('catalog-page') ?? 1)), 1) + const tagPage = Math.max(Number.parseInt(String(url.searchParams.get('tag-page') ?? 1)), 1) + + const paginatedCatalogs = await loadPaginatedCatalogs(catalogPage, 15, fetch) + const paginatedTags = await loadPaginatedTags(tagPage, 15, fetch) + + return { paginatedCatalogs, paginatedTags, link } +} diff --git a/src/routes/(app)/links/[source]/edit/updateLink.request.schema.ts b/src/routes/(app)/links/[source]/edit/updateLink.request.schema.ts new file mode 100644 index 0000000..96c9408 --- /dev/null +++ b/src/routes/(app)/links/[source]/edit/updateLink.request.schema.ts @@ -0,0 +1,8 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + target: zod.string().min(1).url(), + isExternal: zod.preprocess(value => value !== undefined, zod.boolean()), + catalog: zod.string().min(1).nullable(), + tag: zod.string().uuid().nullable(), +}) diff --git a/src/routes/(app)/links/create/+page.server.ts b/src/routes/(app)/links/create/+page.server.ts new file mode 100644 index 0000000..3423332 --- /dev/null +++ b/src/routes/(app)/links/create/+page.server.ts @@ -0,0 +1,38 @@ +import type { ActionFailure } from '@sveltejs/kit' +import { fail, redirect } from '@sveltejs/kit' + +import type { FormFailure } from '$customType/forms' +import { linkService, sessionService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import createLinkRequestSchema from './createLink.request.schema' + +export const actions = { + default: async ({ request, cookies, locals }): Promise> => { + const z3Request = Z3Request.of(request, createLinkRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const link = await linkService.createLink( + cookies, + validation.validated.source, + validation.validated.target, + validation.validated.isExternal, + validation.validated.nobrand ? null : locals!.activeBrand!.uuid, + validation.validated.catalog, + validation.validated.tag, + ) + + sessionService.getFlash(cookies).flashSuccess(`${t.get('entities.list.link')} à bien été crée`) + redirect(302, `/links/${link.source}/edit`) + }, +} satisfies Actions diff --git a/src/routes/(app)/links/create/+page.svelte b/src/routes/(app)/links/create/+page.svelte new file mode 100644 index 0000000..4991f73 --- /dev/null +++ b/src/routes/(app)/links/create/+page.svelte @@ -0,0 +1,172 @@ + + + + +
+
+
+
+ Redirection + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ { loadCatalogs(nextPage) }} + onClickPrevious={(previousPage): void => { loadCatalogs(previousPage) }} + outerClassList={['w-fit md:w-64']} + paginatedEntityList={$paginatedCatalogs} + value={olds['catalog']} + /> +
+
+ { loadTags(nextPage) }} + onClickPrevious={(previousPage): void => { loadTags(previousPage) }} + outerClassList={['w-fit md:w-64']} + paginatedEntityList={$paginatedTags} + value={olds['tag']} + /> +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/links/create/+page.ts b/src/routes/(app)/links/create/+page.ts new file mode 100644 index 0000000..8db1c0e --- /dev/null +++ b/src/routes/(app)/links/create/+page.ts @@ -0,0 +1,12 @@ +import { loadPaginatedCatalogs, loadPaginatedTags } from '$lib/client/stores' +import type { PageLoad } from './$types.js' + +export const load: PageLoad = async ({ url, fetch }) => { + const catalogPage = Math.max(Number.parseInt(String(url.searchParams.get('catalog-page') ?? 1)), 1) + const tagPage = Math.max(Number.parseInt(String(url.searchParams.get('tag-page') ?? 1)), 1) + + const paginatedCatalogs = await loadPaginatedCatalogs(catalogPage, 15, fetch) + const paginatedTags = await loadPaginatedTags(tagPage, 15, fetch) + + return { paginatedCatalogs, paginatedTags } +} diff --git a/src/routes/(app)/links/create/createLink.request.schema.ts b/src/routes/(app)/links/create/createLink.request.schema.ts new file mode 100644 index 0000000..39853d8 --- /dev/null +++ b/src/routes/(app)/links/create/createLink.request.schema.ts @@ -0,0 +1,10 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + source: zod.string().min(1).nullable(), + target: zod.string().min(1), + isExternal: zod.preprocess(value => value !== undefined, zod.boolean()), + nobrand: zod.preprocess(value => value !== undefined, zod.boolean()), + catalog: zod.string().uuid().nullable(), + tag: zod.string().uuid().nullable(), +}) diff --git a/src/routes/(app)/links/export/+page.svelte b/src/routes/(app)/links/export/+page.svelte new file mode 100644 index 0000000..84b11a4 --- /dev/null +++ b/src/routes/(app)/links/export/+page.svelte @@ -0,0 +1,193 @@ + + + + + + + + + +
+
+

Vous pouvez exporter les liens de plusieurs manières :

+
+
+ + {#if activeBrand} + + {/if} + +
+ +
+ Cliquez simplement sur le bouton ci-dessous pour exporter tous les liens sans enseigne : + +
+
+ +
+
+
+ + {#if activeBrand} +
+ Cliquez simplement sur le bouton ci-dessous pour exporter tous les liens de l'enseigne actuelle: + +
+ + {Str.toTitleCase($t('entities.list.brand'))} :  + {activeBrand.name} + + + +
+
+ {/if} + +
+ Choisissez le catalogue pour lequel il faut exporter les liens : + +
+
+
+ { loadCatalogs(nextPage) }} + onClickPrevious={(previousPage): void => { loadCatalogs(previousPage) }} + outerClassList={['w-fit md:w-64']} + paginatedEntityList={$paginatedCatalogs} + required + /> +
+
+ +
+
+
+
+
+ + diff --git a/src/routes/(app)/links/export/+page.ts b/src/routes/(app)/links/export/+page.ts new file mode 100644 index 0000000..8bb3d9c --- /dev/null +++ b/src/routes/(app)/links/export/+page.ts @@ -0,0 +1,10 @@ +import { loadPaginatedCatalogs } from '$lib/client/stores' +import type { PageLoad } from './$types.js' + +export const load: PageLoad = async ({ url, fetch }) => { + const catalogPage = Math.max(Number.parseInt(String(url.searchParams.get('catalog-page') ?? 1)), 1) + + const paginatedCatalogs = await loadPaginatedCatalogs(catalogPage, 15, fetch) + + return { paginatedCatalogs } +} diff --git a/src/routes/(app)/links/export/page.svelte.ts b/src/routes/(app)/links/export/page.svelte.ts new file mode 100644 index 0000000..a264894 --- /dev/null +++ b/src/routes/(app)/links/export/page.svelte.ts @@ -0,0 +1,3 @@ +export enum ExportType { + NOBRAND, BRAND, CATALOG, +} diff --git a/src/routes/(app)/links/import/+page.server.ts b/src/routes/(app)/links/import/+page.server.ts new file mode 100644 index 0000000..324d3e4 --- /dev/null +++ b/src/routes/(app)/links/import/+page.server.ts @@ -0,0 +1,77 @@ +import { fail, type ActionFailure, type Actions } from '@sveltejs/kit' + +import type { ImportResponse } from '$customType' +import type { FormFailure } from '$customType/forms' +import { linkService, sessionService } from '$lib/server/services' +import { linkImportBrandRequest, linkImportCatalogRequest } from './linkImport.request.schema' + +export const actions = { + brands: async ({ request, cookies, locals }): Promise> => { + const formData = Object.fromEntries((await request.formData()).entries()) + const validation = linkImportBrandRequest.safeParse(formData) + const errors = validation.error?.flatten().fieldErrors ?? {} + + if (Object.values(errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: { + ...(errors.brand ? { brand: errors.brand?.join(', ') } : {}), + ...(errors.tag ? { tag: errors.tag?.join(', ') } : {}), + ...(errors.file ? { file: errors.file?.join(', ') } : {}), + }, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const results = await linkService.importBrandLinks( + cookies, + validation.data!.brand, + validation.data!.tag, + validation.data!.file, + ) + + return { + type: !results.failedImports.length ? 'success' : 'warning', + message: + (!results.failedImports.length) + ? `L'import à bien été fait (${results?.importedLinks} liens importés)` + : `L'import à bien été fait (${results?.importedLinks} liens importés) mais des lignes n´ont pu être traités`, + ...results, + } + }, + catalogs: async ({ request, cookies, locals }): Promise> => { + const formData = Object.fromEntries((await request.formData()).entries()) + const validation = linkImportCatalogRequest.safeParse(formData) + const errors = validation.error?.flatten().fieldErrors ?? {} + + if (Object.values(errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: { + ...(errors.catalog ? { brand: errors.catalog?.join(', ') } : {}), + ...(errors.tag ? { tag: errors.tag?.join(', ') } : {}), + ...(errors.file ? { file: errors.file?.join(', ') } : {}), + }, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const results = await linkService.importCatalogLinks( + cookies, + validation.data!.catalog, + validation.data!.tag, + validation.data!.file, + ) + + return { + type: !results.failedImports.length ? 'success' : 'warning', + message: + (!results.failedImports.length) + ? `L'import à bien été fait (${results?.importedLinks} liens importés)` + : `L'import à bien été fait (${results?.importedLinks} liens importés) mais des lignes n´ont pu être traités`, + ...results, + } + }, +} satisfies Actions diff --git a/src/routes/(app)/links/import/+page.svelte b/src/routes/(app)/links/import/+page.svelte new file mode 100644 index 0000000..fb92fd6 --- /dev/null +++ b/src/routes/(app)/links/import/+page.svelte @@ -0,0 +1,284 @@ + + + + + + + + + +
+
+

+ Pour importer des liens utilisez le formulaire ci-dessous. + Vous pouvez également télecharger le modèle a utiliser pour l'import.
+ + Veuillez noter que si des liens dans votre fichier possèdent la même clé Source, + leur valeur de redirection Target sera remplacée et ce dans de manière itérative. + +

+ Télécharger le fichier d'import d'exemple +
+ {#if failedImports.length} +
+ +
+ {/if} + +
+
+
+ {#if activeBrand} + + {/if} + +
+ + {#if activeBrand} +
+
+
+ + {Str.toTitleCase($t('entities.list.brand'))} :  + {activeBrand.name} + + +
+ { loadTags(nextPage) }} + onClickPrevious={(previousPage): void => { loadTags(previousPage) }} + outerClassList="w-fit md:w-64 mx-2" + paginatedEntityList={$paginatedTags} + /> +
+
+ +
+
+ +
+
+ {/if} + +
+ Choisissez le catalogue pour lequel il faut importer les liens : + +
+
+
+ { loadCatalogs(nextPage) }} + onClickPrevious={(previousPage): void => { loadCatalogs(previousPage) }} + outerClassList="w-fit md:w-64 mx-2" + paginatedEntityList={$paginatedCatalogs} + required + /> +
+
+ { loadTags(nextPage) }} + onClickPrevious={(previousPage): void => { loadTags(previousPage) }} + outerClassList="w-fit md:w-64 mx-2" + paginatedEntityList={$paginatedTags} + /> +
+
+ +
+
+ +
+
+
+
+
+ + diff --git a/src/routes/(app)/links/import/+page.ts b/src/routes/(app)/links/import/+page.ts new file mode 100644 index 0000000..8bb3d9c --- /dev/null +++ b/src/routes/(app)/links/import/+page.ts @@ -0,0 +1,10 @@ +import { loadPaginatedCatalogs } from '$lib/client/stores' +import type { PageLoad } from './$types.js' + +export const load: PageLoad = async ({ url, fetch }) => { + const catalogPage = Math.max(Number.parseInt(String(url.searchParams.get('catalog-page') ?? 1)), 1) + + const paginatedCatalogs = await loadPaginatedCatalogs(catalogPage, 15, fetch) + + return { paginatedCatalogs } +} diff --git a/src/routes/(app)/links/import/linkImport.request.schema.ts b/src/routes/(app)/links/import/linkImport.request.schema.ts new file mode 100644 index 0000000..9deb62b --- /dev/null +++ b/src/routes/(app)/links/import/linkImport.request.schema.ts @@ -0,0 +1,18 @@ +import { fileImportSchema } from '$lib/server/schemas' +import { zod } from '$lib/tools' + +export const linkImportBrandRequest = zod.object({ + brand: zod.string().min(1), + tag: zod.preprocess(value => (typeof value === 'string' && value.length) + ? value + : undefined, zod.optional(zod.string().min(1))), + file: fileImportSchema, +}) + +export const linkImportCatalogRequest = zod.object({ + catalog: zod.string().min(1), + tag: zod.preprocess(value => (typeof value === 'string' && value.length) + ? value + : undefined, zod.optional(zod.string().min(1))), + file: fileImportSchema, +}) diff --git a/src/routes/(app)/links/import/page.svelte.ts b/src/routes/(app)/links/import/page.svelte.ts new file mode 100644 index 0000000..6f5bce1 --- /dev/null +++ b/src/routes/(app)/links/import/page.svelte.ts @@ -0,0 +1,3 @@ +export enum ImportType { + BRAND, CATALOG, +} diff --git a/src/routes/(app)/links/link-statistics/+page.server.ts b/src/routes/(app)/links/link-statistics/+page.server.ts new file mode 100644 index 0000000..98f8d1e --- /dev/null +++ b/src/routes/(app)/links/link-statistics/+page.server.ts @@ -0,0 +1,15 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { linkStatisticsService } from '$lib/server/services' + +export const load: ServerLoad = async ({ locals, cookies }) => { + const { activeBrand } = locals + + let globalStatistics: Record = {} + + if (activeBrand) { + globalStatistics = await linkStatisticsService.globalStatisticsForBrand(cookies, activeBrand.uuid) + } + + return { globalStatistics } +} diff --git a/src/routes/(app)/links/link-statistics/+page.svelte b/src/routes/(app)/links/link-statistics/+page.svelte new file mode 100644 index 0000000..e290559 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/+page.svelte @@ -0,0 +1,59 @@ + + + + + diff --git a/src/routes/(app)/links/link-statistics/brands/+page.server.ts b/src/routes/(app)/links/link-statistics/brands/+page.server.ts new file mode 100644 index 0000000..a6754ef --- /dev/null +++ b/src/routes/(app)/links/link-statistics/brands/+page.server.ts @@ -0,0 +1,21 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' + +export const load: ServerLoad = async ({ locals, cookies }) => { + const { brands } = locals + + const availableBrandUuids = brands.map(b => b.uuid) + const linkStatistics: Record = {} + + // Keep only available brands + Object.entries(await linkStatisticsService.linkStatisticsForBrands(cookies)).forEach((statLineEntry) => { + if (availableBrandUuids.indexOf(statLineEntry[0]) !== -1) { + linkStatistics[statLineEntry[0]] = statLineEntry[1] + } + }) + + return { linkStatistics } +} diff --git a/src/routes/(app)/links/link-statistics/brands/+page.svelte b/src/routes/(app)/links/link-statistics/brands/+page.svelte new file mode 100644 index 0000000..3a89043 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/brands/+page.svelte @@ -0,0 +1,105 @@ + + + + +{#if chartDataSet.length} +
+
+ + + + +
+ +
+
+
+ +
+
+ +
+
+{:else} +{Str.ucFirst($t('statistics.statement.no_entity_stats', {'entity': $t('crud.inflection.of_p') + $t('entities.list.brand')}))} +{/if} diff --git a/src/routes/(app)/links/link-statistics/catalogs/+page.server.ts b/src/routes/(app)/links/link-statistics/catalogs/+page.server.ts new file mode 100644 index 0000000..58ffd16 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/catalogs/+page.server.ts @@ -0,0 +1,16 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' + +export const load: ServerLoad = async ({ locals, cookies }) => { + const { activeBrand } = locals + let linkStatistics: Record = {} + + if (activeBrand) { + linkStatistics = await linkStatisticsService.linkCatalogStatisticsFor(cookies, activeBrand.uuid) + } + + return { linkStatistics } +} diff --git a/src/routes/(app)/links/link-statistics/catalogs/+page.svelte b/src/routes/(app)/links/link-statistics/catalogs/+page.svelte new file mode 100644 index 0000000..3c214f8 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/catalogs/+page.svelte @@ -0,0 +1,104 @@ + + + + +{#if chartDataSet.length} +
+
+ + + + +
+ +
+
+
+ +
+
+ +
+
+{:else} +{Str.ucFirst($t('statistics.statement.no_entity_stats', {'entity': $t('crud.inflection.of') + ' ' + $t('entities.list.catalog')}))} +{/if} diff --git a/src/routes/(app)/links/link-statistics/tags/+page.server.ts b/src/routes/(app)/links/link-statistics/tags/+page.server.ts new file mode 100644 index 0000000..ec1ae78 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/tags/+page.server.ts @@ -0,0 +1,16 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import type { Uuid } from '$customType/value-objects' +import type { StatisticLine } from '$lib/models' +import { linkStatisticsService } from '$lib/server/services' + +export const load: ServerLoad = async ({ locals, cookies }) => { + const { activeBrand } = locals + let linkStatistics: Record = {} + + if (activeBrand) { + linkStatistics = await linkStatisticsService.linkTagStatisticsFor(cookies, activeBrand.uuid) + } + + return { linkStatistics } +} diff --git a/src/routes/(app)/links/link-statistics/tags/+page.svelte b/src/routes/(app)/links/link-statistics/tags/+page.svelte new file mode 100644 index 0000000..5f70b47 --- /dev/null +++ b/src/routes/(app)/links/link-statistics/tags/+page.svelte @@ -0,0 +1,104 @@ + + + + +{#if chartDataSet.length} +
+
+ + + + +
+ +
+
+
+ +
+
+ +
+
+{:else} +{Str.ucFirst($t('statistics.statement.no_entity_stats', {'entity': $t('crud.inflection.of_p') + $t('entities.list.tag')}))} +{/if} diff --git a/src/routes/(app)/statistics/+page.svelte b/src/routes/(app)/statistics/+page.svelte new file mode 100644 index 0000000..c7198ba --- /dev/null +++ b/src/routes/(app)/statistics/+page.svelte @@ -0,0 +1,61 @@ + + + + + + +
+
+ + + + + +
+
diff --git a/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.server.ts b/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.server.ts new file mode 100644 index 0000000..f69b79d --- /dev/null +++ b/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.server.ts @@ -0,0 +1,8 @@ +import { statisticsService } from "$lib/server/services" +import type { ServerLoad } from "@sveltejs/kit" + +export const load: ServerLoad = async ({ cookies }) => { + const statistics = (await statisticsService.getInvalidLinksByWeekAndByCatalog(cookies)) + + return { statistics } +} diff --git a/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.svelte b/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.svelte new file mode 100644 index 0000000..07c75b8 --- /dev/null +++ b/src/routes/(app)/statistics/invalid-links/per-brand-per-week/+page.svelte @@ -0,0 +1,63 @@ + + + + + diff --git a/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.server.ts b/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.server.ts new file mode 100644 index 0000000..624b637 --- /dev/null +++ b/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.server.ts @@ -0,0 +1,8 @@ +import { statisticsService } from "$lib/server/services" +import type { ServerLoad } from "@sveltejs/kit" + +export const load: ServerLoad = async ({ cookies }) => { + const statistics = (await statisticsService.topMostFrequentReferrers(cookies)) + + return { statistics } +} diff --git a/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.svelte b/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.svelte new file mode 100644 index 0000000..7afce57 --- /dev/null +++ b/src/routes/(app)/statistics/top-lists/referrer-by-brand/+page.svelte @@ -0,0 +1,74 @@ + + + + + diff --git a/src/routes/(app)/statistics/top-lists/source-by-brand/+page.server.ts b/src/routes/(app)/statistics/top-lists/source-by-brand/+page.server.ts new file mode 100644 index 0000000..af1c559 --- /dev/null +++ b/src/routes/(app)/statistics/top-lists/source-by-brand/+page.server.ts @@ -0,0 +1,8 @@ +import { statisticsService } from "$lib/server/services" +import type { ServerLoad } from "@sveltejs/kit" + +export const load: ServerLoad = async ({ cookies }) => { + const statistics = (await statisticsService.topMostVisitedSources(cookies)) + + return { statistics } +} diff --git a/src/routes/(app)/statistics/top-lists/source-by-brand/+page.svelte b/src/routes/(app)/statistics/top-lists/source-by-brand/+page.svelte new file mode 100644 index 0000000..cc66b04 --- /dev/null +++ b/src/routes/(app)/statistics/top-lists/source-by-brand/+page.svelte @@ -0,0 +1,69 @@ + + + + + diff --git a/src/routes/(app)/statistics/unique-visitors/per-brand/+page.server.ts b/src/routes/(app)/statistics/unique-visitors/per-brand/+page.server.ts new file mode 100644 index 0000000..d891a1c --- /dev/null +++ b/src/routes/(app)/statistics/unique-visitors/per-brand/+page.server.ts @@ -0,0 +1,8 @@ +import { statisticsService } from "$lib/server/services" +import type { ServerLoad } from "@sveltejs/kit" + +export const load: ServerLoad = async ({ cookies }) => { + const statistics = (await statisticsService.getUniqueVisitorsByDayAndByBrand(cookies)) + + return { statistics } +} diff --git a/src/routes/(app)/statistics/unique-visitors/per-brand/+page.svelte b/src/routes/(app)/statistics/unique-visitors/per-brand/+page.svelte new file mode 100644 index 0000000..1361799 --- /dev/null +++ b/src/routes/(app)/statistics/unique-visitors/per-brand/+page.svelte @@ -0,0 +1,63 @@ + + + + + + diff --git a/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.server.ts b/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.server.ts new file mode 100644 index 0000000..8a55834 --- /dev/null +++ b/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.server.ts @@ -0,0 +1,8 @@ +import { statisticsService } from "$lib/server/services" +import type { ServerLoad } from "@sveltejs/kit" + +export const load: ServerLoad = async ({ cookies }) => { + const statistics = (await statisticsService.getUniqueVisitorsByDayAndByCatalog(cookies)) + + return { statistics } +} diff --git a/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.svelte b/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.svelte new file mode 100644 index 0000000..a3edfc3 --- /dev/null +++ b/src/routes/(app)/statistics/unique-visitors/per-catalog/+page.svelte @@ -0,0 +1,63 @@ + + + + + diff --git a/src/routes/(app)/tags/+page.server.ts b/src/routes/(app)/tags/+page.server.ts new file mode 100644 index 0000000..f8b46b5 --- /dev/null +++ b/src/routes/(app)/tags/+page.server.ts @@ -0,0 +1,11 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { tagService } from '$lib/server/services' + +export const load: ServerLoad = async ({ url, cookies }) => { + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 15)), 1), 50) + const paginatedTags = await tagService.listTags(cookies, page, perPage) + + return { paginatedTags } +} diff --git a/src/routes/(app)/tags/+page.svelte b/src/routes/(app)/tags/+page.svelte new file mode 100644 index 0000000..aed575c --- /dev/null +++ b/src/routes/(app)/tags/+page.svelte @@ -0,0 +1,134 @@ + + + + + + +{#if data.paginatedTags.list.length > 5} +
+
+ +
+
+{/if} + +
+
+ {#if data.paginatedTags.list.length} +
+ + + + + + + + + + {#each data.paginatedTags.list as tag (tag.uuid)} + + + + + + {/each} + +
+ Nom + + Couleur + {Str.toTitleCase($t('common.action.actions'))}
{tag.name} + + +
+ + + + + +
{ + if (!await modals.open(Modal, { + title: Str.toTitleCase($t('common.action.confirm')), + message: Str.ucFirst($t('common.statement.ask_for_delete')), + confirm: true, + })) { + cancel() + } + return async ({ result }): Promise => { + applyAction(result) + } + }} + > + + +
+
+
+ {:else} + {Str.ucFirst($t('common.statement.no_entity', { entity: $t('common.pages.tags') }))} + {/if} +
+
+ +
+
+
+ +
+
diff --git a/src/routes/(app)/tags/[slug]/+page.server.ts b/src/routes/(app)/tags/[slug]/+page.server.ts new file mode 100644 index 0000000..0f083ed --- /dev/null +++ b/src/routes/(app)/tags/[slug]/+page.server.ts @@ -0,0 +1,35 @@ +import type { Actions, Cookies, Redirect, ServerLoad } from '@sveltejs/kit' +import { error, redirect } from '@sveltejs/kit' + +import type { Tag } from '$lib/models' +import { sessionService, tagService } from '$lib/server/services' +import { Str, t } from '$lib/tools' + +export const load: ServerLoad = async ({ params, cookies }) => { + const tag = await getRequestEntity(params, cookies) + + return { tag } +} + +export const actions = { + delete: async ({ cookies, params }): Promise => { + const tag = await getRequestEntity(params, cookies) + + const message = await tagService.deleteTag(cookies, tag.uuid) + + sessionService.getFlash(cookies).flashWarning( + Str.ucFirst(`${t.get('entities.list.tag')} ${message.message}`.toLowerCase()), + ) + + redirect(302, '/tags') + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + tag = slug ? await tagService.getTag(cookies, slug) : null + + if (!tag) error(404) + + return new Promise(resolve => resolve(tag)) +} diff --git a/src/routes/(app)/tags/[slug]/+page.svelte b/src/routes/(app)/tags/[slug]/+page.svelte new file mode 100644 index 0000000..7ebe34f --- /dev/null +++ b/src/routes/(app)/tags/[slug]/+page.svelte @@ -0,0 +1,87 @@ + + + + + +
+ +
+
+
+ +
+
+
+
+
UUID
+
{data.tag.uuid}
+
+
+
Nom
+
{data.tag.name}
+
+
+
Couleur
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/tags/[slug]/edit/+page.server.ts b/src/routes/(app)/tags/[slug]/edit/+page.server.ts new file mode 100644 index 0000000..6c19211 --- /dev/null +++ b/src/routes/(app)/tags/[slug]/edit/+page.server.ts @@ -0,0 +1,51 @@ +import type { ActionFailure, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { Tag } from '$lib/models' +import { sessionService, tagService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import updateTagRequestSchema from './updateTag.request.schema' + +export const load: ServerLoad = async ({ params, cookies }) => { + const tag = await getRequestEntity(params, cookies) + + return { tag } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + const tag = await getRequestEntity(params, cookies) + + const z3Request = Z3Request.of(request, updateTagRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + Z3Request.assignEntityProps(tag, validation.validated) + + await tagService.updateTag(cookies, tag) + + const message = `${t.get('entities.list.tag')} à bien été mis à jour` + locals.flashMessages = [{ message: message, type: 'success' }] + return { message } + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const slug = params['slug'], + tag = slug ? await tagService.getTag(cookies, slug) : null + + if (!tag) error(404) + + return new Promise(resolve => resolve(tag)) +} diff --git a/src/routes/(app)/tags/[slug]/edit/+page.svelte b/src/routes/(app)/tags/[slug]/edit/+page.svelte new file mode 100644 index 0000000..df5d1af --- /dev/null +++ b/src/routes/(app)/tags/[slug]/edit/+page.svelte @@ -0,0 +1,153 @@ + + + + + + +
+ +
+
+
+ +
+
+
+ Identifiants + +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/tags/[slug]/edit/updateTag.request.schema.ts b/src/routes/(app)/tags/[slug]/edit/updateTag.request.schema.ts new file mode 100644 index 0000000..60c2024 --- /dev/null +++ b/src/routes/(app)/tags/[slug]/edit/updateTag.request.schema.ts @@ -0,0 +1,7 @@ +import { hexColorSchema } from '$lib/server/schemas/utils' +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), + color: hexColorSchema, +}) diff --git a/src/routes/(app)/tags/create/+page.server.ts b/src/routes/(app)/tags/create/+page.server.ts new file mode 100644 index 0000000..20c27e4 --- /dev/null +++ b/src/routes/(app)/tags/create/+page.server.ts @@ -0,0 +1,34 @@ +import type { ActionFailure } from '@sveltejs/kit' +import { fail, redirect } from '@sveltejs/kit' + +import type { FormFailure } from '$customType/forms' +import { sessionService, tagService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import createTagRequestSchema from './createTag.request.schema' + +export const actions = { + default: async ({ request, cookies, locals }): Promise> => { + const z3Request = Z3Request.of(request, createTagRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const tag = await tagService.createTag( + cookies, + validation.validated.name, + validation.validated.color, + ) + + sessionService.getFlash(cookies).flashSuccess(`${t.get('entities.list.tag')} à bien été crée`) + redirect(302, `/tags/${tag.uuid}/edit`) + }, +} satisfies Actions diff --git a/src/routes/(app)/tags/create/+page.svelte b/src/routes/(app)/tags/create/+page.svelte new file mode 100644 index 0000000..74b9f89 --- /dev/null +++ b/src/routes/(app)/tags/create/+page.svelte @@ -0,0 +1,95 @@ + + + + +
+
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/tags/create/createTag.request.schema.ts b/src/routes/(app)/tags/create/createTag.request.schema.ts new file mode 100644 index 0000000..60c2024 --- /dev/null +++ b/src/routes/(app)/tags/create/createTag.request.schema.ts @@ -0,0 +1,7 @@ +import { hexColorSchema } from '$lib/server/schemas/utils' +import { zod } from '$lib/tools' + +export default zod.object({ + name: zod.string().min(1), + color: hexColorSchema, +}) diff --git a/src/routes/(app)/users/+page.server.ts b/src/routes/(app)/users/+page.server.ts new file mode 100644 index 0000000..2a7f2b2 --- /dev/null +++ b/src/routes/(app)/users/+page.server.ts @@ -0,0 +1,11 @@ +import type { ServerLoad } from '@sveltejs/kit' + +import { userService } from '$lib/server/services' + +export const load: ServerLoad = async ({ url, cookies }) => { + const page = Math.max(Number.parseInt(String(url.searchParams.get('page') ?? 1)), 1) + const perPage = Math.min(Math.max(Number.parseInt(String(url.searchParams.get('per-page') ?? 15)), 1), 50) + const paginatedUsers = await userService.listUsers(cookies, page, perPage) + + return { paginatedUsers } +} diff --git a/src/routes/(app)/users/+page.svelte b/src/routes/(app)/users/+page.svelte new file mode 100644 index 0000000..87027bf --- /dev/null +++ b/src/routes/(app)/users/+page.svelte @@ -0,0 +1,144 @@ + + + +{#if can('user:create', data.auth)} + + +{/if} + + +{#if data.paginatedUsers.list.length > 5} +
+
+ +
+
+{/if} + +
+
+ {#if data.paginatedUsers.list.length} +
+ + + + + + + + + + + + {#each data.paginatedUsers.list as user (user.uuid)} + + + + + + + + {/each} + +
+ Role + + Prénom + + Nom + + E-mail + {Str.toTitleCase($t('common.action.actions'))}
{user.firstName}{user.lastName}{user.email} +
+ {#if can('user:view', data.auth)} + + + {/if} + {#if can('user:update', data.auth)} + + + {/if} + {#if can('user:delete', data.auth) && data.auth.uuid !== user.uuid} +
{ + if (!await modals.open(Modal, { + title: Str.toTitleCase($t('common.action.confirm')), + message: Str.ucFirst($t('common.statement.ask_for_delete')), + confirm: true, + })) { + cancel() + } + return async ({ result }): Promise => { + applyAction(result) + } + }} + > + + + {/if} +
+
+
+ {:else} + {Str.ucFirst($t('common.statement.no_entity', { entity: $t('common.pages.users') }))} + {/if} +
+
+ +
+
+
+ +
+
diff --git a/src/routes/(app)/users/[id]/+page.server.ts b/src/routes/(app)/users/[id]/+page.server.ts new file mode 100644 index 0000000..d7f0fd6 --- /dev/null +++ b/src/routes/(app)/users/[id]/+page.server.ts @@ -0,0 +1,50 @@ +import type { ActionFailure, Actions, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail, redirect } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { User } from '$lib/models' +import { sessionService, userService } from '$lib/server/services' +import { isValidUuid, Str, t } from '$lib/tools' + +export const load: ServerLoad = async ({ params, cookies }) => { + const user = await getRequestEntity(params, cookies) + + return { user } +} + +export const actions = { + delete: async ({ cookies, params }): Promise => { + const user = await getRequestEntity(params, cookies) + + const message = await userService.deleteUser(cookies, user.uuid) + + sessionService.getFlash(cookies).flashWarning( + Str.ucFirst(`${t.get('entities.list.user')} ${message.message}`.toLowerCase()), + ) + + redirect(302, '/users') + }, + reset: async ({ params, locals, cookies }): Promise> => { + const user = await getRequestEntity(params, cookies) + + if (!await userService.resetUserPassword(cookies, user.uuid)) { + const message = 'Echec de la réinitialisation du mot de passe' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(500, { message }) + } + + const message = `Le mot de passe de cet utilisateur à été réinitialisé, il va recevoir un message avec ses nouveaux identifiants` + locals.flashMessages = [{ message: message, type: 'info' }] + return { message } + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const id = params['id'], + uuid = (id && isValidUuid(id)) ? id : null, + user = uuid ? await userService.getUser(cookies, uuid) : null + + if (!user) error(404) + + return new Promise(resolve => resolve(user)) +} diff --git a/src/routes/(app)/users/[id]/+page.svelte b/src/routes/(app)/users/[id]/+page.svelte new file mode 100644 index 0000000..d2eb463 --- /dev/null +++ b/src/routes/(app)/users/[id]/+page.svelte @@ -0,0 +1,139 @@ + + + + + {#if can('user:setLinksAlerts', data.auth)} + + + {/if} + {#if can('user:update', data.auth)} + + {/if} + {#if can('user:delete', data.auth) && data.auth.uuid !== data.user.uuid} +
+ +
+ {/if} +
+
+ +
+
+
+
+
UUID
+
{data.user.uuid}
+
+
+
Nom
+
{data.user.lastName}
+
+
+
Prénom
+
{data.user.firstName}
+
+
+
Email
+
{data.user.email}
+
+
+
Role
+
+
+
+
+ {#if can('user:reset', data.auth) && data.auth.uuid !== data.user.uuid} +
+
+
+

Réinitialisation du mot de passe

+

+ Cliquez sur le bouton ci-dessous pour réinitialiser le mot de passe de cet utilisateur.
+ Il recevra un email contenant ses nouveau identifiants. +

+
+
+ +
+
+
+
+
+ {/if} +
diff --git a/src/routes/(app)/users/[id]/alerts/+page.server.ts b/src/routes/(app)/users/[id]/alerts/+page.server.ts new file mode 100644 index 0000000..15cd8ff --- /dev/null +++ b/src/routes/(app)/users/[id]/alerts/+page.server.ts @@ -0,0 +1,65 @@ +import type { ActionFailure, Actions, Cookies, ServerLoad } from '@sveltejs/kit' +import { error, fail } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { Uuid } from '$customType/value-objects' +import { Role } from '$lib/enums' +import type { Brand, User } from '$lib/models' +import { brandService, reportingSubscriptionService, sessionService, userService } from '$lib/server/services' +import { isValidUuid } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import updateReportingSubscriptionsSchema from './updateReportingSubscriptions.schema' + +export const load: ServerLoad = async ({ params, cookies, locals }) => { + const user = await getRequestEntity(params, cookies) + + const brandsUuids = (await reportingSubscriptionService.getReportingSubscriptions(cookies, user.uuid)).map(r => r.brand) + const userAvailableBrands + = user.role === Role.ADMINISTRATOR + ? locals.brands + : await getBrandsFromUuids(cookies, (user.brands ?? [])) + + return { user, brands: userAvailableBrands, selectedBrands: brandsUuids } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + const user = await getRequestEntity(params, cookies) + + const z3Request = Z3Request.of(request, updateReportingSubscriptionsSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const reponseMessage = await reportingSubscriptionService.updateUserReportingSubscriptions(cookies, user.uuid, validation.validated.brands) + + locals.flashMessages = [{ message: reponseMessage.message, type: 'success' }] + return { message: reponseMessage.message } + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const id = params['id'], + uuid = (id && isValidUuid(id)) ? id : null, + user = uuid ? await userService.getUser(cookies, uuid) : null + + if (!user) error(404) + return new Promise(resolve => resolve(user)) +} + +async function getBrandsFromUuids(cookies: Cookies, brandsUuids: Uuid[]): Promise { + const brands = new Map(); + + (await brandService.getAllBrands(cookies)).forEach(brand => brands.set(brand.uuid, brand)) + + return brandsUuids + .map(brandUuid => brands.get(brandUuid)) + .filter(v => v !== undefined) +} diff --git a/src/routes/(app)/users/[id]/alerts/+page.svelte b/src/routes/(app)/users/[id]/alerts/+page.svelte new file mode 100644 index 0000000..9c1bd60 --- /dev/null +++ b/src/routes/(app)/users/[id]/alerts/+page.svelte @@ -0,0 +1,100 @@ + + + + +
+
+
+ Alertes de liens invalides + +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/users/[id]/alerts/updateReportingSubscriptions.schema.ts b/src/routes/(app)/users/[id]/alerts/updateReportingSubscriptions.schema.ts new file mode 100644 index 0000000..de1c01a --- /dev/null +++ b/src/routes/(app)/users/[id]/alerts/updateReportingSubscriptions.schema.ts @@ -0,0 +1,8 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + brands: zod.preprocess( + value => value !== undefined ? value : [], + zod.array(zod.string().min(1).uuid()), + ), +}) diff --git a/src/routes/(app)/users/[id]/edit/+page.server.ts b/src/routes/(app)/users/[id]/edit/+page.server.ts new file mode 100644 index 0000000..6c60dbb --- /dev/null +++ b/src/routes/(app)/users/[id]/edit/+page.server.ts @@ -0,0 +1,51 @@ +import { error, fail, type ActionFailure, type Cookies, type ServerLoad } from '@sveltejs/kit' + +import type { FormFailure, FormSuccess } from '$customType/forms' +import type { User } from '$lib/models' +import { sessionService, userService } from '$lib/server/services' +import { isValidUuid, t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import updateUserRequestSchema from './updateUser.request.schema' + +export const load: ServerLoad = async ({ params, cookies }) => { + const user = await getRequestEntity(params, cookies) + + return { user } +} + +export const actions = { + default: async ({ request, params, cookies, locals }): Promise> => { + const user = await getRequestEntity(params, cookies) + + const z3Request = Z3Request.of(request, updateUserRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + Z3Request.assignEntityProps(user, validation.validated) + + await userService.updateUser(cookies, user) + + const message = `${t.get('entities.list.user')} à bien été mis à jour` + locals.flashMessages = [{ message: message, type: 'success' }] + return { message } + }, +} satisfies Actions + +async function getRequestEntity(params: Partial>, cookies: Cookies): Promise { + const id = params['id'], + uuid = (id && isValidUuid(id)) ? id : null, + user = uuid ? await userService.getUser(cookies, uuid) : null + + if (!user) error(404) + + return new Promise(resolve => resolve(user)) +} diff --git a/src/routes/(app)/users/[id]/edit/+page.svelte b/src/routes/(app)/users/[id]/edit/+page.svelte new file mode 100644 index 0000000..31303d9 --- /dev/null +++ b/src/routes/(app)/users/[id]/edit/+page.svelte @@ -0,0 +1,219 @@ + + + + + {#if can('user:setLinksAlerts', data.auth)} + + + {/if} + {#if can('user:view', data.auth)} + + + {/if} + {#if can('user:delete', data.auth) && data.auth.uuid !== data.user.uuid} +
+ +
+ {/if} +
+
+ +
+
+
+ Identifiants + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(app)/users/[id]/edit/updateUser.request.schema.ts b/src/routes/(app)/users/[id]/edit/updateUser.request.schema.ts new file mode 100644 index 0000000..5fc1ec1 --- /dev/null +++ b/src/routes/(app)/users/[id]/edit/updateUser.request.schema.ts @@ -0,0 +1,11 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + firstName: zod.string().min(1), + lastName: zod.string().min(1), + email: zod.string().min(1), + brands: zod.preprocess( + value => value !== undefined ? value : [], + zod.array(zod.string().min(1).uuid()), + ), +}) diff --git a/src/routes/(app)/users/create/+page.server.ts b/src/routes/(app)/users/create/+page.server.ts new file mode 100644 index 0000000..f3948cc --- /dev/null +++ b/src/routes/(app)/users/create/+page.server.ts @@ -0,0 +1,38 @@ +import type { ActionFailure } from '@sveltejs/kit' +import { fail, redirect } from '@sveltejs/kit' + +import type FormFailure from '$customType/forms/FormFailure.type' +import { sessionService, userService } from '$lib/server/services' +import { t } from '$lib/tools' +import { Z3Request } from '$lib/tools/request.tools' +import type { Actions } from './$types' +import createUserRequestSchema from './createUser.request.schema' + +export const actions = { + default: async ({ request, cookies, locals }): Promise> => { + const z3Request = Z3Request.of(request, createUserRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { + message, errors: validation.errors, + olds: sessionService.getFlashOlds(cookies).consumeOlds(), + }) + } + + const user = await userService.createUser( + cookies, + validation.validated.firstName, + validation.validated.lastName, + validation.validated.email, + validation.validated.role, + validation.validated.login, + validation.validated.brands, + ) + + sessionService.getFlash(cookies).flashSuccess(`${t.get('entities.list.user')} à bien été crée`) + redirect(302, `/users/${user.uuid}/edit`) + }, +} satisfies Actions diff --git a/src/routes/(app)/users/create/+page.svelte b/src/routes/(app)/users/create/+page.svelte new file mode 100644 index 0000000..e85e2b3 --- /dev/null +++ b/src/routes/(app)/users/create/+page.svelte @@ -0,0 +1,169 @@ + + + + +
+
+
+
+ Administration + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ Informations + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/routes/(app)/users/create/createUser.request.schema.ts b/src/routes/(app)/users/create/createUser.request.schema.ts new file mode 100644 index 0000000..2e00547 --- /dev/null +++ b/src/routes/(app)/users/create/createUser.request.schema.ts @@ -0,0 +1,14 @@ +import { Role } from '$lib/enums' +import { zod } from '$lib/tools' + +export default zod.object({ + firstName: zod.string().min(1), + lastName: zod.string().min(1), + email: zod.string().min(1), + role: zod.nativeEnum(Role), + login: zod.string().min(1), + brands: zod.preprocess( + value => value !== undefined ? value : [], + zod.array(zod.string().min(1).uuid()), + ), +}) diff --git a/src/routes/(auth)/+layout.server.ts b/src/routes/(auth)/+layout.server.ts new file mode 100644 index 0000000..1029201 --- /dev/null +++ b/src/routes/(auth)/+layout.server.ts @@ -0,0 +1,39 @@ +import { redirect } from '@sveltejs/kit' + +import { sessionService } from '$lib/server/services' +import { defaultLocale, loadTranslations, locales, translations } from '$lib/tools' +import type { LayoutServerLoad } from './$types' + +export const load: LayoutServerLoad = async ({ url, cookies, request, locals }) => { + const { pathname } = url + + if (locals.session) { + return redirect(302, '/') + } + + // Try to get the locale from cookie + let locale = cookies.get('lang')?.toLowerCase() + + // Get user preferred locale + if (!locale) { + const match = /[a-zA-Z]+?(?=-|_|,|;)/.exec((`${request.headers.get('accept-language')}`))?.[0] + locale = match?.toLowerCase() + } + + // Get defined locales + const supportedLocales = locales.get().map(l => l.toLowerCase()) + + // Use default locale if current locale is not supported + if (!locale || !supportedLocales.includes(locale)) { + locale = defaultLocale + } + + await loadTranslations(locale, pathname) // keep this just before the `return` + + return { + errors: sessionService.getFlashErrors(cookies).consumeErrors(), + messages: sessionService.getFlash(cookies).consumeMessages(), + i18n: { locale, route: pathname }, + translations: translations.get(), + } +} diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 0000000..ba5f1d6 --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,60 @@ + + + + {$title} + + +
+ {#if data.messages?.length} +
+ {#each flashMessages as message, index(index)} +
+ onClosedFlashMessage(index)} + /> +
+ {/each} +
+ {/if} + {@render children()} +
+ +
+ +
+ + diff --git a/src/routes/(auth)/+layout.ts b/src/routes/(auth)/+layout.ts new file mode 100644 index 0000000..eaaae7a --- /dev/null +++ b/src/routes/(auth)/+layout.ts @@ -0,0 +1,14 @@ +import { addTranslations, setLocale, setRoute } from '$lib/tools' +import type { LayoutLoad } from './$types' + +export const load: LayoutLoad = async ({ data }) => { + const { i18n, translations, messages, errors } = data + const { locale, route } = i18n + + addTranslations(translations) + + await setRoute(route) + await setLocale(locale) + + return { i18n, messages, errors } +} diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts new file mode 100644 index 0000000..951e3b9 --- /dev/null +++ b/src/routes/(auth)/login/+page.server.ts @@ -0,0 +1,43 @@ +import type { ActionFailure, Actions } from '@sveltejs/kit' +import { error, fail, redirect } from '@sveltejs/kit' +import { RateLimiter } from 'sveltekit-rate-limiter/server' + +import type { FormFailure } from '$customType/forms' +import { authService } from '$lib/server/services' +import { Z3Request } from '$lib/tools/request.tools' +import loginRequestSchema from './login.request.schema' + +const limiter = new RateLimiter({ + // A rate is defined as [number, unit] + IP: [10, 'h'], // IP address limiter + IPUA: [5, 'm'], // IP + User Agent limiter +}) + +export const actions = { + default: async (event): Promise> => { + if (await limiter.isLimited(event)) error(429) + + const { request, cookies, locals } = event + + const auth$ = authService + + const z3Request = Z3Request.of(request, loginRequestSchema) + const validation = await z3Request.validated() + + if (Object.values(validation.errors).length) { + const message = 'Des données n\'ont pas pu être validés' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(422, { message, errors: validation.errors }) + } + + const sessionId = await auth$.authenticate(cookies, validation.validated.login, validation.validated.password) + + if (sessionId === false) { + const message = 'Identifiants invalides' + locals.flashMessages = [{ message: message, type: 'warning' }] + return fail(401, { message }) + } + + redirect(302, '/') + }, +} satisfies Actions diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte new file mode 100644 index 0000000..d4f29ad --- /dev/null +++ b/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,125 @@ + + +
+ + +
+ + diff --git a/src/routes/(auth)/login/login.request.schema.ts b/src/routes/(auth)/login/login.request.schema.ts new file mode 100644 index 0000000..ac658f5 --- /dev/null +++ b/src/routes/(auth)/login/login.request.schema.ts @@ -0,0 +1,6 @@ +import { zod } from '$lib/tools' + +export default zod.object({ + login: zod.string().min(1), + password: zod.string().min(1), +}) diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts new file mode 100644 index 0000000..88c1127 --- /dev/null +++ b/src/routes/(auth)/logout/+page.server.ts @@ -0,0 +1,18 @@ +import type { Actions } from '@sveltejs/kit' +import { redirect } from '@sveltejs/kit' + +import type { UserSessionId } from '$customType' +import { authService, sessionService } from '$lib/server/services' + +export const actions = { + default: async ({ cookies }): Promise => { + const sessionId: UserSessionId | undefined = cookies.get('sessionId') + + if (sessionId) { + authService.disconnect(sessionId) + sessionService.getFlash(cookies).flashInfo('Vous avez éte deconnecté') + } + + return redirect(301, '/') + }, +} satisfies Actions diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..3c2d27c --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,53 @@ + + + + {preventDisplay ? 'redirection' : $title} + + +{#if !preventDisplay} +
+
+
+ {#if status == 404} + + {:else} + + {/if} +
+
+
+{/if} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..bba0051 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + +{@render children()} diff --git a/src/types/FailedImports.type.ts b/src/types/FailedImports.type.ts new file mode 100644 index 0000000..ecff20c --- /dev/null +++ b/src/types/FailedImports.type.ts @@ -0,0 +1,4 @@ +export type ImportResponse = { + failedImports: string[] + importedLinks: number +} & Record diff --git a/src/types/FlashMessage.type.ts b/src/types/FlashMessage.type.ts new file mode 100644 index 0000000..295380c --- /dev/null +++ b/src/types/FlashMessage.type.ts @@ -0,0 +1,6 @@ +export interface FlashMessage { + type: 'default' | 'success' | 'info' | 'warning' | 'error' + message: string + /** An optionnal destination url */ + at?: string +} diff --git a/src/types/JsonValue.type.ts b/src/types/JsonValue.type.ts new file mode 100644 index 0000000..3ccc617 --- /dev/null +++ b/src/types/JsonValue.type.ts @@ -0,0 +1,35 @@ +// https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 +// https://github.com/sindresorhus/type-fest/blob/HEAD/source/basic.d.ts + +/** +Matches a JSON object. + +This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. Don't use this as a direct return type as the user would have to double-cast it: `jsonObject as unknown as CustomResponse`. Instead, you could extend your CustomResponse type from it to ensure your type only uses JSON-compatible types: `interface CustomResponse extends JsonObject { … }`. + +@category JSON +*/ +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +export type JsonObject = { [Key in string]: JsonValue } & { [Key in string]?: JsonValue | undefined } + +/** +Matches a JSON array. + +@category JSON +*/ +export type JsonArray = JsonValue[] | readonly JsonValue[] + +/** +Matches any valid JSON primitive value. + +@category JSON +*/ +export type JsonPrimitive = string | number | boolean | null + +/** +Matches any valid JSON value. + +@see `Jsonify` if you need to transform a type to one that is assignable to `JsonValue`. + +@category JSON +*/ +export type JsonValue = JsonPrimitive | JsonObject | JsonArray diff --git a/src/types/PinoLogger.type.ts b/src/types/PinoLogger.type.ts new file mode 100644 index 0000000..ccbf27b --- /dev/null +++ b/src/types/PinoLogger.type.ts @@ -0,0 +1,21 @@ +import { type Logger, type LoggerOptions } from 'pino' + +export enum ServerEnvironment { + DEV, + PREV, + STG, + PROD, +} + +export enum LogLevel { + SILENT = 'silent', + TRACE = 'trace', + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', +} + +export type PinoLogger = Logger & { + setLogLevel?: (NODE_ENV: ServerEnvironment) => LoggerOptions['level'] +} diff --git a/src/types/Policies.type.ts b/src/types/Policies.type.ts new file mode 100644 index 0000000..7b3363f --- /dev/null +++ b/src/types/Policies.type.ts @@ -0,0 +1,7 @@ +import type User from '$lib/models/User.type' + +export type Policy = (auth: User) => boolean + +export interface Policies { + getPolicies(): Record +} diff --git a/src/types/ServerFieldsErrors.type.ts b/src/types/ServerFieldsErrors.type.ts new file mode 100644 index 0000000..b66e579 --- /dev/null +++ b/src/types/ServerFieldsErrors.type.ts @@ -0,0 +1 @@ +export type ServerFieldsErrors = Record diff --git a/src/types/UserSessionId.type.ts b/src/types/UserSessionId.type.ts new file mode 100644 index 0000000..cc4cfb0 --- /dev/null +++ b/src/types/UserSessionId.type.ts @@ -0,0 +1,3 @@ +import type { Uuid } from './value-objects/Uuid.type' + +export type UserSessionId = Uuid diff --git a/src/types/ZodSchema.type.ts b/src/types/ZodSchema.type.ts new file mode 100644 index 0000000..db8c7c2 --- /dev/null +++ b/src/types/ZodSchema.type.ts @@ -0,0 +1,3 @@ +import type { z, ZodRawShape } from 'zod' + +export type ZodSchema = z.ZodObject diff --git a/src/types/forms/FormFailure.type.ts b/src/types/forms/FormFailure.type.ts new file mode 100644 index 0000000..a2fddd4 --- /dev/null +++ b/src/types/forms/FormFailure.type.ts @@ -0,0 +1,8 @@ +import type { FormFieldsError } from './FormFieldsError.type' +import type { FormFieldsOlds } from './FormFieldsOlds.type' + +export default interface FormFailure extends Record { + message?: string + errors?: FormFieldsError + olds?: FormFieldsOlds +} diff --git a/src/types/forms/FormFieldsError.type.ts b/src/types/forms/FormFieldsError.type.ts new file mode 100644 index 0000000..1094e9d --- /dev/null +++ b/src/types/forms/FormFieldsError.type.ts @@ -0,0 +1 @@ +export type FormFieldsError = Record diff --git a/src/types/forms/FormFieldsOlds.type.ts b/src/types/forms/FormFieldsOlds.type.ts new file mode 100644 index 0000000..35c1ca2 --- /dev/null +++ b/src/types/forms/FormFieldsOlds.type.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type FormFieldsOlds = Partial> diff --git a/src/types/forms/FormSuccess.type.ts b/src/types/forms/FormSuccess.type.ts new file mode 100644 index 0000000..826494a --- /dev/null +++ b/src/types/forms/FormSuccess.type.ts @@ -0,0 +1,3 @@ +export default interface FormSuccess extends Record { + message?: string +} diff --git a/src/types/forms/index.ts b/src/types/forms/index.ts new file mode 100644 index 0000000..dccd633 --- /dev/null +++ b/src/types/forms/index.ts @@ -0,0 +1,11 @@ +import type FormFailure from './FormFailure.type' +import type { FormFieldsError } from './FormFieldsError.type' +import type { FormFieldsOlds } from './FormFieldsOlds.type' +import type FormSuccess from './FormSuccess.type' + +export type { + FormFailure, + FormFieldsError, + FormFieldsOlds, + FormSuccess, +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..d678635 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,20 @@ +import type { ImportResponse } from './FailedImports.type' +import type { FlashMessage } from './FlashMessage.type' +import type { JsonValue } from './JsonValue.type' +import type { PinoLogger } from './PinoLogger.type' +import type { Policies, Policy } from './Policies.type' +import type { ServerFieldsErrors } from './ServerFieldsErrors.type' +import type { UserSessionId } from './UserSessionId.type' +import type { ZodSchema } from './ZodSchema.type' + +export type { + FlashMessage, + ImportResponse, + JsonValue, + PinoLogger, + Policies, + Policy, + ServerFieldsErrors, + UserSessionId, + ZodSchema, +} diff --git a/src/types/value-objects/HexColor.type.ts b/src/types/value-objects/HexColor.type.ts new file mode 100644 index 0000000..843ab31 --- /dev/null +++ b/src/types/value-objects/HexColor.type.ts @@ -0,0 +1 @@ +export type HexColor = string diff --git a/src/types/value-objects/Slug.type.ts b/src/types/value-objects/Slug.type.ts new file mode 100644 index 0000000..787c45f --- /dev/null +++ b/src/types/value-objects/Slug.type.ts @@ -0,0 +1 @@ +export type Slug = string diff --git a/src/types/value-objects/Source.type.ts b/src/types/value-objects/Source.type.ts new file mode 100644 index 0000000..3fa3a52 --- /dev/null +++ b/src/types/value-objects/Source.type.ts @@ -0,0 +1,2 @@ +/** A slugged string */ +export type Source = string diff --git a/src/types/value-objects/Url.type.ts b/src/types/value-objects/Url.type.ts new file mode 100644 index 0000000..a77071a --- /dev/null +++ b/src/types/value-objects/Url.type.ts @@ -0,0 +1,2 @@ +/** A value url */ +export type Url = string diff --git a/src/types/value-objects/Uuid.type.ts b/src/types/value-objects/Uuid.type.ts new file mode 100644 index 0000000..3bf240f --- /dev/null +++ b/src/types/value-objects/Uuid.type.ts @@ -0,0 +1 @@ +export type Uuid = string diff --git a/src/types/value-objects/index.ts b/src/types/value-objects/index.ts new file mode 100644 index 0000000..95c62cc --- /dev/null +++ b/src/types/value-objects/index.ts @@ -0,0 +1,11 @@ +import type { HexColor } from './HexColor.type' +import type { Slug } from './Slug.type' +import type { Source } from './Source.type' +import type { Url } from './Url.type' +import type { Uuid } from './Uuid.type' + +export type { + HexColor, + Slug, + Source, Url, Uuid, +} diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..12fbedb Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/assets/help/brands.md b/static/assets/help/brands.md new file mode 100644 index 0000000..a2ca1ba --- /dev/null +++ b/static/assets/help/brands.md @@ -0,0 +1,47 @@ +# Les Enseignes + +[Retour au sommaire](/help) + +Pour gérer les enseignes vous pouvez vous rendre sur [la page de gestion des enseignes](/brands) (pour les administrateurs). + +## Fonctionnement + +Chaque enseigne est indépendante. En tant qu'administrateur toutes les enseignes et leur contenu sont accessibles. +En tant qu'utilisateur il vous est assigné en principe au moins une enseigne. +**A noter, les liens sans enseignes sont accessibles à tous le monde** + +Il est possible d'ajouter une enseigne mais pas de la supprimer. + +Chaque enseigne peut possèder les élements suivants : + +- Des liens +- Des catalogues + +## Les champs + +- Nom : Sans incidence, c'est un nom qui permet d'identifier visuellement l'enseigne dans le backoffice. +- FQDNS : La liste des nom de domaines associés à l'enseigne. Il peut y en avoir `0` ou `N`. + +## Ajouter un `FQDN` ou `Nom de domaine` + +**Modifier un FQDN aura une incidence sur la résolution des liens.** +Pour ajouter un nom de domaine sur une enseigne vous pouvez utiliser le backoffice pour directement l'ajouter. +Vous devrez cependant effectuer une étape suplémentaire si vous voulez que vos liens puissent être résolue avec nom de domaine. +Il vous foudra l'ajouter dans la zone DNS qui possède l'adresse du resolveur de Z3 ou ansi pouvoir proposer des liens comme . + +Concrètement vous aurez dans votre zone DNS possiblement une ou deux entrée : + +```dns +mon-nom-de-domaine.com 3600 IN A 207.124.120.25 +mon-nom-de-domaine.com 3600 IN AAAA 2001:db8:1234::1 +``` + +Il vous faudra ajouter quelque chose comme : + +```dns +short.mon-nom-de-domaine.com. 3600 IN CNAME adresse-du-resolveur.z3. +``` + +Ici rediriger `short.mon-nom-de-domaine.com.` vers `adresse-du-resolveur.z3.` mais le navigateur utilisera bien `short.mon-nom-de-domaine.com.`. + +Il faudra également possiblement configurer le serveur Z3 pour qu'il redirige les requêtes vers le resolver. diff --git a/static/assets/help/import.md b/static/assets/help/import.md new file mode 100644 index 0000000..a5980b4 --- /dev/null +++ b/static/assets/help/import.md @@ -0,0 +1,63 @@ +# Importer un fichier + +[Retour au sommaire](/help) + +Pour importer un fichier vous pouvez vous rendre sur [la page d'import de fichiers](/links/import). + +**Note : Il est important de savoir qu'il vous sera impossible d'écraser un lien appartenant à une autre enseigne.** + +## L'import de manière général + +Pour connaitre le format attendu vous pouvez télecharger le fichier d'exemple avec le bouton dédié à cet effet. + +Voici un exemple : + +| source | target | isExternal | +| ------------------- | ------------------------------------------------------- | ---------- | +| promos-de-septembre | | false | +| promo-de-juin | | 1 | +| promos-de-juillet | | true | +| Promo d'août | | 0 | +| | | 0 | + +### `source` + +Comme on peut le voir la colonne `source` offre plusieurs possibilités : + +- Mettre du texte quelconque, il sera transformé par ex : `Promo d'août` deviendras `promo-d-aout` +- Ne rien mettre et dans ce cas une valeur aléatoire sera utilisé par exemple `4o2il2bo7g8psavudukcdql8vj` + +### `target` + +Dans cette colonne il faut impérativement une url en **`https`**. Elle peut contenir n'importe quoi du moment que cela reste une url valide. Par exemple `https://example.net/promo?toto=1#ancre` + +### `isExternal` + +Cette colonne doit être renseigné. +Cela permet de dire si le lien est un lien interne au groupe ou un lien externe, par exemple qui redirige vers `google` etc. + +Les valeurs possible sont : + +- Pour oui : `1`, `true`, `TRUE` +- Pour non : `0`, `false`, `FALSE` + +### Ce qui'il faut savoir + +- L'import fonctionne par écrasement : + - Si un lien n'existe pas il est crée + - Si un lien existe il est mis à jour (`isExternal`, `Catalogue`, `Étiquette`) + - Il n'est pas possible d'écraser un lien d'une autre enseigne. +- Tout le fichier est traité et les erreurs (liens non importés sont affichés à la fin du traitement) + - Ceci permet de rejouer un même fichier autant de fois que vous le souhaitez, ou de conserver plusieurs fichiers correspondant à vos catalogues et par enseignes. + +## Importer par enseigne + +Pour sélectionner une enseigne, utiliser le dropdown en haut à gauche de l'écran. Si vous ne pouvez le faire, ce que vous avez accès à une seul enseigne. +Dans tous les cas l'enseigne courante serà affiché dans l'encart d'import. + +Il vous est possible de choisir l'étiquette qui sera assigné à tous les liens importés pour l'enseigne. + +## Import par catalogue + +Choisissez pour quel catalogue l'import devras se faire, de même avec les étiquettes. +Tous les liens importés seront assignés à ceux catalogue et cette étiquette, y compris les liens qui auront été mis à jours. diff --git a/static/assets/help/index.md b/static/assets/help/index.md new file mode 100644 index 0000000..a241281 --- /dev/null +++ b/static/assets/help/index.md @@ -0,0 +1,93 @@ +# Aide de Z3 + +Bienvenue sur l'aide de Z3. C'est une application qui sert à gérer des liens racourcis. + +## Sommaire + +1. [Importer un lien](/help/import) +2. [Les Enseignes](/help/brands) +3. [Résoudre un lien](/help/resolve) +4. [Comptage des visites sur les liens](/help/visits) +5. [Statistiques de l'application](/help/statistics) + +## Description de Z3 + +- [Aide de Z3](#aide-de-z3) + - [Sommaire](#sommaire) + - [Description de Z3](#description-de-z3) + - [Objets importants](#objets-importants) + - [Les `Liens`](#les-liens) + - [La `Source`](#la-source) + - [La `Cible`](#la-cible) + - [Une `Enseigne`](#une-enseigne) + - [Un `Catalogue`](#un-catalogue) + - [Un `FQDN`](#un-fqdn) + +## Objets importants + +### Les `Liens` + +Chaque lien est constitué d'au moins 2 élements : + +- La `Source` +- La `Cible` + +Chaque lien peut appartenir ou non à une `Enseigne`. +Chaque lien peut appartenir ou non à un `Catalogue`. +Chaque lien peut appartenir ou non à une `Étiquette`. + +*Un lien qui n'appartient à aucune `Enseigne` est disponible pour tous le monde à travers l'application.* + +En tant qu'`Utilisateur` un ou plusieurs `Enseigne` vous sont attribués. + +Pour chaque `Enseigne` il vous est possbile de créer `Catalogue` et `Lien`. + +Vous pouvez également créer des `Étiquettes` et y associer des `Liens`. Sachez que ces `Étiquettes` sont partagés au travers de toute l'application. + +### La `Source` + +Elle est consitué d'un chaine de caractères sans espaces et contenant des tirets ou charactères alpha numériques. + +Elle peut ête généré automatiquement ou renseigné. +**Elle est unique dans toute l'application**, ce qui veut dire que deux `Enseignes` ne peuvent avoir de lien avec la même `Source`. + +### La `Cible` + +Ell contient une url en **https**, ce qui veux dire une page web accessible par internet en mode "sécurisé". + +La cible ne peut contenir autre chose, des exemples valides sont : + +- `http://mon-ecommerce.com/produits/four-electrique` +- `http://mon-ecommerce.com/produits/four-electrique?version=rouge` +- - `http://mon-ecommerce.com/produits/four-electrique#version-rouge` + +### Une `Enseigne` + +En tant qu'utilisateur une ou plusieurs `Enseigne` vous sont attribués. + +Il vous est possible de gérer les `Liens` et `Catalogues` de ces enseignes. + +Un `Lien` peut appartenir à une enseigne ou non. + +Pour plus d'information vois la [page d'aide dédié au enseignes](/help/brands). + +### Un `Catalogue` + +Les liens peuvent être regroupés par `Catalogue`. Ceci permet de faire des exports ou des imports par `Catalogue`. +Il deviens également possible de consulter les liens par `Catalogue`. + +### Un `FQDN` + +Cela représente un nom de domain complet par exemple `google.fr.`, il peut aussi contenir un sous domaine : `photo.google.fr`. + +Techniquement il se termine toujours pas un point, même si il est facultatif à l'usage. + +Il se lien en réalité à l'envers et permet de cible une machine distante avec un nom en texte plutôt qu'une adresse IP. + +Les différentes parties : + +- TLD (Top level domain) : `.com` +- Domain : `google` +- Sub domain : `photo` + +Pour plus d'informations voir : [Une `Enseigne`](#une-enseigne). diff --git a/static/assets/help/resolve.md b/static/assets/help/resolve.md new file mode 100644 index 0000000..58d12e2 --- /dev/null +++ b/static/assets/help/resolve.md @@ -0,0 +1,27 @@ +# Résoudre un lien + +[Retour au sommaire](/help) + +## Fonctionnement + + +### Préambule + +Les liens sont tous résolus à partir d'un 'site internet' que l'on appelle un résolveur. Z3 en possède un. +Il est accessible avec un adresse de style `z3-resolver.com`. + +Normalement ce résolveur devrais aussi être accessible avec d'autre noms de domaines qui appartiennent à un enseigne. +Par exemple `short.qama.fr`. + +#### Résolution d'un lien + +Résoudre un lien ca veut dire : trouver que `https://z3-resolver.com/source-du-lien` correspond au lien `source-du-lien` et qui redirige vers `https://cible.com` ou alors que `source-du-lien` n'existe pas. + +##### Pour tous les liens + +La première chose à savoir c'est que tous les liens peuvent être résolus avec le nom de domaine par défaut par exemple `z3-resolver.com`. C'est également le cas pour les liens qui n'appartiennent à aucune enseigne et c'est la seule manière dont ceux-ci pourront être résolus. + +##### Pour les liens qui appartiennent à une enseigne + +Un lien dans ce cas appartient à une enseigne. Cette enseigne va soit posséder un nom de domaine ou plusieurs ou aucun. Si elle possède un nom de domaine ex `short.qama.fr`. Alors le lien pourra être résolu avec `https://short.qama.fr/source-du-lien`. +**Un aucun cas un lien ne pourra être résolu avec le nom de domaine d'une autre enseigne** diff --git a/static/assets/help/statistics.md b/static/assets/help/statistics.md new file mode 100644 index 0000000..f64e386 --- /dev/null +++ b/static/assets/help/statistics.md @@ -0,0 +1,27 @@ +# Statistiques globales de l'application + +[Retour au sommaire](/help) + +Cette page concerne [les statistiques de l'application](http://localhost:5173/statistics) uniquement. + +Ces statistiques sont basées à partir des données collectées qui sont anonymisées à chaque utilisation d'un lien de redirection. + +## Fonctionnement + +À chaque redirection des statistiques sont collectées par le `Resolveur` (la partie applicative Web qui répond aux demandes de redirection des navigateurs Web). En effet, une ligne contenant des informations anonymisées est stockée à chaque demande de redirection. + +Ceci permet de savoir si la redirection a été bien faite, de quelle redirection on parle ( `Source` -> `Cible`), mais également d'où venait le client (à t-il cliqué sur un lien contenu dans un site en particulier ?), son pays, etc. + +De ces informations, on peut déduire certaines statistiques. Celle-ci sont représentées sous forme de graphiques au sein de l'application. +Notez que les statistiques sont présentées sur une durée maximale d'un an. +Pour l'instant les fonctionnalités proposés se limitent à de l'affichage et de l'export en image. + +Si vous avez des besoins ou demandes supplémentaires n'hésiter pas à les faire remonter. + +Voici une liste des statistiques qui sont actuellement affichées : + +- Visites uniques par enseignes, il s'agit d'un total par jour pour chaque enseigne sur une durée max d'un an. +- Visites uniques par catalogues, il s'agit d'un total par jour pour chaque catalogue sur une durée max d'un an. +- Liens invalides par enseignes, il s'agit d'un total hebdomadaire pour chaque enseigne. +- Provenance des visites par enseigne les plus fréquentes, c'est un classement basé sur le nombre de fois qu'une provenance apparait avec leurs enseignes associées, c'est un top 20, ce sur une durée max d'un an. +- Les liens les plus utilisés par enseigne, il s'agit d'un classement de `Source` les plus utilisées avec leurs enseignes associés, c'est un top 30, ce sur une durée max d'un an. diff --git a/static/assets/help/visits.md b/static/assets/help/visits.md new file mode 100644 index 0000000..76c5379 --- /dev/null +++ b/static/assets/help/visits.md @@ -0,0 +1,35 @@ +# Comptage des visites sur les liens + +[Retour au sommaire](/help) + +Cette page concerne [les visites sur les liens](http://localhost:5173/links/link-statistics) uniquement. + +## Fonctionnement + +### Toutes enseignes confondues + +- Liens invalides : + Le nombre de liens invalides de toutes les enseignes, celà inclus les liens sans enseignes. + +- Visites sur tous les liens : + Le nombre de liens invalides de l'enseigne. Ce nombre varie en fonction de l'enseigne sélectionnée et des liens invalides qu'elle contient. + +- Visites sur des liens externes : + Le nombre de visites sur des liens externes tous confondus. Ce nombre varie en fonction des visites effectuées sur les liens externes de toute l'application. + +- Visites sur tous les liens : + Le nombre de liens invalides de l'enseigne. Ce nombre varie en fonction de l'enseigne sélectionnée et des liens invalides qu'elle contient. + +### Pour une enseigne donnée + +- Liens invalides : + Le nombre de liens invalides de l'enseigne. Ce nombre varie en fonction de l'enseigne sélectionnée et des liens invalides qu'elle contient. + +- Visites sur l'enseigne : + Le nombre de visites sur les liens de l'enseigne sélectionnée. + +- Visites sur des liens externes : + Le nombre de visites sur des liens externes de l'enseigne sélectionnée. + +- Visites sur les liens sans enseignes : + Le nombre total de visites sur les liens sans enseignes. diff --git a/static/assets/z3-application-import-template.csv b/static/assets/z3-application-import-template.csv new file mode 100644 index 0000000..37eb06a --- /dev/null +++ b/static/assets/z3-application-import-template.csv @@ -0,0 +1,6 @@ +source,target,isExternal +promos-de-septembre,https://example.net/promos/septembre,false +promo-de-juin,https://example.net/promo/juin,1 +promos-de-juillet,https://example.net/promos/juillet,true +promo-de-aout,https://example.net/promo/aout,0 +,https://example.net/promo/source-genere-aleatoirement,0 \ No newline at end of file diff --git a/static/favicon-96x96.png b/static/favicon-96x96.png new file mode 100644 index 0000000..4eb88ef Binary files /dev/null and b/static/favicon-96x96.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..e096589 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..0a03f24 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/static/fonts/Ubuntu-Bold.woff b/static/fonts/Ubuntu-Bold.woff new file mode 100644 index 0000000..82b01b4 Binary files /dev/null and b/static/fonts/Ubuntu-Bold.woff differ diff --git a/static/fonts/Ubuntu-Bold.woff2 b/static/fonts/Ubuntu-Bold.woff2 new file mode 100644 index 0000000..04d6fb5 Binary files /dev/null and b/static/fonts/Ubuntu-Bold.woff2 differ diff --git a/static/fonts/Ubuntu-BoldItalic.woff b/static/fonts/Ubuntu-BoldItalic.woff new file mode 100644 index 0000000..47b92e8 Binary files /dev/null and b/static/fonts/Ubuntu-BoldItalic.woff differ diff --git a/static/fonts/Ubuntu-BoldItalic.woff2 b/static/fonts/Ubuntu-BoldItalic.woff2 new file mode 100644 index 0000000..b4314e2 Binary files /dev/null and b/static/fonts/Ubuntu-BoldItalic.woff2 differ diff --git a/static/fonts/Ubuntu-Italic.woff b/static/fonts/Ubuntu-Italic.woff new file mode 100644 index 0000000..aa41742 Binary files /dev/null and b/static/fonts/Ubuntu-Italic.woff differ diff --git a/static/fonts/Ubuntu-Italic.woff2 b/static/fonts/Ubuntu-Italic.woff2 new file mode 100644 index 0000000..2d02308 Binary files /dev/null and b/static/fonts/Ubuntu-Italic.woff2 differ diff --git a/static/fonts/Ubuntu-Light.woff b/static/fonts/Ubuntu-Light.woff new file mode 100644 index 0000000..e195d15 Binary files /dev/null and b/static/fonts/Ubuntu-Light.woff differ diff --git a/static/fonts/Ubuntu-Light.woff2 b/static/fonts/Ubuntu-Light.woff2 new file mode 100644 index 0000000..7802590 Binary files /dev/null and b/static/fonts/Ubuntu-Light.woff2 differ diff --git a/static/fonts/Ubuntu-LightItalic.woff b/static/fonts/Ubuntu-LightItalic.woff new file mode 100644 index 0000000..c0c15f8 Binary files /dev/null and b/static/fonts/Ubuntu-LightItalic.woff differ diff --git a/static/fonts/Ubuntu-LightItalic.woff2 b/static/fonts/Ubuntu-LightItalic.woff2 new file mode 100644 index 0000000..029c505 Binary files /dev/null and b/static/fonts/Ubuntu-LightItalic.woff2 differ diff --git a/static/fonts/Ubuntu-Medium.woff b/static/fonts/Ubuntu-Medium.woff new file mode 100644 index 0000000..0be08fa Binary files /dev/null and b/static/fonts/Ubuntu-Medium.woff differ diff --git a/static/fonts/Ubuntu-Medium.woff2 b/static/fonts/Ubuntu-Medium.woff2 new file mode 100644 index 0000000..7e8324c Binary files /dev/null and b/static/fonts/Ubuntu-Medium.woff2 differ diff --git a/static/fonts/Ubuntu-MediumItalic.woff b/static/fonts/Ubuntu-MediumItalic.woff new file mode 100644 index 0000000..dd72055 Binary files /dev/null and b/static/fonts/Ubuntu-MediumItalic.woff differ diff --git a/static/fonts/Ubuntu-MediumItalic.woff2 b/static/fonts/Ubuntu-MediumItalic.woff2 new file mode 100644 index 0000000..994f00b Binary files /dev/null and b/static/fonts/Ubuntu-MediumItalic.woff2 differ diff --git a/static/fonts/Ubuntu-Regular.woff b/static/fonts/Ubuntu-Regular.woff new file mode 100644 index 0000000..24d1e5e Binary files /dev/null and b/static/fonts/Ubuntu-Regular.woff differ diff --git a/static/fonts/Ubuntu-Regular.woff2 b/static/fonts/Ubuntu-Regular.woff2 new file mode 100644 index 0000000..f20fbbc Binary files /dev/null and b/static/fonts/Ubuntu-Regular.woff2 differ diff --git a/static/fonts/UbuntuMono-Bold.woff b/static/fonts/UbuntuMono-Bold.woff new file mode 100644 index 0000000..bb3b7a6 Binary files /dev/null and b/static/fonts/UbuntuMono-Bold.woff differ diff --git a/static/fonts/UbuntuMono-Bold.woff2 b/static/fonts/UbuntuMono-Bold.woff2 new file mode 100644 index 0000000..1f7f7c8 Binary files /dev/null and b/static/fonts/UbuntuMono-Bold.woff2 differ diff --git a/static/fonts/UbuntuMono-BoldItalic.woff b/static/fonts/UbuntuMono-BoldItalic.woff new file mode 100644 index 0000000..1d5aff0 Binary files /dev/null and b/static/fonts/UbuntuMono-BoldItalic.woff differ diff --git a/static/fonts/UbuntuMono-BoldItalic.woff2 b/static/fonts/UbuntuMono-BoldItalic.woff2 new file mode 100644 index 0000000..53a58f3 Binary files /dev/null and b/static/fonts/UbuntuMono-BoldItalic.woff2 differ diff --git a/static/fonts/UbuntuMono-Italic.woff b/static/fonts/UbuntuMono-Italic.woff new file mode 100644 index 0000000..08e0698 Binary files /dev/null and b/static/fonts/UbuntuMono-Italic.woff differ diff --git a/static/fonts/UbuntuMono-Italic.woff2 b/static/fonts/UbuntuMono-Italic.woff2 new file mode 100644 index 0000000..20abfc6 Binary files /dev/null and b/static/fonts/UbuntuMono-Italic.woff2 differ diff --git a/static/fonts/UbuntuMono-Regular.woff b/static/fonts/UbuntuMono-Regular.woff new file mode 100644 index 0000000..e361a1e Binary files /dev/null and b/static/fonts/UbuntuMono-Regular.woff differ diff --git a/static/fonts/UbuntuMono-Regular.woff2 b/static/fonts/UbuntuMono-Regular.woff2 new file mode 100644 index 0000000..8a3bc1e Binary files /dev/null and b/static/fonts/UbuntuMono-Regular.woff2 differ diff --git a/static/fonts/UbuntuSans-Italic.woff b/static/fonts/UbuntuSans-Italic.woff new file mode 100644 index 0000000..9aa7fa3 Binary files /dev/null and b/static/fonts/UbuntuSans-Italic.woff differ diff --git a/static/fonts/UbuntuSans-Italic.woff2 b/static/fonts/UbuntuSans-Italic.woff2 new file mode 100644 index 0000000..e43b5d8 Binary files /dev/null and b/static/fonts/UbuntuSans-Italic.woff2 differ diff --git a/static/fonts/UbuntuSans-Regular.woff b/static/fonts/UbuntuSans-Regular.woff new file mode 100644 index 0000000..6d0bdcc Binary files /dev/null and b/static/fonts/UbuntuSans-Regular.woff differ diff --git a/static/fonts/UbuntuSans-Regular.woff2 b/static/fonts/UbuntuSans-Regular.woff2 new file mode 100644 index 0000000..bba3c74 Binary files /dev/null and b/static/fonts/UbuntuSans-Regular.woff2 differ diff --git a/static/site.webmanifest b/static/site.webmanifest new file mode 100644 index 0000000..42d85d8 --- /dev/null +++ b/static/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Z3 - Link", + "short_name": "Z3", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#212121", + "background_color": "#212121", + "display": "standalone" +} \ No newline at end of file diff --git a/static/web-app-manifest-192x192.png b/static/web-app-manifest-192x192.png new file mode 100644 index 0000000..c94ea69 Binary files /dev/null and b/static/web-app-manifest-192x192.png differ diff --git a/static/web-app-manifest-512x512.png b/static/web-app-manifest-512x512.png new file mode 100644 index 0000000..f6de368 Binary files /dev/null and b/static/web-app-manifest-512x512.png differ diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..0dfb815 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,37 @@ +import adapter from '@sveltejs/adapter-node' +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + + // compilerOptions: { + // runes: true, + // }, + + compileOptions: { + rootDirs: ['./src/types'], + }, + + kit: { + adapter: adapter(), + alias: { + $customType: 'src/types', + }, + csp: { + mode: 'auto', + directives: { + 'script-src': ['self'], + }, + reportOnly: { + 'script-src': ['self'], + 'report-uri': ['/'], + }, + }, + version: { + name: process.env.npm_package_version, + }, + }, +} + +export default config diff --git a/tests/e2e/authentication.test.ts b/tests/e2e/authentication.test.ts new file mode 100644 index 0000000..55a603c --- /dev/null +++ b/tests/e2e/authentication.test.ts @@ -0,0 +1,29 @@ +// import { expect, test } from '@playwright/test'; + +// test('login test', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('turlututu'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await page.waitForURL('http://localhost:4173/'); +// await expect(page.getByText('costin.cadeau@pegase-market.com')).toBeVisible(); +// }); + +// test('logout test', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('turlututu'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await page.goto('http://localhost:4173/'); +// await page.getByTestId('dropdown-profile').click(); +// await page.getByRole('button', { name: 'Déconnexion' }).click(); +// await expect(page.getByRole('heading', { name: 'Connexion' })).toBeVisible(); +// }); + +// test('error login test', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('toto'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await expect(page.getByRole('heading', { name: 'Erreur' })).toBeVisible(); +// }); diff --git a/tests/e2e/navigationInSidebar.test.ts b/tests/e2e/navigationInSidebar.test.ts new file mode 100644 index 0000000..f06fa2b --- /dev/null +++ b/tests/e2e/navigationInSidebar.test.ts @@ -0,0 +1,36 @@ +// import { expect, test } from '@playwright/test'; + +// test('navigate on pages', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('turlututu'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await page.goto('http://localhost:4173/'); +// await page.screenshot(); +// await page.getByRole('button', { name: 'Pages' }).click(); +// await expect(page.getByRole('button', { name: 'Pages' })).toBeFocused(); +// await expect(page.getByTestId('page-test')).toBeVisible(); +// await page.screenshot(); +// }); + +// test('navigate on banners', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('turlututu'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await page.goto('http://localhost:4173/'); +// await page.getByRole('button', { name: 'Bannières' }).click(); +// await expect(page.getByRole('button', { name: 'Bannières' })).toBeFocused(); +// await expect(page.getByTestId('banner-test')).toBeVisible(); +// }); + +// test('navigate on notifications', async ({ page }) => { +// await page.goto('http://localhost:4173/login'); +// await page.getByLabel('Email').fill('costin.cadeau@pegase-market.com'); +// await page.getByLabel('Mot de passe').fill('turlututu'); +// await page.getByRole('button', { name: 'se connecter' }).click(); +// await page.goto('http://localhost:4173/'); +// await page.getByRole('button', { name: 'Notifications' }).click(); +// await expect(page.getByRole('button', { name: 'Notifications' })).toBeFocused(); +// await expect(page.getByTestId('notification-test')).toBeVisible(); +// }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e787328 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "noFallthroughCasesInSwitch": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "importHelpers": true, + }, + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..a8c8e4b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,53 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { svelteTesting } from '@testing-library/svelte/vite'; +// @ts-ignore +import path from 'node:path'; +import checker from 'vite-plugin-checker'; +import { svelteChecker } from 'vite-plugin-svelte-checker'; +import { defineConfig } from 'vitest/config'; + +const __dirname = path.resolve(); + +export default defineConfig({ + resolve: { + alias: [ + { find: '$src/', replacement: path.join(__dirname, 'src/') }, + ], + }, + plugins: [ + svelteChecker({ + typescript: true, + ignoreWarnings: false + }), + checker({ + typescript: true, + }), + tailwindcss(), + sveltekit(), + ], + test: { + workspace: [{ + extends: './vite.config.ts', + plugins: [svelteTesting()], + + test: { + name: 'client', + environment: 'jsdom', + clearMocks: true, + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], + exclude: ['src/lib/server/**'], + setupFiles: ['./vitest-setup-client.ts'], + }, + }, { + extends: './vite.config.ts', + + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], + }, + }], + }, +}) diff --git a/vitest-setup-client.ts b/vitest-setup-client.ts new file mode 100644 index 0000000..d2ef9ab --- /dev/null +++ b/vitest-setup-client.ts @@ -0,0 +1,18 @@ +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; + +// required for svelte5 + jsdom as jsdom does not support matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + enumerable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) + +// add more mocks here if you need them