diff options
| author | Martial Simon <msimon_fr@hotmail.com> | 2025-09-15 01:07:58 +0200 |
|---|---|---|
| committer | Martial Simon <msimon_fr@hotmail.com> | 2025-09-15 01:07:58 +0200 |
| commit | 967be9e750221ab2ab783f95df79bb26d290a45e (patch) | |
| tree | 6802900a5e975f9f68b169f0f503f040056d6952 /ping | |
Diffstat (limited to 'ping')
158 files changed, 15400 insertions, 0 deletions
diff --git a/ping/.env b/ping/.env new file mode 100644 index 0000000..dc9d11a --- /dev/null +++ b/ping/.env @@ -0,0 +1 @@ +PROJECT_DEFAULT_PATH = ping
\ No newline at end of file diff --git a/ping/.gitignore b/ping/.gitignore new file mode 100644 index 0000000..512f0b1 --- /dev/null +++ b/ping/.gitignore @@ -0,0 +1,174 @@ +.idea/ +.vscode/ +node_modules +flake.lock +*.tar +target/ +*.iml +viewer/ + +# Created by https://www.toptal.com/developers/gitignore/api/java,intellij,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=java,intellij,visualstudiocode + +### Intellij ### +# 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 Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### 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/java,intellij,visualstudiocode + +# Temporary files +*.swp* diff --git a/ping/.gitlab-ci.yml b/ping/.gitlab-ci.yml new file mode 100644 index 0000000..299a656 --- /dev/null +++ b/ping/.gitlab-ci.yml @@ -0,0 +1,92 @@ +stages: + - test + - deploy + + +.connect_to_intra_git: + image: + name: alpine/git:2.47.1 + entrypoint: [""] + environment: + name: forge-intra + url: https://intra.forge.epita.fr/epita-ing-assistants-yaka/ping-2027/root/ping-2027/ping-backend + before_script: + - test -n "$GIT_INTRA_REPO_URL" + - test -n "$GIT_INTRA_GLOBAL_URL" + - test -n "$GIT_DEPLOY_BRANCH" + - test -n "$GIT_RUNNER_NAME" + - test -n "$GIT_RUNNER_EMAIL" + + - 'command -v ssh-agent >/dev/null || apk add --no-cache openssh' + - eval $(ssh-agent -s) + + - chmod 400 "$SSH_PRIVATE_KEY" + - ssh-add "$SSH_PRIVATE_KEY" + + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + + - ssh-keyscan -v -H $GIT_INTRA_GLOBAL_URL >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + + - ssh -T git@"$GIT_INTRA_GLOBAL_URL" + + - git config --global user.email "$RUNNER_GIT_EMAIL" + - git config --global user.name "$RUNNER_GIT_NAME" + + - git ls-remote --exit-code -h "$GIT_INTRA_REPO_URL" || { [ $? -ne 2 ] && return 1; } + + - git remote | grep 'intra' || git remote add intra "$GIT_INTRA_REPO_URL" + - git remote update + + +compilation-job: + stage: test + image: maven:3.9.9-amazoncorretto-21-alpine + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: always + script: + - cd ping + - mvn quarkus:build + + +push-master-state-to-intra-job: + extends: .connect_to_intra_git + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push" + when: always + script: + - git log --all --oneline --graph + - git push intra HEAD:"$GIT_DEPLOY_BRANCH" + + +push-to-intra-job: + extends: .connect_to_intra_git + stage: deploy + rules: + - if: $CI_COMMIT_TAG != null + && ($CI_COMMIT_TAG =~ "/^release.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-dementor-.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-submission-.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-continuous-tag-.*$/") + when: always + script: + - git push intra "$CI_COMMIT_TAG" + + +release-job: + stage: deploy + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: $CI_COMMIT_TAG != null + when: always + needs: + - job: push-to-intra-job + artifacts: false + script: + - echo "creating a new release for tag $CI_COMMIT_TAG..." + release: + tag_name: '$CI_COMMIT_TAG' + description: '$CI_COMMIT_TAG' diff --git a/ping/README.md b/ping/README.md new file mode 100644 index 0000000..aa308d2 --- /dev/null +++ b/ping/README.md @@ -0,0 +1,93 @@ +# PING + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.cri.epita.fr/martial.simon/ping.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.cri.epita.fr/martial.simon/ping/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README + +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/ping/docker-compose.yml b/ping/docker-compose.yml new file mode 100644 index 0000000..b9d5289 --- /dev/null +++ b/ping/docker-compose.yml @@ -0,0 +1,58 @@ +services: + db-postgres: + container_name: db-postgres + environment: + - PGDATA=$HOME/postgres_data + - PGHOST=/tmp + - PGPORT=5432 + - POSTGRES_HOST_AUTH_METHOD=trust # pas de mdp + - POSTGRES_USER=postgres + - POSTGRES_DB=ping + image: reg.undercloud.cri.epita.fr/docker/postgres:17.5-alpine3.22 + ports: + - 0.0.0.0:5432:5432 # pas safe pour la prod + networks: + - backend + + java-backend: + container_name: java-backend + image: reg.undercloud.cri.epita.fr/docker/maven:3.9.9-amazoncorretto-21-alpine + volumes: + - ./ping/pom.xml:/app/pom.xml + - ./ping/src:/app/src + working_dir: /app + command: sh -c "apk add --no-cache curl git && mvn clean install && mvn quarkus:dev -Dquarkus.http.host=0.0.0.0 -Dquarkus.datasource.jdbc.url=jdbc:postgresql://db-postgres:5432/ping" + depends_on: + - db-postgres + ports: + - 0.0.0.0:8080:8080 # pas safe pour la prod + networks: + - backend + - frontend + environment: + - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://db-postgres:5432/ping + + sveltekit-frontend: + container_name: sveltekit-frontend + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - 3000:3000 + depends_on: + - java-backend + environment: + - VITE_BACKEND_URL=http://java-backend:8080 + - ALPHA_VANTAGE_API_KEY=102780243600020474604 + networks: + - frontend + volumes: + - ./frontend:/tmp/ping/frontend + - /tmp/ping/frontend/node_modules # pour éviter les conflits de dépendances entre hôte et conteneur + working_dir: /tmp/ping/frontend + +networks: + backend: + driver: bridge + frontend: + driver: bridge diff --git a/ping/frontend/.dockerignore b/ping/frontend/.dockerignore new file mode 100644 index 0000000..bcf8731 --- /dev/null +++ b/ping/frontend/.dockerignore @@ -0,0 +1,2 @@ +.svelte-kit +node_modules
\ No newline at end of file diff --git a/ping/frontend/.gitignore b/ping/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/ping/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/ping/frontend/.npmrc b/ping/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/ping/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/ping/frontend/.prettierignore b/ping/frontend/.prettierignore new file mode 100644 index 0000000..6562bcb --- /dev/null +++ b/ping/frontend/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb diff --git a/ping/frontend/.prettierrc b/ping/frontend/.prettierrc new file mode 100644 index 0000000..7ebb855 --- /dev/null +++ b/ping/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/ping/frontend/Dockerfile b/ping/frontend/Dockerfile new file mode 100644 index 0000000..52b38c4 --- /dev/null +++ b/ping/frontend/Dockerfile @@ -0,0 +1,8 @@ +FROM node:24-alpine +WORKDIR /tmp/ping/frontend + +COPY package*.json ./ +RUN npm install + +COPY . . +ENTRYPOINT npm run dev -- --host 0.0.0.0 --port 3000
\ No newline at end of file diff --git a/ping/frontend/README.md b/ping/frontend/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/ping/frontend/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## 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 +``` + +## 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://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/ping/frontend/eslint.config.js b/ping/frontend/eslint.config.js new file mode 100644 index 0000000..be96133 --- /dev/null +++ b/ping/frontend/eslint.config.js @@ -0,0 +1,36 @@ +import prettier from 'eslint-config-prettier'; +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'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { 'no-undef': 'off' } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/ping/frontend/package-lock.json b/ping/frontend/package-lock.json new file mode 100644 index 0000000..95da3b3 --- /dev/null +++ b/ping/frontend/package-lock.json @@ -0,0 +1,4385 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "chart.js": "^4.5.0", + "yahoo-finance2": "^2.13.3" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.6", + "vite-plugin-devtools-json": "^0.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.0.tgz", + "integrity": "sha512-ZBygRBqpDYiIHsN+d1WyHn3TYgzgpzLEcgJUxTATyiInQbKZz6wZb6+ljwdg8xeeOe4v03z6Uh6lELiw0/mVhQ==", + "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.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "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-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "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/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/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "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/@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/@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.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.32.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.35.tgz", + "integrity": "sha512-Ul3YyOTU++to8cgNkttakC0dWvpERr6RYoHO2W47DLbFvrwBDJUY31B1sImH6JZSYc4Kt4PyHtoPNu+vL2r2dA==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz", + "integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.0.tgz", + "integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "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", + "vitefu": "^1.0.6" + }, + "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 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", + "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "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==", + "dev": true, + "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/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", + "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz", + "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-x64": "4.1.10", + "@tailwindcss/oxide-freebsd-x64": "4.1.10", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-x64-musl": "4.1.10", + "@tailwindcss/oxide-wasm32-wasi": "4.1.10", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz", + "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz", + "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz", + "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz", + "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz", + "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz", + "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz", + "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz", + "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz", + "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz", + "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", + "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz", + "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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==", + "dev": true, + "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.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.10.tgz", + "integrity": "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.10", + "@tailwindcss/oxide": "4.1.10", + "tailwindcss": "4.1.10" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "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/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "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.35.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "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/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "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": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "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/type-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "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.1.0" + }, + "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/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" + }, + "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "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/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-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/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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/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/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/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "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==", + "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/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": 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==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "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.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@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.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.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-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.3.tgz", + "integrity": "sha512-PlcyK80sqAZ43IITeZkgl3zPFWJytx/Joup9iKGqIOsXM2m3pWfPbWuXPr5PN3loXFEypqTY/JyZwNqlSpSvRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "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.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "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.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", + "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", + "dev": true, + "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/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/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/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.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/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/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-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-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-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "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/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": 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.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "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/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "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.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "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/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/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "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/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/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/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/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/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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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.11", + "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": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "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==", + "dev": true, + "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/prettier": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", + "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "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/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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "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.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", + "fsevents": "~2.3.2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "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==", + "dev": true, + "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/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/svelte": { + "version": "5.34.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.8.tgz", + "integrity": "sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==", + "dev": true, + "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.8", + "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.2.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", + "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", + "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": "1.2.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.2.0.tgz", + "integrity": "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie-file-store": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz", + "integrity": "sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==", + "license": "MIT", + "dependencies": { + "tough-cookie": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" + }, + "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/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "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/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "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-plugin-devtools-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.2.0.tgz", + "integrity": "sha512-K7PoaWOEJECZ1n3VbhJXsUAX2PsO0xY7KFMM/Leh7tUev0M5zi+lz+vnVVdCK17IOK9Jp9rdzHXc08cnQirGbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "uuid": "^11.1.0" + }, + "peerDependencies": { + "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz", + "integrity": "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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/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/yahoo-finance2": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-2.13.3.tgz", + "integrity": "sha512-ZECy6wQ7ymT08nVrxqQf+gwmINJ4/ECLyq+vM3SQmH3HWzje5DX1WX5YcZpWpWi4KXdmo2Vuk9OAdrTP09nE4g==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.32.27", + "@types/tough-cookie": "^4.0.2", + "tough-cookie": "^4.1.2", + "tough-cookie-file-store": "^2.0.3" + }, + "bin": { + "yahoo-finance": "bin/yahoo-finance.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "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==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ping/frontend/package.json b/ping/frontend/package.json new file mode 100644 index 0000000..ac61dfe --- /dev/null +++ b/ping/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "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", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.6", + "vite-plugin-devtools-json": "^0.2.0" + }, + "dependencies": { + "chart.js": "^4.5.0", + "yahoo-finance2": "^2.13.3" + } +} diff --git a/ping/frontend/src/app.css b/ping/frontend/src/app.css new file mode 100644 index 0000000..42e2b32 --- /dev/null +++ b/ping/frontend/src/app.css @@ -0,0 +1,58 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; + +:root { + --bg-primary: #1A1C22; + --bg-secondary: #282C35; + --btn-primary: #343844; + --btn-primary-hover: #2d8f3a; + --btn-secondary: #454b5a; + --text-lime: #00FF77; +} + +body { + background-color: var(--bg-secondary); +} + +input,select,textarea { + background-color: var(--bg-secondary); + border:none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; +} + +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--text-lime); + border-radius: 8px; + border:none; +} + +.btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; +} + +.btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); +} + +.card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: 100%; +}
\ No newline at end of file diff --git a/ping/frontend/src/app.d.ts b/ping/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/ping/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/ping/frontend/src/app.html b/ping/frontend/src/app.html new file mode 100644 index 0000000..e37ecf4 --- /dev/null +++ b/ping/frontend/src/app.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%sveltekit.assets%/favicon.png" /> + <title>Patapimvest</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + %sveltekit.head% + </head> + <body data-sveltekit-preload-data="hover"> + <div style="display: contents">%sveltekit.body%</div> + </body> +</html> diff --git a/ping/frontend/src/lib/components/Avatar.svelte b/ping/frontend/src/lib/components/Avatar.svelte new file mode 100644 index 0000000..c04b563 --- /dev/null +++ b/ping/frontend/src/lib/components/Avatar.svelte @@ -0,0 +1,48 @@ +<script lang="ts"> + let showTooltip = $state(false); + + interface Props { + username?: string; + url?: string; + onclick?: () => void; + } + + let { username = 'lolo', url = '/img/default-avatar.png', onclick = () => {} }: Props = $props(); +</script> + +<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> +<div style="position: relative; display: inline-block;"> + <!-- svelte-ignore a11y_click_events_have_key_events --> + <img + src={url ?? '/img/default-avatar.png'} + onmouseenter={() => (showTooltip = true)} + onmouseleave={() => (showTooltip = false)} + {onclick} + alt="User Avatar" + /> + {#if showTooltip} + <div class="tooltip">{username}</div> + {/if} +</div> + +<style> + img { + width: 48px; + height: 48px; + border-radius: 50%; + } + .tooltip { + position: absolute; + top: 110%; + left: 50%; + transform: translateX(-50%); + background: #333; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + font-size: 0.9em; + z-index: 1; + pointer-events: none; + } +</style> diff --git a/ping/frontend/src/lib/components/Button.svelte b/ping/frontend/src/lib/components/Button.svelte new file mode 100644 index 0000000..54b26ab --- /dev/null +++ b/ping/frontend/src/lib/components/Button.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + // Accept props and ensure onclick matches the expected event handler signature + const { + href = undefined as string | undefined, + onclick = undefined as + | ((event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) => void) + | undefined, + disabled = false, + children + } = $props(); +</script> + +{#if href} + <a class="btn" {href}>{@render children?.()}</a> +{:else} + <button class="btn" {onclick} {disabled}>{@render children?.()}</button> +{/if} diff --git a/ping/frontend/src/lib/components/NavBar.svelte b/ping/frontend/src/lib/components/NavBar.svelte new file mode 100644 index 0000000..1a76876 --- /dev/null +++ b/ping/frontend/src/lib/components/NavBar.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + const { pageTitle, rightComponent } = $props(); +</script> + +<nav> + <a id="logo" href="/"> + <img src="/img/logo.svg" alt="" style="height:50px" /> + </a> + <div id="middletitle">{pageTitle}</div> + <div id="right">{@render rightComponent()}</div> +</nav> + +<style> + nav { + position: fixed; + width: 100%; + background-color: var(--bg-primary); + color: white; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + height: 64px; + z-index: 2; + } + + #logo { + transition-duration: 0.2s; + } + + #logo:hover { + transform: scale(1.025); + filter: saturate(2); + } + + #middletitle { + font-weight: bold; + font-size: 24px; + } + + #right { + display: flex; + } +</style> diff --git a/ping/frontend/src/lib/components/NumberStatList.svelte b/ping/frontend/src/lib/components/NumberStatList.svelte new file mode 100644 index 0000000..b6444e8 --- /dev/null +++ b/ping/frontend/src/lib/components/NumberStatList.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + export interface INumberStatList { + name: string; + value: string; + icon: string; + color: string; + } + + const { statsList = [] }: { statsList?: INumberStatList[] } = $props(); +</script> + +<section class="numberStatList"> + {#each statsList as stat} + <div class="card"> + <img src={stat.icon} alt="" class="stat-icon" /> + <span class="stat-name">{stat.name}</span> + <span class="stat-value" style:color={stat.color}>{stat.value}</span> + </div> + {/each} +</section> + +<style> + .numberStatList { + color: white; + display: flex; + width: 100%; + align-items: stretch; + justify-content: stretch; + } + + .numberStatList :global(.card) { + flex: 1; + display: grid; + grid-template-areas: 'icon name' 'icon amount'; + align-items: center; + justify-content: space-around; + } + + .stat-icon { + width: 24px; + height: 24px; + grid-area: icon; + } + + .stat-name { + grid-area: name; + font-weight: bold; + } + + .stat-value { + grid-area: amount; + font-size: 1.2em; + font-weight: bold; + } +</style> diff --git a/ping/frontend/src/lib/components/SideBar.svelte b/ping/frontend/src/lib/components/SideBar.svelte new file mode 100644 index 0000000..9ea8a04 --- /dev/null +++ b/ping/frontend/src/lib/components/SideBar.svelte @@ -0,0 +1,61 @@ +<script lang="ts"> + import { pages } from '$lib/pages'; + + const { + selectedIndex = $bindable(0) + }: { + selectedIndex: number; + } = $props(); + + const items = pages; +</script> + +<section id="sidebar"> + {#each items as item, index} + <!-- svelte-ignore a11y_consider_explicit_label --> + <a href={item.href} class:selected={index == selectedIndex} class="flex items-center gap-2"> + <img + src={item.icon} + alt=" " + class="w-6" + style="filter: {index == selectedIndex + ? 'invert(68%) sepia(97%) saturate(749%) hue-rotate(97deg) brightness(154%) contrast(101%)' + : 'none'};" + /> + <span>{item.name}</span> + </a> + {/each} +</section> + +<style> + #sidebar { + position: fixed; + background-color: var(--bg-primary); + height: 100vh; + width: 200px; + padding-top: 72px; + color: white; + display: flex; + flex-direction: column; + gap: 8px; + } + + #sidebar > a { + padding: 8px; + padding-right: 0; + transition-duration: 0.2s; + border-color: var(--text-lime); + } + + #sidebar > a:is(:hover, :focus) { + background-color: var(--bg-secondary); + } + + .selected { + color: var(--text-lime); + background-color: var(--bg-secondary); + font-weight: bold; + border-right: 6px var(--text-lime) solid; + width: 100%; + } +</style> diff --git a/ping/frontend/src/lib/components/SteppedLineChart.svelte b/ping/frontend/src/lib/components/SteppedLineChart.svelte new file mode 100644 index 0000000..d393b44 --- /dev/null +++ b/ping/frontend/src/lib/components/SteppedLineChart.svelte @@ -0,0 +1,76 @@ +<script lang="ts"> + import { + Chart, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + LineController + } from 'chart.js'; + import { onMount } from 'svelte'; + + Chart.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + LineController + ); + + interface IProps { + color?: string; + data?: any[]; + title?: string; + legend?: string; + } + + const { color = 'green', data = [], title = '', legend = '' }: IProps = $props(); + + function updateGraph() { + const ctx = document.getElementById(id) as HTMLCanvasElement; + const chart = new Chart(ctx, { + type: 'line', + data: { + labels: data.map((d) => d.label), + datasets: [ + { + label: legend, + data: data.map((d) => d.value), + borderColor: color, + backgroundColor: color, + fill: false, + tension: 0.1 + } + ] + }, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: title + }, + legend: { + display: true + } + } + } + }); + } + + const id = 'graph-' + crypto.randomUUID(); + + onMount(() => { + updateGraph(); + }); +</script> + +<div> + <canvas {id}></canvas> +</div> diff --git a/ping/frontend/src/lib/components/ToastList.svelte b/ping/frontend/src/lib/components/ToastList.svelte new file mode 100644 index 0000000..5351ef7 --- /dev/null +++ b/ping/frontend/src/lib/components/ToastList.svelte @@ -0,0 +1,67 @@ +<script lang="ts"> + import { toastList } from '$lib/stores/toast'; +</script> + +<div class="toast"> + {#each $toastList as toast} + <div class="toast-item" style="border-top: 4px {toast.color} solid"> + <h3 style="color: {toast.color}">{toast.title}</h3> + <p>{toast.message}</p> + </div> + {/each} +</div> + +<style> + .toast { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 10px; + } + + .toast-item { + background-color: var(--bg-secondary); + padding: 10px; + color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: + transform 0.3s, + opacity 0.3s; + transform: translateY(30px); + opacity: 0; + animation-delay: 0s, 4.7s; + animation-name: toast-in, toast-out; + animation-duration: 0.3s, 0.3s; + animation-fill-mode: forwards, forwards; + } + + @keyframes toast-in { + from { + transform: translateY(30px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes toast-out { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(30px); + opacity: 0; + } + } + + .toast-item h3 { + margin: 0; + font-weight: bold; + } +</style> diff --git a/ping/frontend/src/lib/components/UserItem.svelte b/ping/frontend/src/lib/components/UserItem.svelte new file mode 100644 index 0000000..44add60 --- /dev/null +++ b/ping/frontend/src/lib/components/UserItem.svelte @@ -0,0 +1,42 @@ +<script lang="ts"> + import Avatar from './Avatar.svelte'; + import type { IUser } from '$lib/stores/auth'; + + const { user, onEdit, onDelete }: { user: IUser; onEdit: any; onDelete: any } = $props(); +</script> + +<div class="useritem"> + <div class="useritem-avatar"> + <Avatar username={user.displayName} url={user.avatar || '/img/default-avatar.png'} /> + <button class="btn" onclick={onEdit}> Modifier </button> + <button class="btn" onclick={onDelete}> Supprimer </button> + </div> + <div class="useritem-details"> + <p><b>Nom</b>: {user.displayName}</p> + <p><b>Login</b>: {user.login}</p> + <p><b>Role</b>: {user.isAdmin ? 'ADMIN' : 'USER'}</p> + <p><b>Id</b>: {user.id}</p> + </div> +</div> + +<style> + .useritem { + display: flex; + background-color: var(--bg-secondary); + margin: 16px 0; + padding: 8px; + border-radius: 8px; + gap: 8px; + } + + .useritem-avatar, + .useritem-details { + display: flex; + flex-direction: column; + gap: 4px; + } + + .useritem-avatar { + align-items: center; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte new file mode 100644 index 0000000..3844abc --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte @@ -0,0 +1,10 @@ +<h2>Analyse de risque</h2> +<i>tkt t safe c hardcodé chef</i> + +<style> + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/StockGraph.svelte b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte new file mode 100644 index 0000000..beefed9 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import Chart from 'chart.js/auto'; + import { onMount } from 'svelte'; + import StockSelector from '../input/StockSelector.svelte'; + + async function fetchChartData(stock: string, startDate: string, endDate: string) { + const res = await fetch( + `/stocksapi/chart?query=${stock}&startDate=${startDate}&endDate=${endDate}&interval=${range}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + } + ); + return await res.json(); + } + + function updateGraph() { + fetchChartData(selectedStock, startDate, endDate).then((data) => { + if (chart) { + chart.destroy(); + chart = null; + } + + const { quotes, meta } = data; + + validRanges = meta.validRanges; + + const labels = quotes.map((item: any) => item.date.split('T')[0]); + const datasets = ['low', 'high', 'open', 'close'].map((key) => ({ + label: key.charAt(0).toUpperCase() + key.slice(1), + data: quotes.map((item: any) => item[key]), + borderColor: + key === 'low' + ? 'rgb(255, 99, 132)' + : key === 'high' + ? 'rgb(54, 162, 235)' + : key === 'open' + ? 'rgb(255, 205, 86)' + : 'rgb(75, 192, 192)', + fill: false + })); + + // @ts-ignore + chart = new Chart(document.getElementById('stockgraph'), { + type: 'line', + data: { + labels, + datasets + }, + options: { + responsive: true, + maintainAspectRatio: true, + plugins: { + title: { + display: true, + text: meta.longName + } + } + } + }); + }); + } + + let today = $state(new Date().toISOString().split('T')[0]); + + let selectedStock = $state('2223.SR'); + let startDate = $state(''); + let endDate = $state(''); + let chart = $state<Chart | null>(null); + let range = $state('1d'); + let validRanges = $state(['1d']); + + onMount(() => { + endDate = new Date().toISOString().split('T')[0]; + startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + updateGraph(); + }); + + $effect(() => { + if (selectedStock && startDate && endDate && range) { + updateGraph(); + } + }); + + $effect(() => { + if (startDate && endDate && startDate > endDate) { + let temp = startDate; + startDate = endDate; + endDate = temp; + } + }); +</script> + +<div> + <div class="header"> + <h2>Vue d'ensemble : {selectedStock}</h2> + <div class="controls"> + <input type="date" name="startDate" id="startDate" bind:value={startDate} max={today} /> + <input type="date" name="endDate" id="endDate" bind:value={endDate} max={today} /> + <select name="validRanges" id="validRanges" bind:value={range}> + {#each validRanges as r} + <option value={r}> + {r} + </option> + {/each} + </select> + + <StockSelector bind:selectedStock /> + </div> + </div> + <canvas id="stockgraph"></canvas> +</div> + +<style> + .header { + display: flex; + align-items: center; + justify-content: space-between; + max-height: 512px !important; + } + + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } + + #stockgraph { + width: 100% !important; + max-height: 512px !important; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte new file mode 100644 index 0000000..3d947ad --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte @@ -0,0 +1,85 @@ +<script lang="ts"> + import { Chart } from 'chart.js'; + import { onMount } from 'svelte'; + + function updateGraph() { + getTrendingSymbols().then((trendingSymbols) => { + const labels = trendingSymbols.map((d) => d.longName); + + const datasets = [ + { + label: '50 day performance', + data: trendingSymbols.map((ts) => ts.fiftyDayAverageChange), + backgroundColor: 'green' + }, + { + label: '52 week performance', + data: trendingSymbols.map((ts) => ts.fiftyTwoWeekLowChange), + backgroundColor: 'orange' + } + ]; + + const data = { labels, datasets }; + + // @ts-ignore + new Chart(document.getElementById('graph'), { + type: 'bar', + data: data, + options: { + indexAxis: 'y', + elements: { + bar: { + borderWidth: 2 + } + }, + responsive: true, + plugins: { + legend: { + position: 'bottom' + } + } + } + }); + }); + } + + async function getTrendingSymbols() { + const trendingRes = await fetch('/stocksapi/trendingSymbols'); + if (!trendingRes.ok) { + throw new Error('Failed to fetch trending symbols'); + } + const trendingJson = await trendingRes.json(); + + const { quotes } = trendingJson; + + const trendingSymbols = await Promise.all( + quotes.map(async (quote: any) => { + const quoteRes = await fetch(`/stocksapi/quote?query=${quote.symbol}`); + const quoteJson = await quoteRes.json(); + return quoteJson; + }) + ); + + return trendingSymbols; + } + + onMount(() => { + updateGraph(); + }); +</script> + +<h2>Tendances</h2> +<canvas id="graph"></canvas> + +<style> + h2 { + font-weight: bold; + color: var(--text-lime); + font-size: 24px; + } + + #graph { + max-height: 40vh; + max-width: 40vw; + } +</style> diff --git a/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte new file mode 100644 index 0000000..b9f0224 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte @@ -0,0 +1,165 @@ +<script lang="ts"> + import { authFetch } from '$lib/stores/auth'; + import { onMount } from 'svelte'; + + interface IProps { + isOpen: boolean; + onCreate: (transaction: any) => void; + } + + let { isOpen = $bindable(false), onCreate = () => {} }: IProps = $props(); // Changed default to false + + let amount: number = $state(0); + let currency: string = $state('USD'); + let label: string = $state(''); + let receiverLabel: string = $state(''); + let receiverIban: string = $state(''); + let operationDate: string = $state(''); + + function closeDialog() { + isOpen = false; + } + + async function createTransaction(event: Event) { + event.preventDefault(); + + const transaction = { + amount, + currency, + label, + receiverLabel, + receiverIban, + operationDate + }; + + try { + const response = await authFetch('/api/transactions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(transaction) + }); + + if (!response.ok) { + const error = await response.json(); + alert('Erreur lors de la création de la transaction: ' + error); + return; + } + + onCreate(await response.json()); + + closeDialog(); + } catch (err) { + alert('Erreur réseau lors de la création de la transaction'); + } + } +</script> + +<!-- svelte-ignore a11y_click_events_have_key_events --> +<!-- svelte-ignore a11y_no_static_element_interactions --> +{#if isOpen} + <div class="backdrop" onclick={closeDialog}> + <dialog open onclick={(e) => e.stopPropagation()}> + <h2>Créer une nouvelle transaction</h2> + <form class="flex flex-col gap-2"> + <label> + <span>Montant</span> + <input type="number" step="0.01" min="0" bind:value={amount} required /> + </label> + <label> + <span>Devise</span> + <select bind:value={currency} required> + <option selected value="USD" data-symbol="$" data-name="Dollar américain" + >USD - Dollar américain</option + > + <option value="EUR" data-symbol="€" data-name="Euro">EUR - Euro</option> + <option value="GBP" data-symbol="£" data-name="Livre sterling" + >GBP - Livre sterling</option + > + <option value="JPY" data-symbol="¥" data-name="Yen japonais">JPY - Yen japonais</option> + <option value="CHF" data-symbol="Fr" data-name="Franc suisse">CHF - Franc suisse</option + > + <option value="CAD" data-symbol="$" data-name="Dollar canadien" + >CAD - Dollar canadien</option + > + <option value="AUD" data-symbol="$" data-name="Dollar australien" + >AUD - Dollar australien</option + > + <option value="CNY" data-symbol="¥" data-name="Yuan chinois">CNY - Yuan chinois</option> + </select> + </label> + <label> + <span>Libellé</span> + <input type="text" bind:value={label} required /> + </label> + <label> + <span>Libellé du bénéficiaire</span> + <input type="text" bind:value={receiverLabel} required /> + </label> + <label> + <span>IBAN du bénéficiaire</span> + <input type="text" bind:value={receiverIban} required /> + </label> + <label> + <span>Date de l'opération</span> + <input type="datetime-local" bind:value={operationDate} required /> + </label> + <button class="btn" onclick={createTransaction}>➕ Créer</button> + </form> + </dialog> + </div> +{/if} + +<style> + .backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 90vw; + max-height: 90vh; + border: none; + border-radius: 8px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); + padding: 2rem; + background: var(--bg-primary); + outline: none; + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; + justify-content: center; + color: white; + } + + h2 { + font-weight: 600; + font-size: 32px; + color: var(--text-lime); + } + + form { + display: flex; + flex-direction: column; + gap: 8px; + } + + label { + display: flex; + flex-direction: column; + } +</style> diff --git a/ping/frontend/src/lib/components/input/StockSelector.svelte b/ping/frontend/src/lib/components/input/StockSelector.svelte new file mode 100644 index 0000000..1237128 --- /dev/null +++ b/ping/frontend/src/lib/components/input/StockSelector.svelte @@ -0,0 +1,231 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import Button from '../Button.svelte'; + + let { selectedStock = $bindable('AAPL') } = $props(); + + let stocks: { + symbol: string; + shortname: string; + quoteType: string; + isYahooFinance: boolean; + }[] = $state([]); + let news: { + link: string; + title: string; + thumbnail: { resolutions: { url: string; width: number; height: number }[] }; + }[] = $state([]); + + function openDialog() { + const dialog = document.querySelector('dialog'); + const backdrop = document.getElementById('backdrop'); + // @ts-ignore + dialog.open = true; + // @ts-ignore + backdrop.style.display = 'block'; + } + + function closeDialog() { + const dialog = document.querySelector('dialog'); + const backdrop = document.getElementById('backdrop'); + // @ts-ignore + dialog.open = false; + // @ts-ignore + backdrop.style.display = 'none'; + } + + function onSearch(event: SubmitEvent) { + event.preventDefault(); + // @ts-ignore + const fd = new FormData(event.target); + const searchQuery = fd.get('search'); + + fetch(`/stocksapi/search?query=${searchQuery}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + } + }) + .then((response) => response.json()) + .then((data) => { + stocks = data.quotes.filter((q: any) => q.isYahooFinance); + news = data.news; + }) + .catch((error) => { + console.error('Error fetching stock data:', error); + }); + } + + onMount(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + closeDialog(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }); +</script> + +<Button onclick={openDialog}>{selectedStock}</Button> + +<dialog> + <form onsubmit={onSearch}> + <input + type="search" + name="search" + id="search" + placeholder="Recherche d'actions, ETFs, entreprises" + value={selectedStock || ''} + /> + <button type="submit" class="btn">Rechercher</button> + </form> + <div class="results"> + <div class="stocks"> + {#if stocks.length === 0} + <p>Aucune action, ETFs trouvés.</p> + {:else} + <p>{stocks.length} actions, ETFs trouvés</p> + {#each stocks as stock} + <button + class="btn stock" + onclick={() => { + selectedStock = stock.symbol; + closeDialog(); + }} + disabled={!stock.isYahooFinance} + > + <pre>{stock.symbol}</pre> + <span>{stock.shortname}</span> + <i>{stock.quoteType}</i> + </button> + {/each} + {/if} + </div> + <div class="news"> + {#if news.length === 0} + <p>Pas de news trouvés.</p> + {:else} + <p>{news.length} news trouvées</p> + {#each news as newsItem} + <a class="newsItem" href={newsItem.link}> + {#if newsItem.thumbnail?.resolutions?.length > 0} + <img + src={newsItem.thumbnail.resolutions[0].url} + alt="News Thumbnail" + width={newsItem.thumbnail.resolutions[0].width} + height={newsItem.thumbnail.resolutions[0].height} + /> + {/if} + <h1>{newsItem.title}</h1></a + > + {/each} + {/if} + </div> + </div> +</dialog> +<!-- svelte-ignore a11y_click_events_have_key_events --> +<!-- svelte-ignore a11y_no_static_element_interactions --> +<div id="backdrop" onclick={closeDialog}></div> + +<style> + dialog { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + padding: 20px; + background-color: var(--bg-primary); + border: 4px solid #ccc; + border-radius: 16px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + height: 90vh; + width: 90vw; + z-index: 1000; + color: white; + } + + #backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + display: none; + } + + form { + display: flex; + gap: 10px; + } + + input[type='search'] { + width: 300px; + } + + .results { + display: flex; + flex-direction: row; + gap: 20px; + justify-content: space-evenly; + } + + .stocks, + .news { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + max-height: 70vh; + padding: 16px; + flex: 1; + } + + .stock { + display: flex; + align-items: center; + gap: 8px; + } + + .stock pre { + font-weight: bold; + padding: 8px; + background: var(--bg-primary); + border-radius: 8px; + } + + .stock span { + font-weight: bold; + color: var(--text-lime); + } + + .newsItem { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + padding-bottom: 4px; + border-radius: 8px; + transition-duration: 0.2s; + } + + .newsItem h1 { + margin: 16px; + } + + .newsItem img { + border-radius: 8px; + width: 100%; + height: auto; + } + + .newsItem:hover { + transform: scale(1.025); + background-color: #333; + } +</style> diff --git a/ping/frontend/src/lib/components/input/UserSelector.svelte b/ping/frontend/src/lib/components/input/UserSelector.svelte new file mode 100644 index 0000000..a0c80c7 --- /dev/null +++ b/ping/frontend/src/lib/components/input/UserSelector.svelte @@ -0,0 +1,35 @@ +<script lang="ts"> + import type { IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import Avatar from '../Avatar.svelte'; + + let { users = $bindable<IUser[]>([]) }: { users: IUser[] } = $props(); +</script> + +<div> + {#each users as u, i} + <Avatar + username={u.displayName} + url={u.avatar} + onclick={() => { + users = users.filter((_, index) => index !== i); + }} + /> + {/each} + <Avatar + username={'Ajouter un utilisateur'} + url={'/icons/add-green.svg'} + onclick={() => { + let userId = prompt("Entrez l'ID de l'utilisateur à ajouter :"); + if (userId === null || userId.trim() === '') { + addToast({ title: 'ID utilisateur invalide.' }); + return; + } + if (users.map((u) => u.displayName).includes(userId)) { + addToast({ title: 'Cet utilisateur est déjà ajouté.' }); + return; + } + users = [...users, { displayName: userId, avatar: '/img/default-avatar.png' } as IUser]; + }} + /> +</div> diff --git a/ping/frontend/src/lib/pages.ts b/ping/frontend/src/lib/pages.ts new file mode 100644 index 0000000..e5d424e --- /dev/null +++ b/ping/frontend/src/lib/pages.ts @@ -0,0 +1,54 @@ +interface SideBarItem { + icon: string; + name: string; + href: string; +} + +export const pages: SideBarItem[] = [ + { + name: 'Dashboard', + icon: '/icons/dashboard.svg', + href: '/dashboard' + }, + { + name: 'Transactions', + icon: '/icons/credit-card.svg', + href: '/dashboard/transactions' + }, + { + name: 'Modèles', + icon: '/icons/floppy-disk.svg', + href: '/dashboard/models' + }, + { + name: 'Analyses', + icon: '/icons/magnifying_glass_icon.svg', + href: '/dashboard/analyses' + }, + { + name: 'Personnel', + icon: '/icons/people.svg', + href: '/dashboard/personnel' + }, + { + name: 'Messages', + icon: '/icons/msg.svg', + href: '/dashboard/messages' + }, + { + name: 'Paramètres', + icon: '/icons/settings.svg', + href: '/dashboard/settings' + } +]; + +export function getPageIndex(pathname: string) { + if (pathname === '/dashboard') return 0; + if (pathname.startsWith('/dashboard/transactions')) return 1; + if (pathname.startsWith('/dashboard/models')) return 2; + if (pathname.startsWith('/dashboard/analyses')) return 3; + if (pathname.startsWith('/dashboard/personnel')) return 4; + if (pathname.startsWith('/dashboard/messages')) return 5; + if (pathname.startsWith('/dashboard/settings')) return 6; + return -1; // Not found +}
\ No newline at end of file diff --git a/ping/frontend/src/lib/stores/auth.ts b/ping/frontend/src/lib/stores/auth.ts new file mode 100644 index 0000000..97acaf2 --- /dev/null +++ b/ping/frontend/src/lib/stores/auth.ts @@ -0,0 +1,66 @@ +import { error } from "@sveltejs/kit"; +import { get, writable } from "svelte/store"; + +export interface IUser { + id: string; + login: string; + displayName: string; + avatar: string; + isAdmin: boolean; +} + +export const user = writable<IUser | null>(null); + +export async function getUser() { + const userVal = get(user); + if (userVal) { + return userVal; + } + return getUpdatedUser(); +} + +export async function getUpdatedUser() { + const token = localStorage.getItem("token"); + if (!token) { + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Pas de token"); + } + try { + const userId = JSON.parse(atob(token.split(".")[1])).sub; + + const res = await authFetch(`/api/user/${userId}`); + if (!res.ok) { + throw error(res.status, "Erreur lors de la récupération de l'utilisateur"); + } + const data = await res.json(); + user.set({ ...data }); + return data; + } + catch (e) { + console.error("Erreur lors de la récupération de l'ID utilisateur depuis le token", e); + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Token invalide " + e); + } +} + +// place files you want to import through the `$lib` alias in this folder. +export function authFetch(url: string | URL, options: RequestInit = {}) { + const token = localStorage.getItem("token"); + if (!token) { + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Pas de token"); + } + + const mergedOptions: RequestInit = { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...(options.headers ?? {}) + } + }; + + return fetch(url, mergedOptions); +}
\ No newline at end of file diff --git a/ping/frontend/src/lib/stores/toast.ts b/ping/frontend/src/lib/stores/toast.ts new file mode 100644 index 0000000..454461b --- /dev/null +++ b/ping/frontend/src/lib/stores/toast.ts @@ -0,0 +1,22 @@ +import { writable } from "svelte/store"; + +export interface Toast { + color?: string | undefined; + title?: string | undefined; + message?: string | undefined; +} + +export const toastList = writable<Toast[]>([]); + +export function addToast(toast: Toast) { + toast ??= { color: "red", title: "Error", message: "An error occurred" }; + toast.color ??= "red"; + toast.title ??= "Error"; + toast.message ??= "An error occurred"; + + toastList.update((list) => [...list, toast]); + + setTimeout(() => { + toastList.update((list) => list.filter((t) => t !== toast)); + }, 5000); +}
\ No newline at end of file diff --git a/ping/frontend/src/routes/+layout.svelte b/ping/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..ba995cc --- /dev/null +++ b/ping/frontend/src/routes/+layout.svelte @@ -0,0 +1,9 @@ +<script lang="ts"> + import ToastList from '$lib/components/ToastList.svelte'; + import '../app.css'; + + let { children } = $props(); +</script> + +{@render children()} +<ToastList /> diff --git a/ping/frontend/src/routes/+page.svelte b/ping/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..7088945 --- /dev/null +++ b/ping/frontend/src/routes/+page.svelte @@ -0,0 +1,59 @@ +<script lang="ts"> + import Button from '$lib/components/Button.svelte'; + import NavBar from '$lib/components/NavBar.svelte'; +</script> + +<NavBar pageTitle=""> + <!-- Composant gauche de la navbar --> + {#snippet rightComponent()} + <Button href="/login">Connexion</Button> + {/snippet} +</NavBar> +<header></header> +<section class="colored"></section> +<section id="bottomcard"> + <div class="card"> + <h1>Investissons <b>plus mieux</b> et <b>plus vert</b></h1> + <ul> + <li>Investissez facilement</li> + <li>Analysez les risques et les performances</li> + <li>Soulagez votre éco-conscience</li> + </ul> + </div> +</section> + +<style> + header { + background-image: url('/img/header-bg.jpg'); + height: 80vh; + background-size: cover; + background-position: center; + } + + .colored { + background-color: var(--bg-secondary); + height: 20vh; + } + + #bottomcard { + position: fixed; + bottom: 10vh; + display: flex; + justify-content: center; + align-items: center; + color: white; + } + + #bottomcard h1 { + font-size: 2em; + margin-bottom: 10px; + color: var(--text-lime); + } + + #bottomcard ul { + list-style: square; + padding: 0; + margin: 0; + margin-left: 20px; + } +</style> diff --git a/ping/frontend/src/routes/dashboard/+layout.svelte b/ping/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..a2f7efe --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + import ToastList from '$lib/components/ToastList.svelte'; + import Avatar from '$lib/components/Avatar.svelte'; + import NavBar from '$lib/components/NavBar.svelte'; + import NumberStatList from '$lib/components/NumberStatList.svelte'; + import SideBar from '$lib/components/SideBar.svelte'; + + import '../../app.css'; + import { getPageIndex, pages } from '$lib/pages'; + import { onMount } from 'svelte'; + import { page } from '$app/stores'; + import { user } from '$lib/stores/auth'; + + let { children } = $props(); + + let selectedIndex = $state(0); + + $effect(() => { + selectedIndex = getPageIndex($page.url.pathname); + }); +</script> + +<ToastList /> +<NavBar pageTitle={pages[selectedIndex].name}> + {#snippet rightComponent()} + <div class="flex items-center gap-4"> + <form> + <input type="search" name="search" id="search" placeholder="Search..." /> + </form> + <Avatar username={$user?.login ?? 'No auth'} url={$user?.avatar} /> + </div> + {/snippet} +</NavBar> +<SideBar bind:selectedIndex /> +<section class="dashboard">{@render children()}</section> + +<style> + section.dashboard { + padding-left: 208px; + padding-top: 72px; + width: 100%; + height: calc(100vh); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/+page.svelte b/ping/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..c38e281 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,81 @@ +<script lang="ts"> + import NumberStatList from '$lib/components/NumberStatList.svelte'; + import type { INumberStatList } from '$lib/components/NumberStatList.svelte'; + import RiskAnalysis from '$lib/components/dashboard/RiskAnalysis.svelte'; + import StockGraph from '$lib/components/dashboard/StockGraph.svelte'; + import TrendingSymbols from '$lib/components/dashboard/TrendingSymbols.svelte'; + import { onMount } from 'svelte'; + + const statsList: INumberStatList[] = [ + { + name: 'Portefeuille', + color: 'aqua', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/wallet.svg' + }, + { + name: 'Revenus', + color: '#1FCB4F', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/money-bills.svg' + }, + { + name: 'Depenses', + color: 'orange', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/credit-card.svg' + }, + { + name: 'Eco score', + color: '#1FCB4F', + value: `${Math.random().toFixed(3) * 1000}`, + icon: '/icons/leaf.svg' + } + ]; + + onMount(() => {}); +</script> + +<section class="dashboard-home"> + <NumberStatList {statsList} /> + <div class="home-grid"> + <div class="card overviewGraph"> + <StockGraph /> + </div> + <!-- <div class="card riskAnalysis"> + <RiskAnalysis /> + </div> --> + <!-- <div class="card activity">c</div> --> + <div class="card tendencies"> + <TrendingSymbols /> + </div> + <!-- <div class="card recentInvestments">e</div> --> + </div> +</section> + +<style> + .home-grid { + display: grid; + grid-template-areas: + 'overviewGraph overviewGraph overviewGraph' + 'tendencies tendencies tendencies'; + gap: 16px; + width: calc(100% - 32px); + } + + .overviewGraph { + grid-area: overviewGraph; + } + /* .riskAnalysis { + grid-area: riskAnalysis; + } + .activity { + grid-area: activity; + } */ + .tendencies { + grid-area: tendencies; + } + /* .recentInvestments { + grid-area: recentInvestments; + } */ +</style> diff --git a/ping/frontend/src/routes/dashboard/analyses/+page.svelte b/ping/frontend/src/routes/dashboard/analyses/+page.svelte new file mode 100644 index 0000000..1a8ed47 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/analyses/+page.svelte @@ -0,0 +1,409 @@ +<script lang="ts"> + import type { INumberStatList } from '$lib/components/NumberStatList.svelte'; + import NumberStatList from '$lib/components/NumberStatList.svelte'; + import SteppedLineChart from '$lib/components/SteppedLineChart.svelte'; + import { onMount } from 'svelte'; + + interface Transaction { + id: number; + date: Date; + amount: number; + type: 'buy' | 'sell'; + recipient: string; + company: string; + co2Impact: number; + } + + const mockTransactions: Transaction[] = [ + { id: 1, date: new Date('2025-06-20'), amount: 1200, type: 'buy', recipient: 'Microsoft Corp', company: 'MSFT', co2Impact: 45 }, + { id: 2, date: new Date('2025-06-22'), amount: -800, type: 'sell', recipient: 'Apple Inc', company: 'AAPL', co2Impact: -30 }, + { id: 3, date: new Date('2025-06-24'), amount: 2500, type: 'buy', recipient: 'Tesla Inc', company: 'TSLA', co2Impact: 15 }, + { id: 4, date: new Date('2025-06-26'), amount: 950, type: 'buy', recipient: 'Amazon', company: 'AMZN', co2Impact: 60 }, + { id: 5, date: new Date('2025-06-28'), amount: -1500, type: 'sell', recipient: 'Google', company: 'GOOGL', co2Impact: -40 }, + { id: 6, date: new Date('2025-06-30'), amount: 750, type: 'buy', recipient: 'Netflix', company: 'NFLX', co2Impact: 25 }, + { id: 7, date: new Date('2025-07-01'), amount: 1800, type: 'buy', recipient: 'Microsoft Corp', company: 'MSFT', co2Impact: 50 }, + { id: 8, date: new Date('2025-06-15'), amount: -600, type: 'sell', recipient: 'Tesla Inc', company: 'TSLA', co2Impact: -10 }, + { id: 9, date: new Date('2025-06-18'), amount: 3200, type: 'buy', recipient: 'NVIDIA', company: 'NVDA', co2Impact: 80 }, + { id: 10, date: new Date('2025-06-25'), amount: 450, type: 'buy', recipient: 'Apple Inc', company: 'AAPL', co2Impact: 20 } + ]; + + let transactions = mockTransactions; + let selectedPeriod = 30; + let analysisData: any = {}; + + const thresholds = { + dailySpendingLimit: 1000, + monthlySpendingLimit: 5000, + co2Limit: 100, + profitTarget: 2000 + }; + + function calculateStats() { + const now = new Date(); + const cutoffDate = new Date(now.getTime() - selectedPeriod * 24 * 60 * 60 * 1000); + const filteredTransactions = transactions.filter(t => t.date >= cutoffDate); + + const totalAmount = filteredTransactions.reduce((sum, t) => sum + t.amount, 0); + const averageAmount = filteredTransactions.length > 0 ? totalAmount / filteredTransactions.length : 0; + const uniqueRecipients = new Set(filteredTransactions.map(t => t.recipient)).size; + const totalCO2 = filteredTransactions.reduce((sum, t) => sum + t.co2Impact, 0); + + const recipientTotals = filteredTransactions.reduce((acc, t) => { + acc[t.recipient] = (acc[t.recipient] || 0) + t.amount; + return acc; + }, {} as Record<string, number>); + + const dailyData = filteredTransactions.reduce((acc, t) => { + const date = t.date.toISOString().split('T')[0]; + acc[date] = (acc[date] || 0) + t.amount; + return acc; + }, {} as Record<string, number>); + + const chartData = Object.entries(dailyData) + .map(([date, amount]) => ({ + label: new Date(date).toLocaleDateString('fr-FR', { month: 'short', day: 'numeric' }), + value: amount + })) + .sort((a, b) => a.label.localeCompare(b.label)); + + const recipientChartData = Object.entries(recipientTotals).map(([recipient, total]) => ({ + x: recipient, + y: Math.abs(total) + })).sort((a, b) => b.y - a.y); + + const thresholdAlerts = { + dailySpending: Math.abs(totalAmount / selectedPeriod) > thresholds.dailySpendingLimit, + monthlySpending: Math.abs(totalAmount * (30 / selectedPeriod)) > thresholds.monthlySpendingLimit, + co2Impact: totalCO2 > thresholds.co2Limit, + profitTarget: totalAmount >= thresholds.profitTarget + }; + + analysisData = { + totalAmount, + averageAmount, + uniqueRecipients, + totalCO2, + chartData, + recipientChartData, + thresholdAlerts, + filteredTransactions + }; + } + + $: statsList = [ + { + name: 'Total Transactions', + color: 'aqua', + value: `${analysisData.totalAmount ? analysisData.totalAmount.toFixed(0) : '0'}€`, + icon: '/icons/wallet.svg' + }, + { + name: 'Moyenne', + color: '#1FCB4F', + value: `${analysisData.averageAmount ? analysisData.averageAmount.toFixed(0) : '0'}€`, + icon: '/icons/money-bills.svg' + }, + { + name: 'Destinataires', + color: 'orange', + value: `${analysisData.uniqueRecipients || 0}`, + icon: '/icons/people.svg' + }, + { + name: 'Impact CO2', + color: analysisData.totalCO2 > 0 ? '#ff6b6b' : '#1FCB4F', + value: `${analysisData.totalCO2 ? analysisData.totalCO2.toFixed(0) : '0'}g`, + icon: '/icons/leaf.svg' + } + ]; + + onMount(() => calculateStats()); + $: selectedPeriod && calculateStats(); +</script> + +<section id="analyses"> + <div class="controls"> + <h1>Analyses des Transactions</h1> + <div class="period-selector"> + <label for="period">Période d'analyse:</label> + <select bind:value={selectedPeriod} id="period"> + <option value={7}>7 derniers jours</option> + <option value={30}>30 derniers jours</option> + <option value={90}>3 derniers mois</option> + <option value={365}>1 an</option> + </select> + </div> + </div> + + <NumberStatList {statsList} /> + + {#if analysisData.thresholdAlerts} + <div class="threshold-alerts"> + {#if analysisData.thresholdAlerts.dailySpending} + <div class="alert alert-warning"> + ⚠️ Dépenses quotidiennes élevées ({(Math.abs(analysisData.totalAmount) / selectedPeriod).toFixed(0)}€/jour) + </div> + {/if} + {#if analysisData.thresholdAlerts.co2Impact} + <div class="alert alert-danger"> + 🌍 Impact CO2 élevé ({analysisData.totalCO2}g) + </div> + {/if} + {#if analysisData.thresholdAlerts.profitTarget} + <div class="alert alert-success"> + 🎯 Objectif de profit atteint ({analysisData.totalAmount.toFixed(0)}€) + </div> + {/if} + </div> + {/if} + + <div id="analysis-grid"> + <div class="card"> + <h3>Transactions dans le temps</h3> + {#if analysisData.chartData?.length > 0} + <SteppedLineChart color="#1FCB4F" data={analysisData.chartData} title="Évolution" legend="Montant (€)" /> + {:else} + <p style="color: #888; text-align: center; padding: 20px;">Aucune donnée</p> + {/if} + </div> + + <div class="card"> + <h3>Top destinataires</h3> + <div class="recipients-chart"> + {#each (analysisData.recipientChartData || []).slice(0, 5) as recipient} + <div class="recipient-bar"> + <span class="recipient-name">{recipient.x}</span> + <div class="bar-container"> + {#if analysisData.recipientChartData?.length > 0} + {@const maxValue = Math.max(...analysisData.recipientChartData.map(r => r.y))} + <div class="bar" style="width: {(recipient.y / maxValue) * 100}%"></div> + {:else} + <div class="bar" style="width: 0%"></div> + {/if} + <span class="recipient-amount">{recipient.y.toFixed(0)}€</span> + </div> + </div> + {/each} + </div> + </div> + + <div class="card"> + <h3>Répartition des transactions</h3> + <div class="transaction-breakdown"> + <div class="breakdown-item"> + <span class="breakdown-label">Achats:</span> + <span class="breakdown-value positive"> + {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'buy').length} + </span> + </div> + <div class="breakdown-item"> + <span class="breakdown-label">Ventes:</span> + <span class="breakdown-value negative"> + {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'sell').length} + </span> + </div> + <div class="breakdown-item"> + <span class="breakdown-label">Volume total:</span> + <span class="breakdown-value"> + {Math.abs(analysisData.totalAmount || 0).toFixed(0)}€ + </span> + </div> + </div> + </div> + + <div class="card"> + <h3>Métriques CO2</h3> + <div class="co2-metrics"> + <div class="co2-item"> + <span class="co2-label">Impact total:</span> + <span class="co2-value {analysisData.totalCO2 > 0 ? 'negative' : 'positive'}"> + {analysisData.totalCO2 || 0}g CO2 + </span> + </div> + <div class="co2-item"> + <span class="co2-label">Par transaction:</span> + <span class="co2-value"> + {(analysisData.filteredTransactions?.length ? analysisData.totalCO2 / analysisData.filteredTransactions.length : 0).toFixed(1)}g + </span> + </div> + </div> + </div> + </div> +</section> + +<style> + .card { + margin: 0; + background-color: var(--bg-primary); + border-radius: 8px; + padding: 16px; + color: white; + } + + .card h3 { + margin: 0 0 16px 0; + color: var(--text-lime); + font-size: 18px; + font-weight: 600; + } + + #analyses { + display: flex; + flex-direction: column; + padding-right: 12px; + background-color: var(--bg-secondary); + min-height: calc(100vh - 72px); + color: white; + } + + .controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 12px; + margin-bottom: 8px; + } + + .controls h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + .period-selector { + display: flex; + align-items: center; + gap: 8px; + } + + .period-selector label { + font-size: 14px; + color: white; + } + + .period-selector select { + background-color: var(--bg-primary); + color: white; + border: 1px solid #444; + border-radius: 4px; + padding: 8px 12px; + font-size: 14px; + } + + .threshold-alerts { + padding: 0 12px; + margin-bottom: 16px; + } + + .alert { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 8px; + font-size: 14px; + font-weight: 500; + } + + .alert-warning { + background-color: rgba(255, 193, 7, 0.2); + border-left: 4px solid #ffc107; + color: #ffc107; + } + + .alert-danger { + background-color: rgba(220, 53, 69, 0.2); + border-left: 4px solid #dc3545; + color: #dc3545; + } + + .alert-success { + background-color: rgba(40, 167, 69, 0.2); + border-left: 4px solid #28a745; + color: #28a745; + } + + #analysis-grid { + padding: 0 12px; + width: 100%; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } + + .recipients-chart { + display: flex; + flex-direction: column; + gap: 12px; + } + + .recipient-bar { + display: flex; + flex-direction: column; + gap: 4px; + } + + .recipient-name { + font-size: 12px; + color: #888; + font-weight: 500; + } + + .bar-container { + display: flex; + align-items: center; + gap: 8px; + position: relative; + } + + .bar { + height: 20px; + background: linear-gradient(90deg, var(--text-lime), #1FCB4F); + border-radius: 4px; + transition: width 0.3s ease; + } + + .recipient-amount { + font-size: 12px; + color: white; + font-weight: 600; + min-width: 60px; + text-align: right; + } + + .transaction-breakdown, .co2-metrics { + display: flex; + flex-direction: column; + gap: 12px; + } + + .breakdown-item, .co2-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #333; + } + + .breakdown-item:last-child, .co2-item:last-child { + border-bottom: none; + } + + .breakdown-label, .co2-label { + font-size: 14px; + color: #888; + } + + .breakdown-value, .co2-value { + font-size: 16px; + font-weight: 600; + color: white; + } + + .breakdown-value.positive, .co2-value.positive { + color: var(--text-lime); + } + + .breakdown-value.negative, .co2-value.negative { + color: #ff6b6b; + } +</style> diff --git a/ping/frontend/src/routes/dashboard/messages/+page.svelte b/ping/frontend/src/routes/dashboard/messages/+page.svelte new file mode 100644 index 0000000..52869e4 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/messages/+page.svelte @@ -0,0 +1,403 @@ +<script lang="ts"> + import type { PageData } from './$types'; + + let { data }: { data: PageData } = $props(); + + let searchQuery = ''; + let selectedMessage = null; + let newMessage = ''; + let messages = [ + { + id: 1, + sender: 'Équipe Ping', + subject: 'Nouvelle analyse de portefeuille disponible', + content: 'Votre analyse mensuelle de performance ESG est maintenant disponible. Consultez les dernières métriques de votre portefeuille.', + date: new Date('2024-06-28'), + unread: true + }, + { + id: 2, + sender: 'Système', + subject: 'Alerte: Score ESG en baisse', + content: 'Le score ESG de votre portefeuille a diminué de 5% ce mois-ci. Nous recommandons de réviser vos investissements.', + date: new Date('2024-06-25'), + unread: true + }, + { + id: 3, + sender: 'Support', + subject: 'Mise à jour des modèles prédictifs', + content: 'Nos modèles d\'analyse ont été mis à jour avec les dernières données du marché. Vos prédictions sont maintenant plus précises.', + date: new Date('2024-06-20'), + unread: false + } + ]; + + function handleSearch() { + console.log('Searching messages:', searchQuery); + } + + function selectMessage(message: any) { + selectedMessage = message; + if (message.unread) { + message.unread = false; + messages = [...messages]; + } + } + + function sendMessage() { + if (newMessage.trim()) { + console.log('Sending message:', newMessage); + newMessage = ''; + } + } + + function deleteMessage(messageId: number) { + messages = messages.filter(m => m.id !== messageId); + if (selectedMessage?.id === messageId) { + selectedMessage = null; + } + } +</script> + +<section id="messages"> + <div class="messages-container"> + <!-- Header --> + <div class="messages-header"> + <h1>Messages</h1> + <div class="search-bar"> + <input + type="text" + placeholder="Rechercher dans les messages..." + bind:value={searchQuery} + on:input={handleSearch} + /> + <button class="search-btn">🔍</button> + </div> + </div> + + <div class="messages-layout"> + <!-- Messages List --> + <div class="messages-list card"> + <h2>Boîte de réception</h2> + <div class="message-items"> + {#each messages as message} + <div + class="message-item {message.unread ? 'unread' : ''} {selectedMessage?.id === message.id ? 'selected' : ''}" + on:click={() => selectMessage(message)} + > + <div class="message-header"> + <span class="message-sender">{message.sender}</span> + <span class="message-date">{message.date.toLocaleDateString()}</span> + </div> + <div class="message-subject">{message.subject}</div> + <div class="message-preview">{message.content.substring(0, 80)}...</div> + {#if message.unread} + <div class="unread-indicator"></div> + {/if} + </div> + {/each} + </div> + </div> + + <!-- Message Detail --> + <div class="message-detail card"> + {#if selectedMessage} + <div class="message-detail-header"> + <h3>{selectedMessage.subject}</h3> + <div class="message-actions"> + <button class="btn" on:click={() => deleteMessage(selectedMessage.id)}> + 🗑️ Supprimer + </button> + </div> + </div> + <div class="message-meta"> + <span>De: {selectedMessage.sender}</span> + <span>Date: {selectedMessage.date.toLocaleDateString()}</span> + </div> + <div class="message-content"> + {selectedMessage.content} + </div> + + <!-- Reply Section --> + <div class="reply-section"> + <h4>Répondre</h4> + <textarea + bind:value={newMessage} + placeholder="Tapez votre réponse..." + rows="4" + ></textarea> + <button class="btn" on:click={sendMessage}> + 📤 Envoyer + </button> + </div> + {:else} + <div class="no-message-selected"> + <h3>Sélectionnez un message</h3> + <p>Choisissez un message dans la liste pour le lire</p> + </div> + {/if} + </div> + </div> + </div> +</section> + +<style> + #messages { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .messages-container { + max-width: 1400px; + } + + /* Header */ + .messages-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 16px 0; + font-weight: 600; + } + + .search-bar { + display: flex; + align-items: center; + background-color: var(--bg-primary); + border-radius: 8px; + padding: 4px; + gap: 8px; + } + + .search-bar input { + background: transparent; + border: none; + color: white; + padding: 12px 16px; + font-size: 14px; + outline: none; + width: 300px; + } + + .search-bar input::placeholder { + color: #888888; + } + + .search-btn { + background: transparent; + border: none; + color: #888888; + padding: 8px; + cursor: pointer; + font-size: 16px; + } + + /* Layout */ + .messages-layout { + display: flex; + gap: 16px; + height: calc(100vh - 200px); + } + + .card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + /* Messages List */ + .messages-list { + flex: 1; + max-width: 400px; + } + + .message-items { + display: flex; + flex-direction: column; + gap: 8px; + max-height: calc(100vh - 300px); + overflow-y: auto; + } + + .message-item { + padding: 12px; + border-radius: 8px; + background-color: var(--bg-secondary); + cursor: pointer; + transition-duration: 0.2s; + position: relative; + border-left: 3px solid transparent; + } + + .message-item:hover { + background-color: var(--btn-primary); + transform: scale(1.02); + } + + .message-item.selected { + border-left-color: var(--text-lime); + background-color: var(--btn-primary); + } + + .message-item.unread { + background-color: rgba(0, 255, 119, 0.1); + } + + .message-header { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + } + + .message-sender { + font-weight: 600; + color: var(--text-lime); + } + + .message-date { + font-size: 12px; + color: #888888; + } + + .message-subject { + font-weight: 500; + margin-bottom: 4px; + font-size: 14px; + } + + .message-preview { + font-size: 13px; + color: #888888; + } + + .unread-indicator { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + background-color: var(--text-lime); + border-radius: 50%; + } + + /* Message Detail */ + .message-detail { + flex: 2; + } + + .message-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--btn-primary); + } + + .message-detail-header h3 { + margin: 0; + color: white; + font-size: 20px; + } + + .message-actions { + display: flex; + gap: 8px; + } + + .message-meta { + display: flex; + gap: 20px; + margin-bottom: 20px; + font-size: 14px; + color: #888888; + } + + .message-content { + line-height: 1.6; + margin-bottom: 30px; + padding: 20px; + background-color: var(--bg-secondary); + border-radius: 8px; + } + + .reply-section { + border-top: 1px solid var(--btn-primary); + padding-top: 20px; + } + + .reply-section h4 { + margin: 0 0 12px 0; + color: var(--text-lime); + } + + textarea { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + resize: vertical; + box-sizing: border-box; + margin-bottom: 12px; + } + + textarea::placeholder { + color: #888888; + } + + textarea:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + .no-message-selected { + text-align: center; + padding: 60px 20px; + color: #888888; + } + + .no-message-selected h3 { + margin: 0 0 12px 0; + color: #888888; + } + + .btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; + } + + .btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/models/+page.svelte b/ping/frontend/src/routes/dashboard/models/+page.svelte new file mode 100644 index 0000000..3be6f00 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/models/+page.svelte @@ -0,0 +1,568 @@ +<script lang="ts"> + import Avatar from '$lib/components/Avatar.svelte'; + import UserSelector from '$lib/components/input/UserSelector.svelte'; + import { authFetch, getUser, type IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + interface IModel { + id: string; + members: IUser[]; + name: string; + owner: IUser; + } + + let modelName = $state(''); + let sources = $state([]); + let models = $state<IModel[]>([]); + let modelToEdit = $state<number | null>(null); + let modelOwner = $state<IUser | null>(null); + + let users = $state<IUser[]>([]); + + function addSource() { + sources = [...sources, '']; + } + + function removeSource(index: number) { + sources = sources.filter((_, i) => i !== index); + } + + function handleSubmit(e: SubmitEvent) { + e.preventDefault(); + if (modelToEdit === null) { + createModel(); + } else { + updateModel(); + } + } + + async function createModel() { + try { + const response = await authFetch('/api/projects', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + }, + body: JSON.stringify({ + name: modelName + }) + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + models = [...models, data]; + await Promise.all( + users.map((user) => + authFetch(`/api/projects/${data.id}/add-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: '*/*' + }, + body: JSON.stringify({ userId: user.id }) + }) + .then((res) => { + if (!res.ok) { + addToast({ + color: 'red', + title: 'Erreur', + message: `Impossible d'ajouter l'utilisateur ${user.displayName} au modèle "${modelName}".` + }); + console.error(`HTTP error! status: ${res.status}`); + } + return res.json().catch(() => null); + }) + .then((memberData) => { + console.log('Member added:', memberData); + }) + .catch((err) => { + console.error('Error adding member:', err); + }) + ) + ); + addToast({ + color: 'green', + title: 'Modèle créé', + message: `Le modèle "${modelName}" a été créé avec succès.` + }); + modelName = ''; + return data; + } catch (error) { + console.error('Error creating model:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: `Une erreur est survenue lors de la création du modèle "${modelName}".` + }); + throw error; + } + } + + async function updateModel() { + try { + if (modelToEdit === null || models.length <= modelToEdit) { + addToast({ + color: 'red', + title: 'Erreur', + message: 'Aucun modèle sélectionné pour la mise à jour.' + }); + return; + } + const model = models[modelToEdit]; + console.log('Updating model:', model); + const response = await authFetch(`/api/projects/${model.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + }, + body: JSON.stringify({ + name: modelName + }) + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const updatedModel = await response.json(); + models = models.map((m, i) => (i === modelToEdit ? updatedModel : m)); + modelToEdit = null; + modelName = ''; + addToast({ + color: 'green', + title: 'Modèle mis à jour', + message: `Le modèle "${updatedModel.name}" a été mis à jour avec succès.` + }); + } catch (error) { + console.error('Error updating model:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: `Une erreur est survenue lors de la mise à jour du modèle "${modelName}".` + }); + throw error; + } + } + + async function getMyModels() { + try { + const response = await authFetch('/api/projects', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + } + }); + const data = await response.json(); + console.log('My models:', data); + return data; + } catch (error) { + console.error('Error fetching models:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: 'Impossible de charger les modèles.' + }); + throw error; + } + } + + function deleteModel(i: number) { + if (models.length <= i || i < 0) { + addToast({ + color: 'red', + title: 'Erreur', + message: 'Modèle introuvable.' + }); + return; + } + + modelToEdit = null; + + const modelToDelete = models[i]; + authFetch(`/api/projects/${modelToDelete.id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + } + }) + .then(() => { + models = models.filter((_, index) => index !== i); + addToast({ + color: 'green', + title: 'Modèle supprimé', + message: `Le modèle "${modelToDelete.name}" a été supprimé avec succès.` + }); + }) + .catch((err) => { + console.error('Error deleting model:', err); + addToast({ + color: 'red', + title: 'Erreur', + message: `Impossible de supprimer le modèle "${modelToDelete.name}".` + }); + }); + } + + function onEditButtonPressed(i: number) { + if (modelToEdit === i) { + modelToEdit = null; + modelName = ''; + users = []; + modelOwner = null; + } else { + modelToEdit = i; + modelName = models[i].name; + users = models[i].members || []; + modelOwner = models[i].owner || null; + } + } + + let currentUser = $state<IUser | null>(null); + + onMount(() => { + getMyModels() + .then((lsModels) => { + models = lsModels; + console.log('Models loaded:', models); + }) + .catch((err) => { + console.error('Error loading models:', err); + }); + getUser() + .then((user) => { + currentUser = user; + console.log('Current user:', currentUser); + }) + .catch((err) => { + console.error('Error fetching current user:', err); + }); + }); +</script> + +<section id="models"> + <div class="models-container"> + <div class="active-models"> + <h2>Modèles actifs</h2> + <div class="models-list"> + {#each models as m, i} + <div class="model-item" class:selected={modelToEdit === i}> + <div class="model-name"> + <span>{m.name}</span><br /> + <code style="font-size:8px">{m.id}</code> + </div> + <div class="model-metrics"> + <div class="metric"> + <span class="metric-label">Eco score</span> + <span class="metric-value green">{69} %</span> + </div> + <div class="metric"> + <span class="metric-label">Efficacité</span> + <span class="metric-value green">{69} %</span> + </div> + </div> + <button class="modify-btn" onclick={() => onEditButtonPressed(i)}>✏️</button> + <button class="delete-btn btn" onclick={() => deleteModel(i)}>🗑️</button> + </div> + {/each} + </div> + </div> + + <div class="new-model"> + <form onsubmit={handleSubmit}> + <div class="new-model-header"> + {#if modelToEdit !== null} + <h2>Modifier un modèle</h2> + {:else} + <h2>Nouveau modèle</h2> + {/if} + <button class="register-btn" type="submit">📁 Enregistrer</button> + </div> + <div class="form-group"> + <label for="name">Nom du modèle</label> + <input type="text" id="name" bind:value={modelName} placeholder="Modèle X" /> + <div class="flex gap-2 p-2"> + {#if modelToEdit !== null} + <Avatar username={modelOwner?.displayName} url={modelOwner?.avatar} /> + {:else if currentUser} + <Avatar username={currentUser.displayName} url={currentUser.avatar} /> + {:else} + Chargement en cours... + {/if} + <UserSelector bind:users /> + </div> + </div> + + <div class="sources-section"> + <div class="sources-header"> + <span>Sources de données</span> + <button type="button" class="add-source-btn" onclick={addSource}> + ➕ Ajouter une source de données + </button> + </div> + + {#each sources as source, index} + <div class="source-item"> + <!-- svelte-ignore a11y_label_has_associated_control --> + <label>Source {index + 1}</label> + <div class="source-input-group"> + <input type="text" bind:value={sources[index]} placeholder="Source" /> + <button type="button" class="remove-source-btn" onclick={() => removeSource(index)}> + 🗑️ + </button> + </div> + </div> + {/each} + </div> + </form> + </div> + </div> +</section> + +<style> + #models { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .models-container { + display: flex; + gap: 16px; + max-width: 1400px; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 16px 0; + font-weight: 600; + } + + /* Modèles actifs */ + .active-models { + flex: 1; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + .models-list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .model-item { + background-color: var(--bg-secondary); + padding: 16px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + border: 2px solid transparent; + } + + .model-item.selected { + border: 2px solid var(--text-lime); + } + + .model-name { + font-weight: 500; + font-size: 16px; + color: white; + } + + .model-metrics { + display: flex; + gap: 30px; + } + + .metric { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + } + + .metric-label { + font-size: 12px; + color: #888888; + } + + .metric-value { + font-size: 18px; + font-weight: bold; + } + + .metric-value.green { + color: var(--text-lime); + } + + .modify-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition-duration: 0.2s; + } + + .modify-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + /* Nouveau modèle */ + .new-model { + flex: 1; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + .new-model-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + .register-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition-duration: 0.2s; + } + + .register-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .form-group { + margin-bottom: 16px; + } + + label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: white; + font-weight: 500; + } + + input[type='text'] { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + box-sizing: border-box; + } + + input[type='text']:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + input[type='text']::placeholder { + color: #888888; + } + + /* Sources section */ + .sources-section { + margin-top: 16px; + } + + .sources-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + } + + .sources-header span { + font-size: 14px; + font-weight: 500; + color: white; + } + + .add-source-btn { + background-color: var(--btn-secondary); + color: var(--text-lime); + border: 1px solid var(--text-lime); + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition-duration: 0.2s; + } + + .add-source-btn:hover { + background-color: var(--text-lime); + color: var(--bg-primary); + transform: scale(1.025); + } + + .source-item { + margin-bottom: 15px; + } + + .source-item label { + margin-bottom: 6px; + font-size: 12px; + color: #888888; + } + + .source-input-group { + display: flex; + gap: 10px; + align-items: center; + } + + .source-input-group input { + flex: 1; + } + + .remove-source-btn { + background-color: var(--btn-secondary); + color: #888888; + border: none; + padding: 8px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition-duration: 0.2s; + min-width: 40px; + } + + .remove-source-btn:hover { + color: #ef4444; + background-color: var(--btn-primary); + transform: scale(1.025); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/personnel/+page.svelte b/ping/frontend/src/routes/dashboard/personnel/+page.svelte new file mode 100644 index 0000000..a1722c1 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/personnel/+page.svelte @@ -0,0 +1,290 @@ +<script lang="ts"> + import Avatar from '$lib/components/Avatar.svelte'; + import UserItem from '$lib/components/UserItem.svelte'; + import { authFetch, getUpdatedUser, getUser, type IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + + let user = $state<IUser | null>(null); + let allUsers = $state<IUser[]>([]); + + let idValue = $state<string>(''); + let urlValue = $state<string>(''); + let nameValue = $state<string>(''); + let passwordValue = $state<string>(''); + + async function loadUser() { + try { + user = await getUser(); + if (!user) { + addToast({ title: 'Erreur', message: 'Utilisateur non trouvé.' }); + return null; + } + idValue = user.id; + urlValue = user.avatar || ''; + nameValue = user.displayName || ''; + return user; + } catch (error) { + user = null; + addToast({ title: 'Erreur', message: 'Impossible de charger les informations utilisateur.' }); + return null; + } + } + + async function loadAllUsers() { + try { + const response = await authFetch('/api/user/all'); + if (!response.ok) { + throw new Error('Erreur lors du chargement des utilisateurs'); + } + const users = await response.json(); + return users; + } catch (error) { + addToast({ title: 'Erreur', message: 'Impossible de charger la liste des utilisateurs.' }); + return []; + } + } + + function onEditUser(event: Event) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + + const { id, avatar, username, password } = Object.fromEntries(formData.entries()); + + authFetch(`/api/user/${id}`, { + method: 'PUT', + body: JSON.stringify({ + avatar: avatar || user?.avatar, + displayName: username || user?.displayName, + password: password || undefined + }), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((res) => res.json()) + .then((data) => { + getUpdatedUser().then((updatedUser) => { + user = updatedUser; + }); + console.log('Utilisateur mis à jour', data); + addToast({ + title: 'Succès', + message: 'Informations utilisateur mises à jour avec succès.', + color: 'green' + }); + }) + .catch((error) => { + addToast({ + title: 'Erreur', + message: 'Échec de la mise à jour des informations utilisateur.' + }); + }); + } + + function onCreateUser(event: Event) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + const { login, password, isAdmin } = Object.fromEntries(formData.entries()); + + authFetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + login, + password, + isAdmin: isAdmin === 'on' + }) + }) + .then(async (res) => { + if (!res.ok) { + addToast({ + title: 'Erreur', + message: "Impossible de créer l'utilisateur." + }); + throw new Error("Erreur lors de la création de l'utilisateur"); + } + const newUser = await res.json(); + allUsers = [...allUsers, newUser]; + addToast({ + title: 'Succès', + message: 'Nouvel utilisateur créé avec succès.', + color: 'green' + }); + form.reset(); + }) + .catch(() => { + addToast({ + title: 'Erreur', + message: "Impossible de créer l'utilisateur." + }); + }); + } + + onMount(async () => { + loadUser().then((user) => { + if (user && user.isAdmin) + loadAllUsers().then((users) => { + allUsers = users; + }); + }); + }); +</script> + +{#if user} + <div id="personnel"> + <div> + <section id="myUser" class="card"> + <h2>Mon compte</h2> + <Avatar username={user.displayName} url={user.avatar || '/img/default-avatar.png'} /> + <p><b>Nom</b>: {user.displayName}</p> + <p><b>Login</b>: {user.login}</p> + <p><b>Role</b>: {user.isAdmin ? 'ADMIN' : 'USER'}</p> + <p><b>Id</b>: {user.id}</p> + </section> + <section id="userEdit" class="card"> + <h2>Modifier</h2> + <i + >Laisser vide pour ne pas modifier<br /> + {#if user.isAdmin} + <p>Vous pouvez modifier les informations de n'importe quel utilisateur.</p> + {:else} + <p>Vous ne pouvez modifier que vos propres informations.</p> + {/if} + </i><br /> + <form onsubmit={onEditUser}> + {#if user.isAdmin} + <label for="id">ID de l'utilisateur</label> + <input type="text" placeholder="ID de l'utilisateur" name="id" bind:value={idValue} /> + {/if} + <label for="id">URL de l'avatar</label> + <input type="text" placeholder="URL de l'avatar" name="avatar" bind:value={urlValue} /> + <label for="id">Nom d'utilisateur</label> + <input + type="text" + placeholder="Nom d'utilisateur" + name="username" + bind:value={nameValue} + /> + <label for="id">Mot de passe</label> + <input + type="password" + placeholder="Mot de passe" + name="password" + bind:value={passwordValue} + /> + <button type="submit" class="btn">Modifier</button> + </form> + </section> + </div> + + {#if user.isAdmin} + <section id="adminActions"> + <div class="card"> + <h2>Créer un nouvel utilisateur</h2> + <form class="flex flex-col gap-2" onsubmit={onCreateUser}> + <label for="new-login">Login</label> + <input type="text" id="new-login" name="login" placeholder="Login" required /> + + <label for="new-password">Mot de passe</label> + <input + type="password" + id="new-password" + name="password" + placeholder="Mot de passe" + required + /> + + <label for="new-isAdmin"> + <input type="checkbox" id="new-isAdmin" name="isAdmin" /> + Administrateur + </label> + + <button type="submit" class="btn">Créer</button> + </form> + </div> + <div class="card"> + <h2>Liste des utilisateurs</h2> + <div> + {#each allUsers as u} + <UserItem + user={u} + onEdit={() => { + idValue = u.id; + urlValue = u.avatar || ''; + nameValue = u.displayName || ''; + }} + onDelete={() => { + authFetch(`/api/user/${u.id}`, { + method: 'DELETE' + }) + .then(async (res) => { + if (!res.ok) { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + throw new Error("Erreur lors de la suppression de l'utilisateur"); + } + allUsers = allUsers.filter((user) => user.id !== u.id); + addToast({ + title: 'Succès', + message: 'Utilisateur supprimé avec succès.', + color: 'green' + }); + }) + .catch(() => { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + }); + }} + /> + {/each} + </div> + </div> + </section> + {/if} + </div> +{:else} + <p>Chargement des informations utilisateur...</p> +{/if} + +<style> + * { + color: white; + } + + #personnel { + width: calc(100% - 32px); + display: flex; + gap: 16px; + } + + #personnel > * { + flex: 1; + } + + #myUser { + display: flex; + flex-direction: column; + } + + #userEdit form { + display: flex; + flex-direction: column; + gap: 8px; + } + + h2 { + margin: 0; + font-size: 1.5em; + font-weight: 600; + color: var(--text-lime); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/settings/+page.svelte b/ping/frontend/src/routes/dashboard/settings/+page.svelte new file mode 100644 index 0000000..99c5d8f --- /dev/null +++ b/ping/frontend/src/routes/dashboard/settings/+page.svelte @@ -0,0 +1,452 @@ +<script lang="ts"> + import type { PageData } from './$types'; + + let { data }: { data: PageData } = $props(); + + let userProfile = { + name: 'Baptiste', + email: 'baptiste@ping.com', + company: 'Ping Analytics', + phone: '+33 6 12 34 56 78' + }; + + let notifications = { + emailAlerts: true, + portfolioUpdates: true, + esgAlerts: true, + marketNews: false, + weeklyReports: true + }; + + let security = { + twoFactorAuth: false, + sessionTimeout: '30', + dataEncryption: true + }; + + let display = { + theme: 'dark', + language: 'fr', + currency: 'EUR', + dateFormat: 'DD/MM/YYYY' + }; + + let currentPassword = ''; + let newPassword = ''; + let confirmPassword = ''; + + function saveProfile() { + console.log('Saving profile:', userProfile); + } + + function saveNotifications() { + console.log('Saving notifications:', notifications); + } + + function saveSecurity() { + console.log('Saving security settings:', security); + } + + function saveDisplay() { + console.log('Saving display settings:', display); + } + + function changePassword() { + if (newPassword !== confirmPassword) { + alert('Les mots de passe ne correspondent pas'); + return; + } + console.log('Changing password'); + currentPassword = ''; + newPassword = ''; + confirmPassword = ''; + } + + function exportData() { + console.log('Exporting user data'); + } + + function deleteAccount() { + if (confirm('Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.')) { + console.log('Deleting account'); + } + } +</script> + +<section id="settings"> + <div class="settings-container"> + <div class="settings-header"> + <h1>Paramètres</h1> + </div> + + <div class="settings-layout"> + <div class="settings-section card"> + <h2>👤 Profil utilisateur</h2> + <form on:submit|preventDefault={saveProfile}> + <div class="form-row"> + <div class="form-group"> + <label for="name">Nom complet</label> + <input type="text" id="name" bind:value={userProfile.name} /> + </div> + <div class="form-group"> + <label for="email">Email</label> + <input type="email" id="email" bind:value={userProfile.email} /> + </div> + </div> + <div class="form-row"> + <div class="form-group"> + <label for="company">Entreprise</label> + <input type="text" id="company" bind:value={userProfile.company} /> + </div> + <div class="form-group"> + <label for="phone">Téléphone</label> + <input type="tel" id="phone" bind:value={userProfile.phone} /> + </div> + </div> + <button type="submit" class="btn">💾 Sauvegarder le profil</button> + </form> + </div> + + <div class="settings-section card"> + <h2>🔔 Notifications</h2> + <form on:submit|preventDefault={saveNotifications}> + <div class="checkbox-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.emailAlerts} /> + <span>Alertes par email</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.portfolioUpdates} /> + <span>Mises à jour du portefeuille</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.esgAlerts} /> + <span>Alertes ESG</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.marketNews} /> + <span>Actualités du marché</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.weeklyReports} /> + <span>Rapports hebdomadaires</span> + </label> + </div> + <button type="submit" class="btn">💾 Sauvegarder les notifications</button> + </form> + </div> + + <div class="settings-section card"> + <h2>🔒 Sécurité</h2> + <form on:submit|preventDefault={saveSecurity}> + <div class="form-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={security.twoFactorAuth} /> + <span>Authentification à deux facteurs</span> + </label> + </div> + <div class="form-group"> + <label for="sessionTimeout">Délai d'expiration de session (minutes)</label> + <select id="sessionTimeout" bind:value={security.sessionTimeout}> + <option value="15">15 minutes</option> + <option value="30">30 minutes</option> + <option value="60">1 heure</option> + <option value="120">2 heures</option> + </select> + </div> + <div class="form-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={security.dataEncryption} disabled /> + <span>Chiffrement des données (toujours activé)</span> + </label> + </div> + <button type="submit" class="btn">💾 Sauvegarder la sécurité</button> + </form> + + <div class="password-section"> + <h3>Changer le mot de passe</h3> + <form on:submit|preventDefault={changePassword}> + <div class="form-group"> + <label for="currentPassword">Mot de passe actuel</label> + <input type="password" id="currentPassword" bind:value={currentPassword} /> + </div> + <div class="form-group"> + <label for="newPassword">Nouveau mot de passe</label> + <input type="password" id="newPassword" bind:value={newPassword} /> + </div> + <div class="form-group"> + <label for="confirmPassword">Confirmer le nouveau mot de passe</label> + <input type="password" id="confirmPassword" bind:value={confirmPassword} /> + </div> + <button type="submit" class="btn">🔑 Changer le mot de passe</button> + </form> + </div> + </div> + + <div class="settings-section card"> + <h2>🎨 Affichage</h2> + <form on:submit|preventDefault={saveDisplay}> + <div class="form-row"> + <div class="form-group"> + <label for="theme">Thème</label> + <select id="theme" bind:value={display.theme}> + <option value="dark">Sombre</option> + <option value="light">Clair</option> + <option value="auto">Automatique</option> + </select> + </div> + <div class="form-group"> + <label for="language">Langue</label> + <select id="language" bind:value={display.language}> + <option value="fr">Français</option> + <option value="en">English</option> + <option value="es">Español</option> + </select> + </div> + </div> + <div class="form-row"> + <div class="form-group"> + <label for="currency">Devise</label> + <select id="currency" bind:value={display.currency}> + <option value="EUR">EUR (€)</option> + <option value="USD">USD ($)</option> + <option value="GBP">GBP (£)</option> + </select> + </div> + <div class="form-group"> + <label for="dateFormat">Format de date</label> + <select id="dateFormat" bind:value={display.dateFormat}> + <option value="DD/MM/YYYY">DD/MM/YYYY</option> + <option value="MM/DD/YYYY">MM/DD/YYYY</option> + <option value="YYYY-MM-DD">YYYY-MM-DD</option> + </select> + </div> + </div> + <button type="submit" class="btn">💾 Sauvegarder l'affichage</button> + </form> + </div> + + <div class="settings-section card"> + <h2>📊 Gestion des données</h2> + <div class="data-actions"> + <div class="action-item"> + <div class="action-info"> + <h3>Exporter mes données</h3> + <p>Téléchargez toutes vos données personnelles dans un fichier JSON</p> + </div> + <button class="btn" on:click={exportData}>📥 Exporter</button> + </div> + <div class="action-item danger"> + <div class="action-info"> + <h3>Supprimer mon compte</h3> + <p>Supprimez définitivement votre compte et toutes vos données</p> + </div> + <button class="btn danger" on:click={deleteAccount}>🗑️ Supprimer</button> + </div> + </div> + </div> + </div> + </div> +</section> + +<style> + #settings { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .settings-container { + max-width: 1200px; + } + + .settings-header { + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 20px 0; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + } + + h3 { + color: white; + font-size: 18px; + margin: 20px 0 12px 0; + font-weight: 500; + } + + .settings-layout { + display: flex; + flex-direction: column; + gap: 0; + } + + .settings-section { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: calc(100% - 32px); + } + + .form-row { + display: flex; + gap: 20px; + margin-bottom: 16px; + } + + .form-group { + flex: 1; + margin-bottom: 16px; + } + + label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: white; + font-weight: 500; + } + + input[type="text"], + input[type="email"], + input[type="tel"], + input[type="password"], + select { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + box-sizing: border-box; + } + + input:focus, + select:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + input::placeholder { + color: #888888; + } + + .checkbox-group { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; + } + + .checkbox-label { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-size: 14px; + } + + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--text-lime); + } + + input[type="checkbox"]:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; + font-size: 14px; + } + + .btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .btn.danger { + background-color: #dc2626; + } + + .btn.danger:hover { + background-color: #b91c1c; + } + + .password-section { + border-top: 1px solid var(--btn-primary); + padding-top: 20px; + margin-top: 20px; + } + + .data-actions { + display: flex; + flex-direction: column; + gap: 20px; + } + + .action-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background-color: var(--bg-secondary); + border-radius: 8px; + } + + .action-item.danger { + border-left: 4px solid #dc2626; + } + + .action-info h3 { + margin: 0 0 4px 0; + font-size: 16px; + } + + .action-info p { + margin: 0; + font-size: 14px; + color: #888888; + } + + @media (max-width: 768px) { + .form-row { + flex-direction: column; + } + + .action-item { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + } +</style> diff --git a/ping/frontend/src/routes/dashboard/transactions/+page.svelte b/ping/frontend/src/routes/dashboard/transactions/+page.svelte new file mode 100644 index 0000000..86f9292 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/transactions/+page.svelte @@ -0,0 +1,360 @@ +<script lang="ts"> + import TransactionModal from '$lib/components/dashboard/transactions/TransactionModal.svelte'; + import { authFetch } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + + interface ITransaction { + id: string; + label: string | null; + amount: number; + currency: string; + receiverLabel: string | null; + receiverIban: string; + operationDate: Date; + eco_score: number; + creationDate: Date; + createrId: string; + } + + let searchQuery = $state(''); + let sortBy = $state('Date'); + let sortOrder = $state('Montant'); + let transactions: ITransaction[] = $state([]); + let isCreateDialogOpened = $state(false); + + let visibleTransactions = $state<ITransaction[]>([]); + + $effect(() => { + const query = searchQuery.toLowerCase(); + visibleTransactions = transactions.filter((t) => { + const matchesLabel = t.label?.toLowerCase().includes(query); + const matchesReceiver = t.receiverLabel?.toLowerCase().includes(query); + const receiverIban = t.receiverIban?.toLowerCase().includes(query); + return matchesLabel || matchesReceiver; + }); + }); + + function handleSearch() { + console.log('Searching for:', searchQuery); + } + + function applyFilters() { + console.log('Applying filters:', { sortBy, sortOrder }); + } + + function analyzeTransaction(transaction: any) { + console.log('Analyzing transaction', transaction); + } + + async function getAllTransactions(): Promise<ITransaction[]> { + const res = await authFetch('/api/transactions', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + const data = await res.json(); + + if (!res.ok) { + addToast({ message: data.message, color: 'red', title: 'Erreur' }); + return []; + } + + return data.transactions as ITransaction[]; + } + + function onCreate(tr: ITransaction) { + getAllTransactions().then((transactionsData) => { + if (transactionsData) { + transactions = transactionsData; + } + }); + } + + onMount(() => { + getAllTransactions().then((transactionsData) => { + if (transactionsData) { + transactions = transactionsData; + visibleTransactions = transactionsData; + console.log(transactions); + } + }); + }); +</script> + +<section id="transactions"> + <div class="transactions-container"> + <div class="transactions-header"> + <h1>Transactions</h1> + <div class="search-bar"> + <input + type="text" + placeholder="Recherche..." + bind:value={searchQuery} + oninput={handleSearch} + /> + <button class="search-btn">🔍</button> + </div> + </div> + + <div class="filters-section"> + <div class="filters-left"> + <span class="filter-label">Trier</span> + <div class="filter-group"> + <select bind:value={sortBy} class="filter-select"> + <option value="Date">Date</option> + <option value="Amount">Montant</option> + <option value="Company">Entreprise</option> + </select> + <select bind:value={sortOrder} class="filter-select"> + <option value="Montant">Montant</option> + <option value="ASC">Croissant</option> + <option value="DESC">Décroissant</option> + </select> + </div> + </div> + <div> + <button + class="apply-btn" + onclick={() => { + isCreateDialogOpened = true; + }} + > + ➕ Créer + </button> + <button class="apply-btn" onclick={applyFilters}> 📁 Appliquer </button> + </div> + </div> + + <div class="transactions-list"> + {#each visibleTransactions as transaction} + <div class="transaction-card"> + <div class="transaction-main"> + <div class="transaction-info"> + <h3 class="transaction-type">{transaction.label}</h3> + <p class="transaction-company"> + {transaction.receiverIban} • {transaction.receiverLabel} + </p> + </div> + <div class="transaction-metrics"> + <div class="metric"> + <span class="metric-label">CO2</span> + <span class="metric-value co2">{transaction.eco_score}</span> + </div> + <div class="metric"> + <span class="metric-label">{transaction.currency}</span> + <span class="metric-value amount">{transaction.amount}</span> + </div> + </div> + <button class="analyze-btn" onclick={() => analyzeTransaction(transaction)}> + ✏️ Analyser + </button> + </div> + </div> + {/each} + </div> + </div> +</section> +<TransactionModal bind:isOpen={isCreateDialogOpened} {onCreate} /> + +<style> + #transactions { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .transactions-container { + max-width: 1200px; + } + + .transactions-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + .search-bar { + display: flex; + align-items: center; + background-color: var(--bg-primary); + border-radius: 8px; + padding: 4px; + gap: 8px; + } + + .search-bar input { + background: transparent; + border: none; + color: white; + padding: 12px 16px; + font-size: 14px; + outline: none; + width: 300px; + } + + .search-bar input::placeholder { + color: #888888; + } + + .search-btn { + background: transparent; + border: none; + color: #888888; + padding: 8px; + cursor: pointer; + font-size: 16px; + } + + /* Filters */ + .filters-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 16px; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .filters-left { + display: flex; + align-items: center; + gap: 15px; + } + + .filter-label { + color: white; + font-weight: 500; + font-size: 14px; + } + + .filter-group { + display: flex; + gap: 10px; + } + + .filter-select { + background-color: var(--bg-secondary); + color: white; + border: none; + border-bottom: 2px solid var(--text-lime); + border-radius: 8px 8px 0 0; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + } + + .filter-select:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + .apply-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition-duration: 0.2s; + } + + .apply-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .transactions-list { + display: flex; + flex-direction: column; + gap: 0; + } + + .transaction-card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: calc(100% - 32px); + } + + .transaction-main { + display: flex; + align-items: center; + justify-content: space-between; + } + + .transaction-info h3 { + margin: 0 0 5px 0; + font-size: 18px; + font-weight: 600; + color: white; + } + + .transaction-company { + margin: 0; + font-size: 14px; + color: #888888; + } + + .transaction-metrics { + display: flex; + gap: 40px; + } + + .metric { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + } + + .metric-label { + font-size: 12px; + color: #888888; + font-weight: 500; + } + + .metric-value { + font-size: 24px; + font-weight: bold; + } + + .metric-value.co2, + .metric-value.amount { + color: var(--text-lime); + } + + .analyze-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition-duration: 0.2s; + } + + .analyze-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } +</style> diff --git a/ping/frontend/src/routes/login/+page.svelte b/ping/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..0d66f8d --- /dev/null +++ b/ping/frontend/src/routes/login/+page.svelte @@ -0,0 +1,149 @@ +<script lang="ts"> + import { addToast } from '$lib/stores/toast'; + import { json } from '@sveltejs/kit'; + + let isConnecting = $state(false); + + function onSubmit(event: Event) { + event.preventDefault(); + + isConnecting = true; + const fd = new FormData(event.target as HTMLFormElement); + const { login, password } = Object.fromEntries(fd.entries()); + + fetch('/api/user/login', { + body: JSON.stringify({ + login, + password + }), + headers: { + Accept: '*/*', + 'Content-Type': 'application/json' + }, + method: 'POST' + }) + .then(async (res) => { + const jsonData = await res.json(); + + if (!res.ok) { + isConnecting = false; + addToast({ message: jsonData.message }); + return; + } + + const { token } = jsonData; + localStorage.setItem('token', token); + window.location.href = '/dashboard'; + }) + .catch((err) => { + isConnecting = false; + console.error('Error during login:', err); + addToast({ + message: "Une erreur est survenue lors de la connexion. (Plus d'infos dans la console)" + }); + }); + } + + function createDebugUsers() { + fetch('/api/dbdebuguser') + .then((res) => { + if (res.ok) { + res.json().then((data) => { + if (data.created) { + addToast({ + message: 'Utilisateurs de debug créés avec succès.', + color: 'green', + title: 'Succès' + }); + return data; + } else { + addToast({ + message: 'Utilisateurs de debug déjà créés.', + color: 'green', + title: 'Succès' + }); + } + }); + } else { + addToast({ message: 'Il semblerait que les utilisateurs de debug soient déjà crées.' }); + } + }) + .catch((err) => { + console.error('Erreur lors de la création des utilisateurs de debug:', err); + addToast({ + message: + "Erreur lors de la création des utilisateurs de debug. (Plus d'infos dans la console)" + }); + }); + } +</script> + +<div class="bg"> + <div class="card"> + <h1>Connectez-vous</h1> + <form onsubmit={onSubmit}> + <label for="login">Nom d'utilisateur</label> + <input type="text" id="login" name="login" placeholder="lirili.larila" required /> + <label for="password">Mot de passe</label> + <input type="password" id="password" name="password" placeholder="Mot de passe" required /> + <button class="btn" type="submit" disabled={isConnecting}>Connexion</button> + </form> + </div> +</div> +<button id="fab" class="btn" onclick={createDebugUsers} + ><img src="/icons/debug.svg" width="100%" alt="+" /></button +> + +<style> + .card { + width: auto; + } + + .bg { + background-image: url('/img/header-bg.jpg'); + height: 100vh; + background-size: cover; + background-position: center; + position: relative; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + } + + h1 { + color: var(--text-lime); + font-size: 36px; + text-align: center; + margin: 0; + font-weight: 600; + } + + form { + display: flex; + flex-direction: column; + color: white; + gap: 8px; + min-width: 360px; + } + + #fab { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + border-radius: 50%; + width: 56px; + height: 56px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bolder; + font-size: 32px; + } + + #fab img { + transform: scale(2); + } +</style> diff --git a/ping/frontend/src/routes/stocksapi/chart/+server.ts b/ping/frontend/src/routes/stocksapi/chart/+server.ts new file mode 100644 index 0000000..3271e83 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/chart/+server.ts @@ -0,0 +1,21 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/chart?query=AAPL&startDate=2025-01-01& + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + const startDate = sp.get('startDate') || '2025-01-01'; + const endDate = sp.get('endDate') || new Date().toISOString().split('T')[0]; + const interval = sp.get('interval') || '1d'; + + const data = await yahooFinance.chart(query, { period1: startDate, period2: endDate, interval: interval }); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +};
\ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/insights/+server.ts b/ping/frontend/src/routes/stocksapi/insights/+server.ts new file mode 100644 index 0000000..6be76e9 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/insights/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/insights?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const queryOptions = { lang: 'en-US', reportsCount: 2, region: 'US' }; + const data = await yahooFinance.insights(query, queryOptions); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +};
\ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/quote/+server.ts b/ping/frontend/src/routes/stocksapi/quote/+server.ts new file mode 100644 index 0000000..96619fc --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/quote/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/quote?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const data = await yahooFinance.quote(query); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +};
\ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/search/+server.ts b/ping/frontend/src/routes/stocksapi/search/+server.ts new file mode 100644 index 0000000..e58fc6b --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/search/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/search?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const data = await yahooFinance.search(query, { region: 'US' }) + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +};
\ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts b/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts new file mode 100644 index 0000000..3dac574 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/trendingSymbols + */ +export const GET: RequestHandler = async () => { + const queryOptions = { count: 5, lang: 'en-US' }; + const data = await yahooFinance.trendingSymbols('US', queryOptions); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +};
\ No newline at end of file diff --git a/ping/frontend/static/favicon.png b/ping/frontend/static/favicon.png Binary files differnew file mode 100644 index 0000000..83e6bff --- /dev/null +++ b/ping/frontend/static/favicon.png diff --git a/ping/frontend/static/icons/ICI b/ping/frontend/static/icons/ICI new file mode 100644 index 0000000..01a9115 --- /dev/null +++ b/ping/frontend/static/icons/ICI @@ -0,0 +1,2 @@ +todo : remove this file +fact: les triangles sont pas exactement les memes diff --git a/ping/frontend/static/icons/add-green.svg b/ping/frontend/static/icons/add-green.svg new file mode 100644 index 0000000..d165f30 --- /dev/null +++ b/ping/frontend/static/icons/add-green.svg @@ -0,0 +1,7 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +
<g id="SVGRepo_bgCarrier" stroke-width="0"/> +
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/> +
<g id="SVGRepo_iconCarrier"> <path d="M11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8V11H16C16.5523 11 17 11.4477 17 12C17 12.5523 16.5523 13 16 13H13V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V13H8C7.44771 13 7 12.5523 7 12C7 11.4477 7.44772 11 8 11H11V8Z" fill="#00ff77"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12ZM3.00683 12C3.00683 16.9668 7.03321 20.9932 12 20.9932C16.9668 20.9932 20.9932 16.9668 20.9932 12C20.9932 7.03321 16.9668 3.00683 12 3.00683C7.03321 3.00683 3.00683 7.03321 3.00683 12Z" fill="#00ff77"/> </g> +
</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/bell.svg b/ping/frontend/static/icons/bell.svg new file mode 100644 index 0000000..797220a --- /dev/null +++ b/ping/frontend/static/icons/bell.svg @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 611.999 611.999" xml:space="preserve">
+<g>
+ <g>
+ <g>
+ <path d="M570.107,500.254c-65.037-29.371-67.511-155.441-67.559-158.622v-84.578c0-81.402-49.742-151.399-120.427-181.203
+ C381.969,34,347.883,0,306.001,0c-41.883,0-75.968,34.002-76.121,75.849c-70.682,29.804-120.425,99.801-120.425,181.203v84.578
+ c-0.046,3.181-2.522,129.251-67.561,158.622c-7.409,3.347-11.481,11.412-9.768,19.36c1.711,7.949,8.74,13.626,16.871,13.626
+ h164.88c3.38,18.594,12.172,35.892,25.619,49.903c17.86,18.608,41.479,28.856,66.502,28.856
+ c25.025,0,48.644-10.248,66.502-28.856c13.449-14.012,22.241-31.311,25.619-49.903h164.88c8.131,0,15.159-5.676,16.872-13.626
+ C581.586,511.664,577.516,503.6,570.107,500.254z M484.434,439.859c6.837,20.728,16.518,41.544,30.246,58.866H97.32
+ c13.726-17.32,23.407-38.135,30.244-58.866H484.434z M306.001,34.515c18.945,0,34.963,12.73,39.975,30.082
+ c-12.912-2.678-26.282-4.09-39.975-4.09s-27.063,1.411-39.975,4.09C271.039,47.246,287.057,34.515,306.001,34.515z
+ M143.97,341.736v-84.685c0-89.343,72.686-162.029,162.031-162.029s162.031,72.686,162.031,162.029v84.826
+ c0.023,2.596,0.427,29.879,7.303,63.465H136.663C143.543,371.724,143.949,344.393,143.97,341.736z M306.001,577.485
+ c-26.341,0-49.33-18.992-56.709-44.246h113.416C355.329,558.493,332.344,577.485,306.001,577.485z"/>
+ <path d="M306.001,119.235c-74.25,0-134.657,60.405-134.657,134.654c0,9.531,7.727,17.258,17.258,17.258
+ c9.531,0,17.258-7.727,17.258-17.258c0-55.217,44.923-100.139,100.142-100.139c9.531,0,17.258-7.727,17.258-17.258
+ C323.259,126.96,315.532,119.235,306.001,119.235z"/>
+ </g>
+ </g>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/closed-wallet.svg b/ping/frontend/static/icons/closed-wallet.svg new file mode 100644 index 0000000..ffd10f6 --- /dev/null +++ b/ping/frontend/static/icons/closed-wallet.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 405.867 405.867" xml:space="preserve">
+<g>
+ <path d="M389.887,0H58.973C26.996,0,0.98,26.016,0.98,57.993v287.823c0,33.112,26.938,60.051,60.051,60.051h328.855
+ c8.284,0,15-6.716,15-15V15C404.887,6.716,398.171,0,389.887,0z M58.973,30h315.914v55.985h-23.314V57.993c0-5.523-4.478-10-10-10
+ H32.834C36.869,37.483,47.061,30,58.973,30z M331.572,85.985h-272.6c-11.912,0-22.104-7.483-26.139-17.992h298.738V85.985z
+ M61.031,375.867c-16.57,0-30.051-13.481-30.051-30.051V108.761c8.305,4.598,17.846,7.224,27.992,7.224h315.914v74.778h-63.772
+ c-30.417,0-55.163,24.746-55.163,55.163s24.746,55.163,55.163,55.163h63.772v74.778H61.031z M374.887,281.089h-63.772
+ c-19.389,0-35.163-15.774-35.163-35.163c0-19.389,15.774-35.163,35.163-35.163h63.772V281.089z"/>
+ <path d="M297.166,245.922c0,3.95,1.601,7.82,4.391,10.61s6.66,4.39,10.609,4.39c3.95,0,7.811-1.6,10.601-4.39
+ c2.8-2.79,4.399-6.66,4.399-10.61c0-3.95-1.6-7.81-4.399-10.6c-2.79-2.8-6.65-4.4-10.601-4.4c-3.949,0-7.819,1.6-10.609,4.4
+ C298.767,238.112,297.166,241.982,297.166,245.922z"/>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/coin.svg b/ping/frontend/static/icons/coin.svg new file mode 100644 index 0000000..331f0aa --- /dev/null +++ b/ping/frontend/static/icons/coin.svg @@ -0,0 +1,11 @@ +<?xml version='1.0' encoding='utf-8'?> +<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512"> + <g> + <g> + <path d="m256,11c-135.1,0-245,109.9-245,245s109.9,245 245,245c135.1,0 245-109.9 245-245s-109.9-245-245-245zm0,449.2c-112.6,0-204.2-91.6-204.2-204.2 0-112.6 91.6-204.2 204.2-204.2 112.6,0 204.2,91.6 204.2,204.2 0,112.6-91.6,204.2-204.2,204.2z"/> + <path d="m268.8,224.8v-66.5c11,4.8 17.8,13.8 20.4,27.2l43-5.6c-2.9-17-9.8-30.6-20.4-40.7-10.7-10.2-25-16.3-43-18.5v-16.8h-24.7v16.8c-19.5,1.9-35.1,9.2-46.9,21.9-11.7,12.6-17.6,28.3-17.6,46.9 0,18.4 5.2,34 15.6,46.9 10.4,12.9 26.7,22.5 48.9,28.8v71.3c-6.1-2.9-11.7-7.7-16.7-14.3-5-6.6-8.4-14.4-10.2-23.5l-44.4,4.8c3.4,22.3 11.2,39.6 23.5,51.9s28.2,19.6 47.8,21.9v31h24.7v-31.8c22.1-3.2 39.4-11.8 51.8-25.9 12.4-14.1 18.6-31.4 18.6-51.9 0-18.4-4.9-33.4-14.8-45.2-9.9-11.8-28.4-21.3-55.6-28.7zm-24.7-8.2c-36.1-11.8-24.2-58.9 0-58.9v58.9zm24.7,122.2v-66.4c36.2,7 33.1,59.5 0,66.4z"/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/credit-card.svg b/ping/frontend/static/icons/credit-card.svg new file mode 100644 index 0000000..d84911c --- /dev/null +++ b/ping/frontend/static/icons/credit-card.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.5 10.5H20.5M4.5 9.5H20.5M4.5 11.5H20.5M7 15.5H14M5.5 18.5H19.5C20.0523 18.5 20.5 18.0523 20.5 17.5V7.5C20.5 6.94772 20.0523 6.5 19.5 6.5H5.5C4.94772 6.5 4.5 6.94772 4.5 7.5V17.5C4.5 18.0523 4.94772 18.5 5.5 18.5Z" stroke="#ffffff" stroke-width="1.2"/>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/dashboard.svg b/ping/frontend/static/icons/dashboard.svg new file mode 100644 index 0000000..072ca90 --- /dev/null +++ b/ping/frontend/static/icons/dashboard.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2929 4.29289C14 4.58579 14 5.05719 14 6C14 6.94281 14 7.41421 14.2929 7.70711C14.5858 8 15.0572 8 16 8H18C18.9428 8 19.4142 8 19.7071 7.70711C20 7.41421 20 6.94281 20 6C20 5.05719 20 4.58579 19.7071 4.29289C19.4142 4 18.9428 4 18 4H16C15.0572 4 14.5858 4 14.2929 4.29289ZM4.29289 16.2929C4 16.5858 4 17.0572 4 18C4 18.9428 4 19.4142 4.29289 19.7071C4.58579 20 5.05719 20 6 20H8C8.94281 20 9.41421 20 9.70711 19.7071C10 19.4142 10 18.9428 10 18C10 17.0572 10 16.5858 9.70711 16.2929C9.41421 16 8.94281 16 8 16H6C5.05719 16 4.58579 16 4.29289 16.2929ZM14 14C14 13.0572 14 12.5858 14.2929 12.2929C14.5858 12 15.0572 12 16 12H18C18.9428 12 19.4142 12 19.7071 12.2929C20 12.5858 20 13.0572 20 14V18C20 18.9428 20 19.4142 19.7071 19.7071C19.4142 20 18.9428 20 18 20H16C15.0572 20 14.5858 20 14.2929 19.7071C14 19.4142 14 18.9428 14 18V14ZM4.29289 4.29289C4 4.58579 4 5.05719 4 6V10C4 10.9428 4 11.4142 4.29289 11.7071C4.58579 12 5.05719 12 6 12H8C8.94281 12 9.41421 12 9.70711 11.7071C10 11.4142 10 10.9428 10 10V6C10 5.05719 10 4.58579 9.70711 4.29289C9.41421 4 8.94281 4 8 4H6C5.05719 4 4.58579 4 4.29289 4.29289Z" fill="#ffffff"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4.82324 4.82324L4.82567 4.82187C4.8276 4.82086 4.83092 4.81924 4.83596 4.81717C4.85774 4.80821 4.90611 4.79291 5.00266 4.77993C5.21339 4.7516 5.50739 4.75001 6 4.75001H8C8.49261 4.75001 8.78661 4.7516 8.99734 4.77993C9.09389 4.79291 9.14225 4.80821 9.16403 4.81717C9.16908 4.81924 9.17239 4.82086 9.17433 4.82187L9.17676 4.82324L9.17814 4.82568C9.17915 4.82761 9.18077 4.83093 9.18284 4.83597C9.1918 4.85775 9.20709 4.90611 9.22008 5.00267C9.24841 5.21339 9.25 5.5074 9.25 6.00001V10C9.25 10.4926 9.24841 10.7866 9.22008 10.9973C9.20709 11.0939 9.1918 11.1423 9.18284 11.164C9.18077 11.1691 9.17915 11.1724 9.17814 11.1743L9.17676 11.1768L9.17433 11.1781C9.17239 11.1792 9.16908 11.1808 9.16403 11.1828C9.14225 11.1918 9.09389 11.2071 8.99734 11.2201C8.78661 11.2484 8.49261 11.25 8 11.25H6C5.50739 11.25 5.21339 11.2484 5.00266 11.2201C4.90611 11.2071 4.85774 11.1918 4.83596 11.1828C4.83092 11.1808 4.8276 11.1792 4.82567 11.1781L4.82324 11.1768L4.82186 11.1743C4.82085 11.1724 4.81923 11.1691 4.81716 11.164C4.8082 11.1423 4.79291 11.0939 4.77992 10.9973C4.75159 10.7866 4.75 10.4926 4.75 10V6.00001C4.75 5.5074 4.75159 5.21339 4.77992 5.00267C4.79291 4.90611 4.8082 4.85775 4.81716 4.83597C4.81923 4.83093 4.82085 4.82761 4.82186 4.82568L4.82324 4.82324ZM5.95525 3.25C5.97013 3.25001 5.98504 3.25001 6 3.25001H8C8.01496 3.25001 8.02987 3.25001 8.04475 3.25C8.47756 3.24995 8.8744 3.24991 9.19721 3.29331C9.55269 3.3411 9.92841 3.45355 10.2374 3.76257C10.5465 4.07159 10.6589 4.44732 10.7067 4.8028C10.7501 5.12561 10.7501 5.52245 10.75 5.95526C10.75 5.97014 10.75 5.98505 10.75 6.00001V10C10.75 10.015 10.75 10.0299 10.75 10.0448C10.7501 10.4776 10.7501 10.8744 10.7067 11.1972C10.6589 11.5527 10.5465 11.9284 10.2374 12.2374C9.92841 12.5465 9.55269 12.6589 9.19721 12.7067C8.87439 12.7501 8.47756 12.7501 8.04474 12.75C8.02987 12.75 8.01496 12.75 8 12.75H6C5.98504 12.75 5.97013 12.75 5.95526 12.75C5.52244 12.7501 5.1256 12.7501 4.80279 12.7067C4.44731 12.6589 4.07159 12.5465 3.76256 12.2374C3.45354 11.9284 3.34109 11.5527 3.2933 11.1972C3.2499 10.8744 3.24994 10.4776 3.25 10.0448C3.25 10.0299 3.25 10.015 3.25 10V6.00001C3.25 5.98505 3.25 5.97013 3.25 5.95526C3.24994 5.52244 3.2499 5.12561 3.2933 4.8028C3.34109 4.44732 3.45354 4.07159 3.76256 3.76257C4.07159 3.45355 4.44731 3.3411 4.80279 3.29331C5.1256 3.24991 5.52243 3.24995 5.95525 3.25ZM14.8257 4.82187L14.8232 4.82324L14.8219 4.82568C14.8209 4.82761 14.8192 4.83093 14.8172 4.83597C14.8082 4.85775 14.7929 4.90611 14.7799 5.00267C14.7516 5.21339 14.75 5.5074 14.75 6.00001C14.75 6.49261 14.7516 6.78662 14.7799 6.99734C14.7929 7.0939 14.8082 7.14226 14.8172 7.16404C14.818 7.16606 14.8194 7.1693 14.8194 7.1693C14.8204 7.17152 14.8219 7.17434 14.8219 7.17434L14.8232 7.17677L14.8257 7.17815C14.8276 7.17916 14.8309 7.18077 14.836 7.18285C14.8577 7.19181 14.9061 7.2071 15.0027 7.22008C15.2134 7.24841 15.5074 7.25001 16 7.25001H18C18.4926 7.25001 18.7866 7.24841 18.9973 7.22008C19.0939 7.2071 19.1423 7.19181 19.164 7.18285C19.1691 7.18077 19.1724 7.17916 19.1743 7.17815L19.1768 7.17677L19.1781 7.17434C19.1791 7.1724 19.1808 7.16909 19.1828 7.16404C19.1918 7.14226 19.2071 7.0939 19.2201 6.99734C19.2484 6.78662 19.25 6.49261 19.25 6.00001C19.25 5.5074 19.2484 5.21339 19.2201 5.00267C19.2071 4.90611 19.1918 4.85775 19.1828 4.83597C19.1808 4.83093 19.1791 4.82761 19.1781 4.82568L19.1768 4.82324L19.1743 4.82187C19.1724 4.82086 19.1691 4.81924 19.164 4.81717C19.1423 4.80821 19.0939 4.79291 18.9973 4.77993C18.7866 4.7516 18.4926 4.75001 18 4.75001H16C15.5074 4.75001 15.2134 4.7516 15.0027 4.77993C14.9061 4.79291 14.8577 4.80821 14.836 4.81717C14.8309 4.81924 14.8276 4.82086 14.8257 4.82187ZM15.9553 3.25H18.0447C18.4776 3.24995 18.8744 3.24991 19.1972 3.29331C19.5527 3.3411 19.9284 3.45355 20.2374 3.76257C20.5465 4.07159 20.6589 4.44732 20.7067 4.8028C20.7501 5.12561 20.7501 5.52244 20.75 5.95526V6.04475C20.7501 6.47757 20.7501 6.8744 20.7067 7.19721C20.6589 7.5527 20.5465 7.92842 20.2374 8.23744C19.9284 8.54647 19.5527 8.65891 19.1972 8.70671C18.8744 8.75011 18.4776 8.75006 18.0447 8.75001H15.9553C15.5224 8.75006 15.1256 8.75011 14.8028 8.70671C14.4473 8.65891 14.0716 8.54647 13.7626 8.23744C13.4535 7.92842 13.3411 7.5527 13.2933 7.19721C13.2499 6.8744 13.2499 6.47757 13.25 6.04475C13.25 6.02988 13.25 6.01496 13.25 6.00001C13.25 5.98505 13.25 5.97014 13.25 5.95526C13.2499 5.52245 13.2499 5.12561 13.2933 4.8028C13.3411 4.44732 13.4535 4.07159 13.7626 3.76257C14.0716 3.45355 14.4473 3.3411 14.8028 3.29331C15.1256 3.24991 15.5224 3.24995 15.9553 3.25ZM15.9553 11.25H18.0447C18.4776 11.25 18.8744 11.2499 19.1972 11.2933C19.5527 11.3411 19.9284 11.4535 20.2374 11.7626C20.5465 12.0716 20.6589 12.4473 20.7067 12.8028C20.7501 13.1256 20.7501 13.5224 20.75 13.9553V18.0448C20.7501 18.4776 20.7501 18.8744 20.7067 19.1972C20.6589 19.5527 20.5465 19.9284 20.2374 20.2374C19.9284 20.5465 19.5527 20.6589 19.1972 20.7067C18.8744 20.7501 18.4776 20.7501 18.0447 20.75H15.9553C15.5224 20.7501 15.1256 20.7501 14.8028 20.7067C14.4473 20.6589 14.0716 20.5465 13.7626 20.2374C13.4535 19.9284 13.3411 19.5527 13.2933 19.1972C13.2499 18.8744 13.2499 18.4776 13.25 18.0448L13.25 14C13.25 13.9851 13.25 13.9701 13.25 13.9553C13.2499 13.5224 13.2499 13.1256 13.2933 12.8028C13.3411 12.4473 13.4535 12.0716 13.7626 11.7626C14.0716 11.4535 14.4473 11.3411 14.8028 11.2933C15.1256 11.2499 15.5224 11.25 15.9553 11.25ZM14.8257 12.8219L14.8232 12.8232L14.8219 12.8257C14.8209 12.8276 14.8192 12.8309 14.8172 12.836C14.8082 12.8578 14.7929 12.9061 14.7799 13.0027C14.7516 13.2134 14.75 13.5074 14.75 14V18C14.75 18.4926 14.7516 18.7866 14.7799 18.9973C14.7929 19.0939 14.8082 19.1423 14.8172 19.164C14.8192 19.1691 14.8209 19.1724 14.8219 19.1743L14.8232 19.1768L14.8257 19.1781C14.8276 19.1792 14.8309 19.1808 14.836 19.1828C14.8577 19.1918 14.9061 19.2071 15.0027 19.2201C15.2134 19.2484 15.5074 19.25 16 19.25H18C18.4926 19.25 18.7866 19.2484 18.9973 19.2201C19.0939 19.2071 19.1423 19.1918 19.164 19.1828C19.1691 19.1808 19.1724 19.1792 19.1743 19.1781L19.1768 19.1768L19.1781 19.1743C19.1791 19.1724 19.1808 19.1691 19.1828 19.164C19.1918 19.1423 19.2071 19.0939 19.2201 18.9973C19.2484 18.7866 19.25 18.4926 19.25 18V14C19.25 13.5074 19.2484 13.2134 19.2201 13.0027C19.2071 12.9061 19.1918 12.8578 19.1828 12.836C19.1816 12.833 19.1805 12.8306 19.1797 12.8287L19.1781 12.8257L19.1768 12.8232L19.1743 12.8219C19.1724 12.8209 19.1691 12.8192 19.164 12.8172C19.1423 12.8082 19.0939 12.7929 18.9973 12.7799C18.7866 12.7516 18.4926 12.75 18 12.75H16C15.5074 12.75 15.2134 12.7516 15.0027 12.7799C14.9061 12.7929 14.8577 12.8082 14.836 12.8172C14.8309 12.8192 14.8276 12.8209 14.8257 12.8219ZM4.82567 16.8219L4.82324 16.8232L4.82186 16.8257C4.82085 16.8276 4.81923 16.8309 4.81716 16.836C4.8082 16.8578 4.7929 16.9061 4.77992 17.0027C4.75159 17.2134 4.75 17.5074 4.75 18C4.75 18.4926 4.75159 18.7866 4.77992 18.9973C4.7929 19.0939 4.8082 19.1423 4.81716 19.164C4.81923 19.1691 4.82085 19.1724 4.82186 19.1743L4.82324 19.1768L4.82567 19.1781C4.8276 19.1792 4.83092 19.1808 4.83596 19.1828C4.85774 19.1918 4.90611 19.2071 5.00266 19.2201C5.21339 19.2484 5.50739 19.25 6 19.25H8C8.49261 19.25 8.78661 19.2484 8.99734 19.2201C9.09389 19.2071 9.14225 19.1918 9.16403 19.1828C9.16658 19.1818 9.1704 19.1801 9.1704 19.1801L9.17433 19.1781L9.17676 19.1768L9.17814 19.1743L9.1796 19.1714C9.18049 19.1695 9.18158 19.1671 9.18284 19.164C9.1918 19.1423 9.20709 19.0939 9.22008 18.9973C9.24841 18.7866 9.25 18.4926 9.25 18C9.25 17.5074 9.24841 17.2134 9.22008 17.0027C9.20709 16.9061 9.1918 16.8578 9.18284 16.836C9.18077 16.8309 9.17915 16.8276 9.17814 16.8257L9.17676 16.8232L9.17433 16.8219C9.17239 16.8209 9.16908 16.8192 9.16403 16.8172C9.14225 16.8082 9.09389 16.7929 8.99734 16.7799C8.78661 16.7516 8.49261 16.75 8 16.75H6C5.50739 16.75 5.21339 16.7516 5.00266 16.7799C4.90611 16.7929 4.85774 16.8082 4.83596 16.8172C4.83092 16.8192 4.8276 16.8209 4.82567 16.8219ZM5.95526 15.25H8.04474C8.47756 15.25 8.87439 15.2499 9.19721 15.2933C9.55269 15.3411 9.92841 15.4535 10.2374 15.7626C10.5465 16.0716 10.6589 16.4473 10.7067 16.8028C10.7501 17.1256 10.7501 17.5224 10.75 17.9553V18.0448C10.7501 18.4776 10.7501 18.8744 10.7067 19.1972C10.6589 19.5527 10.5465 19.9284 10.2374 20.2374C9.92841 20.5465 9.55269 20.6589 9.19721 20.7067C8.87439 20.7501 8.47756 20.7501 8.04474 20.75H5.95525C5.52244 20.7501 5.1256 20.7501 4.80279 20.7067C4.44731 20.6589 4.07159 20.5465 3.76256 20.2374C3.45354 19.9284 3.34109 19.5527 3.2933 19.1972C3.2499 18.8744 3.24994 18.4776 3.25 18.0448V17.9553C3.24994 17.5224 3.2499 17.1256 3.2933 16.8028C3.34109 16.4473 3.45354 16.0716 3.76256 15.7626C4.07159 15.4535 4.44731 15.3411 4.80279 15.2933C5.1256 15.2499 5.52244 15.25 5.95526 15.25Z" fill="#ffffff"/> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/debug.svg b/ping/frontend/static/icons/debug.svg new file mode 100644 index 0000000..4f6cfb2 --- /dev/null +++ b/ping/frontend/static/icons/debug.svg @@ -0,0 +1,7 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +
<g id="SVGRepo_bgCarrier" stroke-width="0"/> +
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/> +
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M8 9V13.017C8 15.2261 9.79086 17.017 12 17.017C14.2091 17.017 16 15.2261 16 13.017V9H8Z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.25 9C9.25 9.41421 9.58579 9.75 10 9.75C10.4142 9.75 10.75 9.41421 10.75 9H9.25ZM13.25 9C13.25 9.41421 13.5858 9.75 14 9.75C14.4142 9.75 14.75 9.41421 14.75 9H13.25ZM12.75 13.017C12.75 12.6028 12.4142 12.267 12 12.267C11.5858 12.267 11.25 12.6028 11.25 13.017H12.75ZM11.25 17.034C11.25 17.4482 11.5858 17.784 12 17.784C12.4142 17.784 12.75 17.4482 12.75 17.034H11.25ZM5 12.267C4.58579 12.267 4.25 12.6028 4.25 13.017C4.25 13.4312 4.58579 13.767 5 13.767V12.267ZM8 13.767C8.41421 13.767 8.75 13.4312 8.75 13.017C8.75 12.6028 8.41421 12.267 8 12.267V13.767ZM6.53205 6.45839C6.2401 6.16455 5.76523 6.16301 5.47139 6.45495C5.17755 6.7469 5.17601 7.22177 5.46795 7.51561L6.53205 6.45839ZM7.46795 9.52861C7.7599 9.82245 8.23477 9.82399 8.52861 9.53205C8.82245 9.2401 8.82399 8.76523 8.53205 8.47139L7.46795 9.52861ZM9.70345 16.3862C9.99572 16.0927 9.99472 15.6178 9.70121 15.3255C9.40769 15.0333 8.93282 15.0343 8.64055 15.3278L9.70345 16.3862ZM6.51955 17.4578C6.22728 17.7513 6.22828 18.2262 6.52179 18.5185C6.81531 18.8107 7.29018 18.8097 7.58245 18.5162L6.51955 17.4578ZM19 13.767C19.4142 13.767 19.75 13.4312 19.75 13.017C19.75 12.6028 19.4142 12.267 19 12.267V13.767ZM16 12.267C15.5858 12.267 15.25 12.6028 15.25 13.017C15.25 13.4312 15.5858 13.767 16 13.767V12.267ZM18.532 7.51561C18.824 7.22177 18.8224 6.7469 18.5286 6.45495C18.2348 6.16301 17.7599 6.16455 17.468 6.45839L18.532 7.51561ZM15.468 8.47139C15.176 8.76523 15.1776 9.2401 15.4714 9.53205C15.7652 9.82399 16.2401 9.82245 16.532 9.52861L15.468 8.47139ZM15.3595 15.3278C15.0672 15.0343 14.5923 15.0333 14.2988 15.3255C14.0053 15.6178 14.0043 16.0927 14.2965 16.3862L15.3595 15.3278ZM16.4175 18.5162C16.7098 18.8097 17.1847 18.8107 17.4782 18.5185C17.7717 18.2262 17.7727 17.7513 17.4805 17.4578L16.4175 18.5162ZM10.75 9V8H9.25V9H10.75ZM10.75 8C10.75 7.30964 11.3096 6.75 12 6.75V5.25C10.4812 5.25 9.25 6.48122 9.25 8H10.75ZM12 6.75C12.6904 6.75 13.25 7.30964 13.25 8H14.75C14.75 6.48122 13.5188 5.25 12 5.25V6.75ZM13.25 8V9H14.75V8H13.25ZM11.25 13.017V17.034H12.75V13.017H11.25ZM5 13.767H8V12.267H5V13.767ZM5.46795 7.51561L7.46795 9.52861L8.53205 8.47139L6.53205 6.45839L5.46795 7.51561ZM8.64055 15.3278L6.51955 17.4578L7.58245 18.5162L9.70345 16.3862L8.64055 15.3278ZM19 12.267H16V13.767H19V12.267ZM17.468 6.45839L15.468 8.47139L16.532 9.52861L18.532 7.51561L17.468 6.45839ZM14.2965 16.3862L16.4175 18.5162L17.4805 17.4578L15.3595 15.3278L14.2965 16.3862Z" fill="#ffffff"/> </g> +
</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/edit-pen.svg b/ping/frontend/static/icons/edit-pen.svg new file mode 100644 index 0000000..0c6f108 --- /dev/null +++ b/ping/frontend/static/icons/edit-pen.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" id="_24x24_On_Light_Edit" data-name="24x24/On Light/Edit" xmlns="http://www.w3.org/2000/svg">
+ <rect id="view-box" width="24" height="24" fill="none"/>
+ <path id="Shape" d="M.75,17.5A.751.751,0,0,1,0,16.75V12.569a.755.755,0,0,1,.22-.53L11.461.8a2.72,2.72,0,0,1,3.848,0L16.7,2.191a2.72,2.72,0,0,1,0,3.848L5.462,17.28a.747.747,0,0,1-.531.22ZM1.5,12.879V16h3.12l7.91-7.91L9.41,4.97ZM13.591,7.03l2.051-2.051a1.223,1.223,0,0,0,0-1.727L14.249,1.858a1.222,1.222,0,0,0-1.727,0L10.47,3.91Z" transform="translate(3.25 3.25)" fill="#ffffff"/>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/floppy-disk.svg b/ping/frontend/static/icons/floppy-disk.svg new file mode 100644 index 0000000..4952daf --- /dev/null +++ b/ping/frontend/static/icons/floppy-disk.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
+<g id="Floppy-disk">
+ <path d="M35.2673988,6.0411h-7.9999981v10h7.9999981V6.0411z M33.3697014,14.1434002h-4.2046013V7.9387999h4.2046013V14.1434002z"
+ />
+ <path d="M41,47.0410995H21c-0.5527992,0-1,0.4472008-1,1c0,0.5527,0.4472008,1,1,1h20c0.5527,0,1-0.4473,1-1
+ C42,47.4883003,41.5527,47.0410995,41,47.0410995z"/>
+ <path d="M41,39.0410995H21c-0.5527992,0-1,0.4472008-1,1c0,0.5527,0.4472008,1,1,1h20c0.5527,0,1-0.4473,1-1
+ C42,39.4883003,41.5527,39.0410995,41,39.0410995z"/>
+ <path d="M12,56.0410995h38v-26H12V56.0410995z M14,32.0410995h34v22H14V32.0410995z"/>
+ <path d="M49.3811989,0.0411L49.3610992,0H7C4.7908001,0,3,1.7909,3,4v56c0,2.2092018,1.7908001,4,4,4h50
+ c2.2090988,0,4-1.7907982,4-4V11.6962996L49.3811989,0.0411z M39.9604988,2.0804999v17.9211006H14.0394001V2.0804999H39.9604988z
+ M59,60c0,1.1027985-0.8972015,2-2,2H7c-1.1027999,0-2-0.8972015-2-2V4c0-1.1027999,0.8972001-2,2-2h5v20.0410995h30V2h6.5099983
+ L59,12.5228996V60z"/>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/leaf.svg b/ping/frontend/static/icons/leaf.svg new file mode 100644 index 0000000..d58987d --- /dev/null +++ b/ping/frontend/static/icons/leaf.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 512 512" xml:space="preserve">
+<g>
+ <g>
+ <path d="M499.672,13.221c-1.005-3.461-3.895-6.039-7.451-6.628C253.888-33.118,71.535,114.906,23.355,227.462
+ c-35.724,83.437,22.129,165.159,42.197,189.858c-23.098,45.747-33.31,81.006-33.813,82.74c-1.417,4.973,1.463,10.153,6.436,11.574
+ c0.859,0.247,1.719,0.366,2.578,0.366c4.068,0,7.817-2.683,8.996-6.793c0.226-0.793,9.89-33.723,31.239-76.571
+ c12.847,3.18,49.792,11.226,94.587,11.226c73.157,0,167.241-21.471,211.326-120.656c17.901-40.276,19.245-75.529,20.433-106.632
+ c0.841-21.955,1.627-42.69,8.018-62.854C438.93,75.343,496.344,23.323,496.92,22.806
+ C499.608,20.406,500.677,16.676,499.672,13.221z M397.506,144.061c-7.158,22.577-7.99,44.541-8.886,67.8
+ c-1.17,30.851-2.395,62.753-18.833,99.738c-62.625,140.912-239.093,109.357-279.9,99.924
+ c31.683-58.444,83.545-128.747,160.565-162.444c4.745-2.071,6.902-7.593,4.827-12.328c-2.066-4.731-7.597-6.889-12.324-4.823
+ c-79.73,34.878-132.93,106.024-163.529,159.571c-1.452,2.542-2.855,5.06-4.235,7.564c-20.886-28.017-63.373-97.083-34.629-164.232
+ C85.185,130.604,251.457-5.476,470.855,22.377C451.263,43.299,415.589,87.008,397.506,144.061z"/>
+ </g>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/magnifying_glass_icon.svg b/ping/frontend/static/icons/magnifying_glass_icon.svg new file mode 100644 index 0000000..dd521f6 --- /dev/null +++ b/ping/frontend/static/icons/magnifying_glass_icon.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="490" height="490"> +<path fill="none" stroke="#fff" stroke-width="36" stroke-linecap="round" +d="m280,278a153,153 0 1,0-2,2l170,170m-91-117 110,110-26,26-110-110"/> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/money-bills.svg b/ping/frontend/static/icons/money-bills.svg new file mode 100644 index 0000000..57b37a7 --- /dev/null +++ b/ping/frontend/static/icons/money-bills.svg @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 193 193" xml:space="preserve">
+<path style="fill:#ffffff;" d="M150.14,138.849c-0.006,0-0.011,0-0.017,0H2.5c-1.381,0-2.5-1.119-2.5-2.5V93.934
+ c0-0.066,0.001-0.133,0.006-0.199c0.033-0.487,0.207-0.937,0.479-1.308c0.104-0.143,0.226-0.275,0.361-0.395
+ c0.002-0.002,0.002-0.002,0.004-0.004c0.037-0.032,0.074-0.063,0.112-0.093l46.904-37.241c0.442-0.351,0.99-0.542,1.555-0.542
+ h141.067c0.12-0.006,0.223,0.007,0.333,0.021l0,0c0.005,0,0.009,0.002,0.012,0.002h0.001c0.305,0.041,0.596,0.138,0.86,0.281
+ c0.001,0.001,0.002,0.001,0.003,0.002c0,0,0,0,0.001,0c0.215,0.118,0.41,0.265,0.578,0.435c0.187,0.189,0.345,0.409,0.466,0.654
+ c0.093,0.189,0.163,0.391,0.206,0.6c0,0.001,0,0.002,0,0.002c0,0.001,0,0.002,0,0.003c0.036,0.173,0.052,0.349,0.051,0.522v11.452
+ c0,0.005,0,0.01,0,0.014v10.562c0,0.005,0,0.01,0,0.014v10.667c0,0.005,0,0.01,0,0.014v9.777c0,0.698-0.292,1.365-0.806,1.839
+ l-40.359,37.176c-0.002,0.002-0.005,0.004-0.007,0.007h0c-0.003,0.003-0.006,0.002-0.008,0.008c0,0-0.001,0-0.001,0.001
+ c-0.42,0.38-0.969,0.619-1.574,0.644C150.209,138.848,150.175,138.849,150.14,138.849z M102.76,133.849h44.88v-5.358h-44.88V133.849
+ z M53.839,133.849H97.76V96.408H53.839V133.849z M5,133.849h43.839v-5.358H5V133.849z M152.641,127.739v2.908L188,98.077V95.1
+ L152.641,127.739z M102.76,123.491h44.88v-6.138h-44.88V123.491z M5,123.491h43.839v-6.138H5V123.491z M152.641,117.058v3.876
+ L188,88.295v-3.876L152.641,117.058z M102.76,112.353h44.88v-5.699h-44.88V112.353z M5,112.353h43.839v-5.699H5V112.353z
+ M152.641,106.483v3.771L188,77.615v-3.771L152.641,106.483z M102.76,101.654h44.88v-5.246h-44.88V101.654z M5,101.654h43.839
+ v-5.246H5V101.654z M152.641,95.002v4.676L188,67.039v-4.677L152.641,95.002z M106.641,91.408h42.522l34.943-32.255h-42.625
+ l-4.898,4.535c1.644,0.933,3.282,2.354,3.882,4.455c0.802,2.806-0.452,5.814-3.725,8.944c-7.998,7.646-20.262,8.517-23.922,8.602
+ L106.641,91.408z M58.267,91.408h41.014l10.837-10.033c0.01-0.01,0.02-0.019,0.03-0.028l23.972-22.193H97.133l-9.779,8.115
+ c-0.026,0.022-0.052,0.044-0.079,0.065L58.267,91.408z M9.669,91.408h40.768l8.944-7.423c-1.478-0.889-2.861-2.201-3.31-4.1
+ c-1.055-4.466,3.825-8.195,5.429-9.421c9.792-7.482,20.038-7.773,23.388-7.647l4.416-3.664H50.293L9.669,91.408z M78.2,68.368
+ c-3.893,0.738-8.867,2.403-13.664,6.069c-3.091,2.363-3.706,3.846-3.599,4.299c0.133,0.564,1.406,1.286,2.699,1.718L78.2,68.368z
+ M132.618,67.358l-13.677,12.663c4.493-0.815,10.223-2.608,14.344-6.548c1.721-1.646,2.608-3.123,2.374-3.954
+ C135.418,68.669,133.982,67.847,132.618,67.358z"/>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/msg.svg b/ping/frontend/static/icons/msg.svg new file mode 100644 index 0000000..6f3930f --- /dev/null +++ b/ping/frontend/static/icons/msg.svg @@ -0,0 +1,7 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +
<g id="SVGRepo_bgCarrier" stroke-width="0"/> +
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/> +
<g id="SVGRepo_iconCarrier"> <path d="M18 9V7.2C18 6.0799 18 5.51984 17.782 5.09202C17.5903 4.71569 17.2843 4.40973 16.908 4.21799C16.4802 4 15.9201 4 14.8 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40973 4.40973 4.71569 4.21799 5.09202C4 5.51984 4 6.0799 4 7.2V18L8 16M20 20L17.8062 18.5374C17.5065 18.3377 17.3567 18.2378 17.1946 18.167C17.0507 18.1042 16.9 18.0586 16.7454 18.031C16.5713 18 16.3912 18 16.0311 18H11.2C10.0799 18 9.51984 18 9.09202 17.782C8.71569 17.5903 8.40973 17.2843 8.21799 16.908C8 16.4802 8 15.9201 8 14.8V12.2C8 11.0799 8 10.5198 8.21799 10.092C8.40973 9.71569 8.71569 9.40973 9.09202 9.21799C9.51984 9 10.0799 9 11.2 9H16.8C17.9201 9 18.4802 9 18.908 9.21799C19.2843 9.40973 19.5903 9.71569 19.782 10.092C20 10.5198 20 11.0799 20 12.2V20Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> +
</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/people.svg b/ping/frontend/static/icons/people.svg new file mode 100644 index 0000000..b8ad20a --- /dev/null +++ b/ping/frontend/static/icons/people.svg @@ -0,0 +1,7 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +
<g id="SVGRepo_bgCarrier" stroke-width="0"/> +
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/> +
<g id="SVGRepo_iconCarrier"> <path d="M17.5291 7.77C17.4591 7.76 17.3891 7.76 17.3191 7.77C15.7691 7.72 14.5391 6.45 14.5391 4.89C14.5391 3.3 15.8291 2 17.4291 2C19.0191 2 20.3191 3.29 20.3191 4.89C20.3091 6.45 19.0791 7.72 17.5291 7.77Z" fill="#ffffff"/> <path d="M20.7916 14.7004C19.6716 15.4504 18.1016 15.7304 16.6516 15.5404C17.0316 14.7204 17.2316 13.8104 17.2416 12.8504C17.2416 11.8504 17.0216 10.9004 16.6016 10.0704C18.0816 9.8704 19.6516 10.1504 20.7816 10.9004C22.3616 11.9404 22.3616 13.6504 20.7916 14.7004Z" fill="#ffffff"/> <path d="M6.44016 7.77C6.51016 7.76 6.58016 7.76 6.65016 7.77C8.20016 7.72 9.43016 6.45 9.43016 4.89C9.43016 3.29 8.14016 2 6.54016 2C4.95016 2 3.66016 3.29 3.66016 4.89C3.66016 6.45 4.89016 7.72 6.44016 7.77Z" fill="#ffffff"/> <path d="M6.55109 12.8506C6.55109 13.8206 6.76109 14.7406 7.14109 15.5706C5.73109 15.7206 4.26109 15.4206 3.18109 14.7106C1.60109 13.6606 1.60109 11.9506 3.18109 10.9006C4.25109 10.1806 5.76109 9.89059 7.18109 10.0506C6.77109 10.8906 6.55109 11.8406 6.55109 12.8506Z" fill="#ffffff"/> <path d="M12.1208 15.87C12.0408 15.86 11.9508 15.86 11.8608 15.87C10.0208 15.81 8.55078 14.3 8.55078 12.44C8.56078 10.54 10.0908 9 12.0008 9C13.9008 9 15.4408 10.54 15.4408 12.44C15.4308 14.3 13.9708 15.81 12.1208 15.87Z" fill="#ffffff"/> <path d="M8.87078 17.9406C7.36078 18.9506 7.36078 20.6106 8.87078 21.6106C10.5908 22.7606 13.4108 22.7606 15.1308 21.6106C16.6408 20.6006 16.6408 18.9406 15.1308 17.9406C13.4208 16.7906 10.6008 16.7906 8.87078 17.9406Z" fill="#ffffff"/> </g> +
</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/settings.svg b/ping/frontend/static/icons/settings.svg new file mode 100644 index 0000000..25f7ec8 --- /dev/null +++ b/ping/frontend/static/icons/settings.svg @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 512 512" xml:space="preserve"> +<g> + <g> + <g> + <path d="M256,192c-35.285,0-64,28.715-64,64s28.715,64,64,64s64-28.715,64-64S291.285,192,256,192z"/> + <path d="M494.699,212.48l-28.245-5.44c-12.651-2.411-22.571-10.688-27.285-22.635c-4.715-12.011-3.051-24.832,4.587-35.221 + l17.728-24.192c6.229-8.469,5.312-20.245-2.133-27.669l-38.293-38.293c-7.211-7.232-18.603-8.299-27.029-2.581l-23.829,16.107 + c-10.624,7.232-23.467,8.405-35.307,3.285c-11.797-5.163-19.712-15.403-21.653-28.139l-4.565-29.611 + C307.072,7.68,298.112,0,287.573,0h-54.144c-10.219,0-19.008,7.253-20.949,17.301l-7.019,36.48 + c-2.368,12.352-10.389,22.208-22.016,27.008c-11.627,4.843-24.299,3.541-34.709-3.52l-30.763-20.821 + c-8.427-5.717-19.776-4.651-27.029,2.581L52.651,97.323c-7.445,7.424-8.363,19.2-2.133,27.669l17.728,24.213 + c7.637,10.368,9.301,23.189,4.587,35.2c-4.715,11.947-14.635,20.224-27.307,22.635l-28.224,5.44 + C7.253,214.421,0,223.211,0,233.429v54.144c0,10.539,7.68,19.499,18.091,21.099l29.611,4.565 + c12.736,1.941,22.976,9.856,28.139,21.653c5.141,11.797,3.947,24.661-3.285,35.328l-16.107,23.808 + c-5.739,8.448-4.651,19.797,2.581,27.029l38.293,38.293c7.445,7.467,19.2,8.32,27.669,2.133l24.213-17.728 + c10.368-7.616,23.168-9.259,35.2-4.587c11.947,4.715,20.224,14.635,22.635,27.307l5.44,28.224 + C214.421,504.747,223.211,512,233.429,512h54.144c10.539,0,19.499-7.68,21.099-18.091l3.2-20.821 + c2.005-13.035,10.133-23.381,22.293-28.395c12.075-5.035,25.195-3.477,35.84,4.331l17.003,12.459 + c8.427,6.187,20.224,5.333,27.669-2.133l38.293-38.293c7.232-7.232,8.32-18.581,2.581-27.029l-16.107-23.829 + c-7.232-10.645-8.427-23.509-3.285-35.307c5.163-11.797,15.403-19.712,28.139-21.653l29.611-4.565 + c10.411-1.6,18.091-10.56,18.091-21.099v-54.144C512,223.211,504.747,214.421,494.699,212.48z M256,362.667 + c-58.816,0-106.667-47.851-106.667-106.667S197.184,149.333,256,149.333S362.667,197.184,362.667,256 + S314.816,362.667,256,362.667z"/> + </g> + </g> +</g> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/side-menu.svg b/ping/frontend/static/icons/side-menu.svg new file mode 100644 index 0000000..5972d63 --- /dev/null +++ b/ping/frontend/static/icons/side-menu.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 18L20 18" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
+<path d="M4 12L20 12" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
+<path d="M4 6L20 6" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/trash.svg b/ping/frontend/static/icons/trash.svg new file mode 100644 index 0000000..e19d236 --- /dev/null +++ b/ping/frontend/static/icons/trash.svg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 512 512" xml:space="preserve">
+<g>
+ <g>
+ <path d="M465.423,48.241h-137.61V23.955C327.813,10.746,317.082,0,303.893,0h-95.785c-13.19,0-23.92,10.746-23.92,23.955V48.24
+ H46.577c-6.655,0-12.049,5.394-12.049,12.049c0,6.655,5.394,12.049,12.049,12.049h22.332l15.228,396.396
+ C85.069,492.995,104.818,512,129.099,512h253.804c24.281,0,44.03-19.006,44.96-43.267l15.228-396.396h22.332
+ c6.653,0,12.049-5.394,12.049-12.049C477.472,53.635,472.078,48.241,465.423,48.241z M208.285,24.097h95.43v24.143h-95.43V24.097z
+ M403.784,467.809c-0.433,11.268-9.605,20.094-20.882,20.094H129.099c-11.276,0-20.448-8.827-20.882-20.095L93.025,72.338h325.952
+ L403.784,467.809z"/>
+ </g>
+</g>
+<g>
+ <g>
+ <path d="M182.63,181.571c-0.127-6.575-5.494-11.817-12.042-11.817c-0.078,0-0.158,0-0.236,0.002
+ c-6.652,0.128-11.943,5.626-11.815,12.278l3.781,196.634c0.126,6.575,5.495,11.817,12.042,11.817c0.078,0,0.158,0,0.236-0.002
+ c6.653-0.128,11.943-5.624,11.815-12.278L182.63,181.571z"/>
+ </g>
+</g>
+<g>
+ <g>
+ <path d="M255.998,169.753c-6.654,0-12.049,5.394-12.049,12.049v196.634c0,6.654,5.394,12.049,12.049,12.049
+ c6.655,0,12.049-5.394,12.049-12.049V181.802C268.047,175.148,262.653,169.753,255.998,169.753z"/>
+ </g>
+</g>
+<g>
+ <g>
+ <path d="M341.645,169.756c-6.628-0.147-12.151,5.162-12.278,11.815l-3.781,196.634c-0.129,6.653,5.162,12.15,11.815,12.278
+ c0.078,0.001,0.158,0.002,0.236,0.002c6.546,0,11.916-5.244,12.042-11.817l3.781-196.634
+ C353.588,175.38,348.299,169.883,341.645,169.756z"/>
+ </g>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/triangle-down.svg b/ping/frontend/static/icons/triangle-down.svg new file mode 100644 index 0000000..9a40495 --- /dev/null +++ b/ping/frontend/static/icons/triangle-down.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg fill="#ffffff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="800px" height="800px" viewBox="0 0 96.154 96.154"
+ xml:space="preserve">
+<g>
+ <path d="M0.561,20.971l45.951,57.605c0.76,0.951,2.367,0.951,3.127,0l45.956-57.609c0.547-0.689,0.709-1.716,0.414-2.61
+ c-0.061-0.187-0.129-0.33-0.186-0.437c-0.351-0.65-1.025-1.056-1.765-1.056H2.093c-0.736,0-1.414,0.405-1.762,1.056
+ c-0.059,0.109-0.127,0.253-0.184,0.426C-0.15,19.251,0.011,20.28,0.561,20.971z"/>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/triangle-up.svg b/ping/frontend/static/icons/triangle-up.svg new file mode 100644 index 0000000..0b54139 --- /dev/null +++ b/ping/frontend/static/icons/triangle-up.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg fill="#ffffff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="800px" height="800px" viewBox="0 0 123.959 123.959"
+ xml:space="preserve">
+<g>
+ <path d="M66.18,29.742c-2.301-2.3-6.101-2.3-8.401,0l-56,56c-3.8,3.801-1.1,10.2,4.2,10.2h112c5.3,0,8-6.399,4.2-10.2L66.18,29.742
+ z"/>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/icons/wallet.svg b/ping/frontend/static/icons/wallet.svg new file mode 100644 index 0000000..7dfd46f --- /dev/null +++ b/ping/frontend/static/icons/wallet.svg @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 76.304 76.304" xml:space="preserve">
+<g>
+ <path d="M72.325,33.234v-2.947c0-5.389-3.698-9.919-8.686-11.217l-0.009-4.859c0-4.742-3.859-8.601-8.603-8.601h-0.455L14.31,18.1
+ c-0.917,0.053-1.787,0.265-2.604,0.584h-0.105C5.205,18.684,0,23.889,0,30.287v28.804c0,6.397,5.204,11.603,11.601,11.603h49.123
+ c6.396,0,11.601-5.205,11.601-11.603V55.26c2.323-0.899,3.979-3.151,3.979-5.789v-10.45C76.303,36.385,74.648,34.133,72.325,33.234
+ z M70.303,49.47c0,0.118-0.093,0.211-0.211,0.211H53.851c-0.118,0-0.21-0.093-0.21-0.211V39.021c0-0.115,0.094-0.209,0.21-0.209
+ h16.241c0.116,0,0.211,0.094,0.211,0.209V49.47z M55.398,11.637c1.261,0.18,2.232,1.266,2.232,2.579l0.008,4.469H32.679
+ L55.398,11.637z M60.724,64.693H11.602c-3.093,0-5.601-2.509-5.601-5.603V30.287c0-3.095,2.508-5.603,5.601-5.603h49.122
+ c3.094,0,5.601,2.508,5.601,5.603v2.525H53.851c-3.424,0-6.21,2.785-6.21,6.209V49.47c0,3.425,2.786,6.211,6.21,6.211h12.474v3.41
+ C66.325,62.184,63.818,64.693,60.724,64.693z"/>
+</g>
+</svg>
\ No newline at end of file diff --git a/ping/frontend/static/img/default-avatar.png b/ping/frontend/static/img/default-avatar.png Binary files differnew file mode 100644 index 0000000..406d770 --- /dev/null +++ b/ping/frontend/static/img/default-avatar.png diff --git a/ping/frontend/static/img/favicon.svg b/ping/frontend/static/img/favicon.svg new file mode 100644 index 0000000..3d7b3eb --- /dev/null +++ b/ping/frontend/static/img/favicon.svg @@ -0,0 +1,29 @@ +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg623209" version="1.1" viewBox="414 286.5 195 195"> + <metadata id="metadata623215"> + <rdf:rdf> + <cc:work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </cc:work> + </rdf:rdf> + </metadata> + <defs id="defs623213"/> + <linearGradient spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient2-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop623190" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop623192" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <linearGradient gradientTransform="rotate(-30)" spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient3-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop623195" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop623197" stop-opacity="1" stop-color="#cccccc" offset="50%"/> + <stop id="stop623199" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <g id="logo-group"> + <image xlink:href="" id="container" style="display: none;" x="272" y="144" width="480" height="480" transform="translate(0 0)"/> + <g id="logo-center" transform="translate(0 0)"> + <image xlink:href="" id="icon_container" style="display: none;" x="0" y="0"/> + <g id="slogan" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" transform="translate(0 0)"/> + <g id="title" style="font-style:normal;font-weight:400;font-size:72px;line-height:1;font-family:'Brandmark Dots 1 Color';font-variant-ligatures:normal;text-align:center;text-anchor:middle" transform="translate(0 0)"/> + <image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAIoUlEQVR4Xu3dzXYTRxCG4TEn1wXsSe4I/Ad3BOxJ7ioblBHYHCWxpJnq6q6vul7W0z3VX9VzxMiyfLPwjwRI4GwCN2RDAiRwPgGAMB0kcCEBgDAeJAAQZoAEbAnwCmLLjVVFEgBIkUZzTFsCALHlxqoiCQCkSKM5pi0BgNhyY1WRBABSpNEc05YAQGy5sapIAgAp0miOaUsAILbcWFUkAYAUaTTHtCUAEFturCqSAECKNJpj2hIAiC03VhVJACBFGs0xbQkAxJYbq4okAJAijeaYtgQAYsuNVUUSAEiRRnNMWwIAseXGqiIJAKRIozmmLQGA2HJjVZEEAFKk0RzTlgBAbLmxqkgCACnSaI5pSwAgttxYVSQBgBRpNMe0JQAQW26sKpIAQIo02uuY94flN6+9MuwDkAxdEqrx3bflIFRO91IA0j3iuW4AkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cxjkBgDgHynZzJQCQufrJaZwTAIhzoGw3VwIAmaufnMY5AYA4B8p2cyUAkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cJmECv/+53B8Oy51C6fxOukIXqOFXAko4jkUBhOGUSUANB0BkRoNCFHEAhLmUSEAVB0AkxqN2Eco4AFJ7NsNPr44DIOEjUreADDgAUnc+Q0+eBQdAQsek5s0z4QBIzRkNO3U2HAAJG5V6N86IAyD15jTkxFlxACRkXGrdNDMOgNSa1eGnHY3j5mb5sH4K+KPnQfmwomea7PUrgQgcX94sn7x/XwUgDLV7AlE4jgcBiHs72dAzgUgcAPHsJHu5JxCNAyDuLWVDrwQUcMgAWcM4vlPw9/GhyCtg9smbgAoOCSBHHOtbae9/vEe8vq0GkryD7VH5u7+Wh+X7cuux15Y9rs1c6EP6KY7nw1wreMuhuSZnAmo4Ql9BXsIBkpyD7VG1Io4wIJdwgMRj3HLtoYojBMgWHCDJNeAt1SrjGA5kDw6QtIxdjrXqOIYCseAASY5Bt1SZAccwIC04foX/arn9+tr3k5WWxrKmPYEsOIYAccHx3BOQtE9n8A6ZcHQH4ooDJMGj3X77bDi6AumCAyTtUxq0Q0Yc3YB0xQGSoBG33zYrji5AhuAAiX1aB6/MjKMLkDWQD+uHzR6H9YEH92FR771RdhxdgPzYFCR7Z2m662fA0Q1IBJJXy3L3+e3AV67pRtrvQLPg6AoEJH4Dl2mnmXB0BwKSTKPdXutsOIYAAUn74GXYYTSOZdCbM8N+o3D0gzvPJONYzYqjR4IXvzgOJD0ij90THPvyv/rNiiDZF6jy1X98Wx6/L+vPvUb9G/Tfqp7HuQqEZ5Ke8Y/bGxy2rDcBiUCyflvK/fqVQg+2Y7HqNAFw2OdhMxCQ2EOOXAmOtvR3AQFJW9ijV4OjPfHdQEDSHvqIHcDhk7IJCEh8wu+1Czj8kjUDAYlfEzx3Aodnmuv3T7duN/rnJLy7db5j4Gid5v+vbwbCK4l/Uyw7gsOS2vU1LkCCkDysPye5v37E+a8AR78euwEBSb8mXdoZHH1zdwVyLHVt2O36eZ9hPwFfn0nKvpKAoy+O4+7uQEDSv2lPGfPBwwFRdwECkr6d45Wjb76nu3cDApI+TQRHn1zP7doVCEh8mwkO3zy37NYdCEi2tOH6NeC4nlGPK4YAAUlb68DRll/L6mFAQGJrEzhsuXmtGgoEJPvaBo59efW4ejgQkGxrIzi25dT7qhAgILnc1qF/kmIthe8kO9+PMCAgebkp4Oj9mrBv/1AgIPl3s8Cxb3hHXB0OBCQ/2wyOEeO+/x4SQKojAcf+wR21QgZIBJL16fTx6+vlblTYL90HHJHpX7+3FJBqSMBxfUCjr5ADUgUJOKJHf9v9JYHMjgQc24ZT4SpZILMiAYfC2G+vQRrI09ufd4fDwG8v6fjgDo7tg6lypTyQWZCAQ2Xk99WRAkh2JODYN5RKV6cBEoFk/RDfx89vl9uWhoGjJb34tamAZEMCjvgBb60gHZAsSMDROpoa61MCUUcCDo3h9qgiLRBVJODwGEudPVIDUUMCDp3B9qokPRAVJODwGkmtfaYAEo0EHFpD7VnNNEAikKx/euHT8b7rR2Heezbl0l58wcKopH/eZyogEUhGtgscI9OeFMisSMAxHseUryDPMa7PBWM/Bdyxf+DoGO6Vraf7L9bpeWdAwp+9jsMx9SvIDK8k4IjFUQJI1mcScMTjKAMkGxJwaOAoBSQLEnDo4CgH5AnJ/fqDvdAvizs3AuDQwlESiCoScOjhKAtEDQk4NHGUBqKCBBy6OMoDiUYCDm0cAHnqz/oT9+EP7uDQxwGQkx6NRAKOHDgA8p8+jUACjjw4APJCr3oiAUcuHAA5068eSMCRDwdALvTMEwk4cuIAyJW+eSABR14cANnQuxYk4NgQsPglU/9GoVf2FiTg8Eo/dh+AbMx/DxJwbAw1wWUA2dGkLUjAsSPQBJcCZGeTLiEBx84wE1wOEEOTXkICDkOQCZYAxNikUyTgMIaYYBlAGpp0RLIuP3x5szw0bMNS4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotP4B/zpSHgk1qeLgAAAABJRU5ErkJggg==" id="icon" x="414" y="0" width="195" height="195" transform="translate(0 286.5) "/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/img/header-bg.jpg b/ping/frontend/static/img/header-bg.jpg Binary files differnew file mode 100644 index 0000000..ca76595 --- /dev/null +++ b/ping/frontend/static/img/header-bg.jpg diff --git a/ping/frontend/static/img/logo.svg b/ping/frontend/static/img/logo.svg new file mode 100644 index 0000000..c73d1fe --- /dev/null +++ b/ping/frontend/static/img/logo.svg @@ -0,0 +1,41 @@ +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg222447" version="1.1" viewBox="64.45 293.63 895.08 160.43"> + <metadata id="metadata222453"> + <rdf:rdf> + <cc:work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </cc:work> + </rdf:rdf> + </metadata> + <defs id="defs222451"/> + <linearGradient spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient2-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop222428" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop222430" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <linearGradient gradientTransform="rotate(-30)" spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient3-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop222433" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop222435" stop-opacity="1" stop-color="#cccccc" offset="50%"/> + <stop id="stop222437" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <g id="logo-group"> + <image xlink:href="" id="container" style="display: none;" x="272" y="144" width="480" height="480" transform="translate(0 0)"/> + <g id="logo-center" transform="translate(5.684341886080802e-14 0)"> + <image xlink:href="" id="icon_container" style="display: none;"/> + <g id="slogan" style="font-style:normal;font-weight:400;font-size:32px;line-height:1;font-family:Montserrat;font-variant-ligatures:none;text-align:center;text-anchor:middle" transform="translate(0 0)"/> + <g id="title" style="font-style:normal;font-weight:400;font-size:72px;line-height:1;font-family:'Brandmark Dots 1 Color';font-variant-ligatures:normal;text-align:center;text-anchor:middle" transform="translate(0 0)"> + <g id="path222456" aria-label="P" transform="translate(0 312.40198) translate(66.20587072499995 18.536) scale(1.65) translate(-270.48781 50.4)"> <path class="c1" d="M116.52588,101.7917c-1.36816,2.23242-4.32031,2.95215-6.55273,1.51172 c-2.30469-1.36719-2.95215-4.32031-1.58398-6.55176c0.86426-1.44043,1.44043-3.09668,1.44043-4.6084 c0-1.87207-0.43262-7.77734-6.19336-7.77734H90.24365v36.0752c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.5918,2.16016-4.75195,4.75293-4.75195h18.2168 c10.80176,0,15.69727,9,15.69727,17.35352C119.3335,95.3835,118.32568,98.83955,116.52588,101.7917z" transform="translate(189.82131 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M99.74854,109.35225c-2.59277,0-4.75293-2.16016-4.75293-4.82422c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.75195,2.16016,4.75195,4.75195C104.50049,107.19209,102.4126,109.35225,99.74854,109.35225z" transform="translate(189.82131 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222458" aria-label="A" transform="translate(0 312.40198) translate(127.67317222499996 18.536) scale(1.65) translate(-307.74072 50.4)"> <path class="c1" d="M117.13755,104.6003l-12.81689-27.07422c-0.79199-1.65625-2.52051-2.73633-4.32031-2.73633 c-1.87207,0-3.52832,1.08008-4.32031,2.73633l-19.44189,40.82715c-1.08008,2.37598-0.07178,5.25684,2.3042,6.4082 c2.37598,1.08008,5.18457,0.07227,6.33643-2.30371l4.53662-9.50488h24.19385c1.65625,0,3.09619-0.86426,3.96045-2.16016 C119.22593,108.34444,117.92954,106.25655,117.13755,104.6003z M93.30357,104.74385l6.69678-14.11328l6.76855,14.11328H93.30357z" transform="translate(231.934901 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M124.19419,120.44112c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59229,0-4.75244-2.16016-4.75244-4.75293c0-2.66406,2.16016-4.82422,4.75244-4.82422 C122.03403,115.6169,124.19419,117.77705,124.19419,120.44112z" transform="translate(231.934901 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222460" aria-label="T" transform="translate(0 312.40198) translate(200.92322272499987 18.536) scale(1.65) translate(-352.13469 50.4)"> <path class="c1" d="M104.75244,120.44112c0,2.59277-2.08789,4.75293-4.75293,4.75293 c-2.66406,0-4.75195-2.16016-4.75195-4.75293v-36.0752H84.80615c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195h15.19336c2.88086,0,4.75293,2.16016,4.75293,4.75195V120.44112z" transform="translate(272.08049 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M119.9458,79.5417c0,2.66406-2.16016,4.82422-4.75293,4.82422c-2.66406,0-4.82422-2.16016-4.82422-4.82422 c0-2.5918,2.16016-4.75195,4.82422-4.75195C117.78564,74.78975,119.9458,76.94991,119.9458,79.5417z" transform="translate(272.08049 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222462" aria-label="A" transform="translate(0 312.40198) translate(261.55521172499994 18.536) scale(1.65) translate(-388.88135 50.4)"> <path class="c1" d="M117.13755,104.6003l-12.81689-27.07422c-0.79199-1.65625-2.52051-2.73633-4.32031-2.73633 c-1.87207,0-3.52832,1.08008-4.32031,2.73633l-19.44189,40.82715c-1.08008,2.37598-0.07178,5.25684,2.3042,6.4082 c2.37598,1.08008,5.18457,0.07227,6.33643-2.30371l4.53662-9.50488h24.19385c1.65625,0,3.09619-0.86426,3.96045-2.16016 C119.22593,108.34444,117.92954,106.25655,117.13755,104.6003z M93.30357,104.74385l6.69678-14.11328l6.76855,14.11328H93.30357z" transform="translate(313.075531 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M124.19419,120.44112c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59229,0-4.75244-2.16016-4.75244-4.75293c0-2.66406,2.16016-4.82422,4.75244-4.82422 C122.03403,115.6169,124.19419,117.77705,124.19419,120.44112z" transform="translate(313.075531 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222464" aria-label="P" transform="translate(0 312.40198) translate(355.5488477249999 18.536) scale(1.65) translate(-445.84719 50.4)"> <path class="c1" d="M116.52588,101.7917c-1.36816,2.23242-4.32031,2.95215-6.55273,1.51172 c-2.30469-1.36719-2.95215-4.32031-1.58398-6.55176c0.86426-1.44043,1.44043-3.09668,1.44043-4.6084 c0-1.87207-0.43262-7.77734-6.19336-7.77734H90.24365v36.0752c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.5918,2.16016-4.75195,4.75293-4.75195h18.2168 c10.80176,0,15.69727,9,15.69727,17.35352C119.3335,95.3835,118.32568,98.83955,116.52588,101.7917z" transform="translate(365.18069 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M99.74854,109.35225c-2.59277,0-4.75293-2.16016-4.75293-4.82422c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.75195,2.16016,4.75195,4.75195C104.50049,107.19209,102.4126,109.35225,99.74854,109.35225z" transform="translate(365.18069 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222466" aria-label="I" transform="translate(0 312.40198) translate(435.39543322499986 16.39760000000001) scale(1.65) translate(-494.23906 51.696)"> <path class="c3" d="M95.21143,78.2458c0-2.59277,2.16016-4.75293,4.75293-4.75293c2.66406,0,4.82422,2.16016,4.82422,4.75293 c0,2.66406-2.16016,4.82422-4.82422,4.82422C97.37158,83.07002,95.21143,80.90987,95.21143,78.2458z" transform="translate(399.02763 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> <path class="c1" d="M95.21143,120.44112V92.57491c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.82422,2.16016,4.82422,4.75195v27.86621c0,2.59277-2.16016,4.75293-4.82422,4.75293 C97.37158,125.19405,95.21143,123.03389,95.21143,120.44112z" transform="translate(399.02763 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> </g> + <g id="path222468" aria-label="M" transform="translate(0 312.40198) translate(475.07278522499985 18.550630549999994) scale(1.65) translate(-518.28594 50.391133)"> <path class="c1" d="M117.89355,95.59932l-14.04102,27.14648c-0.86426,1.51172-2.44824,2.44824-4.17676,2.44824 c-1.72754,0-3.31152-0.93652-4.17578-2.52051L82.10645,96.03096v24.41016c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.16016,1.44043-4.03223,3.60059-4.6084 c2.08789-0.50391,4.32031,0.43262,5.3291,2.37598l18.28906,34.70703l18.79395-34.77832 c1.00781-1.87207,3.24023-2.80859,5.32813-2.30469c2.08789,0.57617,3.60059,2.44824,3.60059,4.6084v26.57031 c0,2.59277-2.16016,4.75293-4.82422,4.75293c-2.59277,0-4.75293-2.16016-4.75293-4.75293V95.59932z" transform="translate(445.75664 -125.18939)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M122.64648,125.19405c-2.59277,0-4.75293-2.16016-4.75293-4.75293 c0-2.66406,2.16016-4.82422,4.75293-4.82422c2.66406,0,4.82422,2.16016,4.82422,4.82422 C127.4707,123.03389,125.31055,125.19405,122.64648,125.19405z" transform="translate(445.75664 -125.18939)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222470" aria-label="V" style="display: none;"> <path class="c1" d="M104.3493,122.38545c-0.71973,1.72852-2.44824,2.87988-4.3916,2.87988 c-1.87207,0-3.60059-1.15137-4.32031-2.87988l-17.8584-40.89941c-1.08008-2.44824,0-5.25684,2.44824-6.26465 c2.37695-1.08008,5.18457,0,6.26465,2.44824l13.53711,31.10645l7.77734-18.00098c1.00781-2.37695,3.81641-3.45703,6.26465-2.44824 c2.37598,1.08008,3.52832,3.8877,2.44824,6.33594L104.3493,122.38545z" transform="translate(505.227103 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M122.63934,79.5417c0,2.66406-2.08789,4.82422-4.75293,4.82422c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195C120.55145,74.78975,122.63934,76.94991,122.63934,79.5417z" transform="translate(505.227103 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222472" aria-label="E" transform="translate(0 312.40198) translate(755.8694062749997 18.536) scale(1.65) translate(-636.53469 50.4)"> <path class="c1" d="M87.75879,125.19405c-2.5918,0-4.75195-2.16016-4.75195-4.75293V79.5417 c0-2.5918,2.16016-4.75195,4.75195-4.75195h9.9375c2.5918,0,4.75195,2.16016,4.75195,4.75195 c0,2.66406-2.16016,4.82422-4.75195,4.82422h-5.1123V95.3835h16.70508c2.66406,0,4.75195,1.94434,4.75195,4.6084 c0,2.5918-2.08789,4.53613-4.75195,4.53613H92.58398v11.08887h19.65723c2.66406,0,4.75195,2.16016,4.75195,4.82422 c0,2.59277-2.08789,4.75293-4.75195,4.75293H87.75879z" transform="translate(553.52785 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M112.02539,84.36592c-2.66406,0-4.82422-2.16016-4.82422-4.82422c0-2.5918,2.16016-4.75195,4.82422-4.75195 c2.5918,0,4.75195,2.16016,4.75195,4.75195C116.77734,82.20576,114.61719,84.36592,112.02539,84.36592z" transform="translate(553.52785 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path222474" aria-label="S" transform="translate(0 312.40198) translate(825.0332812749998 16.39760000000001) scale(1.65) translate(-678.45219 51.696)"> <path class="c3" d="M90.67529,115.04073c0,2.66406-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.08887-4.75293-4.75293s2.16016-4.75195,4.75293-4.75195 C88.51514,110.28877,90.67529,112.37666,90.67529,115.04073z" transform="translate(597.35405 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> <path class="c1" d="M102.48486,126.48994c-3.74512,0-8.78516-1.44043-8.78516-5.1123 c0-3.02441,2.01563-4.68066,4.39258-4.53613c2.44824,0.14355,5.1123,1.00781,7.7041-0.2168 c2.0166-0.93555,3.52832-2.73633,3.52832-5.25586c0-3.02441-3.38379-4.96875-10.72852-7.63281 c-6.625-2.37598-16.56152-5.32813-16.63379-15.19336c0-3.74414,1.44043-7.27246,4.10449-10.00879 c2.30469-2.30469,6.55273-5.04102,13.96973-5.04102c8.42383,0,14.04102,4.46484,15.91309,7.12891 c1.2959,1.72852,1.08008,4.03223-0.21582,5.68848c-1.72852,2.16016-4.32031,1.94434-6.12109,0.36035 c-1.2959-1.15234-4.32031-4.17676-9.86426-4.17676c-4.8252,0-8.20898,2.52051-8.20898,6.04883 c0,2.30371,2.52051,4.24805,10.00879,7.12891c9.2168,3.52832,17.35352,7.41602,17.35352,16.27344 C118.90186,119.07295,113.93311,126.48994,102.48486,126.48994z" transform="translate(597.35405 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> </g> + <g id="path222476" aria-label="T" transform="translate(0 312.40198) translate(891.9789292749999 18.536) scale(1.65) translate(-719.02531 50.4)"> <path class="c1" d="M104.75244,120.44112c0,2.59277-2.08789,4.75293-4.75293,4.75293 c-2.66406,0-4.75195-2.16016-4.75195-4.75293v-36.0752H84.80615c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195h15.19336c2.88086,0,4.75293,2.16016,4.75293,4.75195V120.44112z" transform="translate(638.97111 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M119.9458,79.5417c0,2.66406-2.16016,4.82422-4.75293,4.82422c-2.66406,0-4.82422-2.16016-4.82422-4.82422 c0-2.5918,2.16016-4.75195,4.82422-4.75195C117.78564,74.78975,119.9458,76.94991,119.9458,79.5417z" transform="translate(638.97111 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + </g> + <image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAIoUlEQVR4Xu3dzXYTRxCG4TEn1wXsSe4I/Ad3BOxJ7ioblBHYHCWxpJnq6q6vul7W0z3VX9VzxMiyfLPwjwRI4GwCN2RDAiRwPgGAMB0kcCEBgDAeJAAQZoAEbAnwCmLLjVVFEgBIkUZzTFsCALHlxqoiCQCkSKM5pi0BgNhyY1WRBABSpNEc05YAQGy5sapIAgAp0miOaUsAILbcWFUkAYAUaTTHtCUAEFturCqSAECKNJpj2hIAiC03VhVJACBFGs0xbQkAxJYbq4okAJAijeaYtgQAYsuNVUUSAEiRRnNMWwIAseXGqiIJAKRIozmmLQGA2HJjVZEEAFKk0RzTlgBAbLmxqkgCACnSaI5pSwAgttxYVSQBgBRpNMe0JQAQW26sKpIAQIo02uuY94flN6+9MuwDkAxdEqrx3bflIFRO91IA0j3iuW4AkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cxjkBgDgHynZzJQCQufrJaZwTAIhzoGw3VwIAmaufnMY5AYA4B8p2cyUAkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cJmECv/+53B8Oy51C6fxOukIXqOFXAko4jkUBhOGUSUANB0BkRoNCFHEAhLmUSEAVB0AkxqN2Eco4AFJ7NsNPr44DIOEjUreADDgAUnc+Q0+eBQdAQsek5s0z4QBIzRkNO3U2HAAJG5V6N86IAyD15jTkxFlxACRkXGrdNDMOgNSa1eGnHY3j5mb5sH4K+KPnQfmwomea7PUrgQgcX94sn7x/XwUgDLV7AlE4jgcBiHs72dAzgUgcAPHsJHu5JxCNAyDuLWVDrwQUcMgAWcM4vlPw9/GhyCtg9smbgAoOCSBHHOtbae9/vEe8vq0GkryD7VH5u7+Wh+X7cuux15Y9rs1c6EP6KY7nw1wreMuhuSZnAmo4Ql9BXsIBkpyD7VG1Io4wIJdwgMRj3HLtoYojBMgWHCDJNeAt1SrjGA5kDw6QtIxdjrXqOIYCseAASY5Bt1SZAccwIC04foX/arn9+tr3k5WWxrKmPYEsOIYAccHx3BOQtE9n8A6ZcHQH4ooDJMGj3X77bDi6AumCAyTtUxq0Q0Yc3YB0xQGSoBG33zYrji5AhuAAiX1aB6/MjKMLkDWQD+uHzR6H9YEH92FR771RdhxdgPzYFCR7Z2m662fA0Q1IBJJXy3L3+e3AV67pRtrvQLPg6AoEJH4Dl2mnmXB0BwKSTKPdXutsOIYAAUn74GXYYTSOZdCbM8N+o3D0gzvPJONYzYqjR4IXvzgOJD0ij90THPvyv/rNiiDZF6jy1X98Wx6/L+vPvUb9G/Tfqp7HuQqEZ5Ke8Y/bGxy2rDcBiUCyflvK/fqVQg+2Y7HqNAFw2OdhMxCQ2EOOXAmOtvR3AQFJW9ijV4OjPfHdQEDSHvqIHcDhk7IJCEh8wu+1Czj8kjUDAYlfEzx3Aodnmuv3T7duN/rnJLy7db5j4Gid5v+vbwbCK4l/Uyw7gsOS2vU1LkCCkDysPye5v37E+a8AR78euwEBSb8mXdoZHH1zdwVyLHVt2O36eZ9hPwFfn0nKvpKAoy+O4+7uQEDSv2lPGfPBwwFRdwECkr6d45Wjb76nu3cDApI+TQRHn1zP7doVCEh8mwkO3zy37NYdCEi2tOH6NeC4nlGPK4YAAUlb68DRll/L6mFAQGJrEzhsuXmtGgoEJPvaBo59efW4ejgQkGxrIzi25dT7qhAgILnc1qF/kmIthe8kO9+PMCAgebkp4Oj9mrBv/1AgIPl3s8Cxb3hHXB0OBCQ/2wyOEeO+/x4SQKojAcf+wR21QgZIBJL16fTx6+vlblTYL90HHJHpX7+3FJBqSMBxfUCjr5ADUgUJOKJHf9v9JYHMjgQc24ZT4SpZILMiAYfC2G+vQRrI09ufd4fDwG8v6fjgDo7tg6lypTyQWZCAQ2Xk99WRAkh2JODYN5RKV6cBEoFk/RDfx89vl9uWhoGjJb34tamAZEMCjvgBb60gHZAsSMDROpoa61MCUUcCDo3h9qgiLRBVJODwGEudPVIDUUMCDp3B9qokPRAVJODwGkmtfaYAEo0EHFpD7VnNNEAikKx/euHT8b7rR2Heezbl0l58wcKopH/eZyogEUhGtgscI9OeFMisSMAxHseUryDPMa7PBWM/Bdyxf+DoGO6Vraf7L9bpeWdAwp+9jsMx9SvIDK8k4IjFUQJI1mcScMTjKAMkGxJwaOAoBSQLEnDo4CgH5AnJ/fqDvdAvizs3AuDQwlESiCoScOjhKAtEDQk4NHGUBqKCBBy6OMoDiUYCDm0cAHnqz/oT9+EP7uDQxwGQkx6NRAKOHDgA8p8+jUACjjw4APJCr3oiAUcuHAA5068eSMCRDwdALvTMEwk4cuIAyJW+eSABR14cANnQuxYk4NgQsPglU/9GoVf2FiTg8Eo/dh+AbMx/DxJwbAw1wWUA2dGkLUjAsSPQBJcCZGeTLiEBx84wE1wOEEOTXkICDkOQCZYAxNikUyTgMIaYYBlAGpp0RLIuP3x5szw0bMNS4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotP4B/zpSHgk1qeLgAAAABJRU5ErkJggg==" id="icon" x="582" y="-73" width="97.20533333333334" height="97.20533333333334" transform="translate(0 312.40198) translate(581.1707717249999 18.536) scale(1.65) translate(-582.58775 50.4)"/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/ping/frontend/static/img/logoFull.svg b/ping/frontend/static/img/logoFull.svg new file mode 100644 index 0000000..505564a --- /dev/null +++ b/ping/frontend/static/img/logoFull.svg @@ -0,0 +1,75 @@ +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg282154" version="1.1" viewBox="64.45 264.58 895.08 203.68"> + <metadata id="metadata282160"> + <rdf:rdf> + <cc:work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </cc:work> + </rdf:rdf> + </metadata> + <defs id="defs282158"/> + <linearGradient spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient2-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop282135" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop282137" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <linearGradient gradientTransform="rotate(-30)" spreadMethod="pad" y2="30%" x2="-10%" y1="120%" x1="30%" id="3d_gradient3-logo-9cc91ee9-bcf4-468b-9589-30bc930545e6"> + <stop id="stop282140" stop-opacity="1" stop-color="#ffffff" offset="0%"/> + <stop id="stop282142" stop-opacity="1" stop-color="#cccccc" offset="50%"/> + <stop id="stop282144" stop-opacity="1" stop-color="#000000" offset="100%"/> + </linearGradient> + <g id="logo-group"> + <image xlink:href="" id="container" style="display: none;" x="272" y="144" width="480" height="480" transform="translate(0 0)"/> + <g id="logo-center" transform="translate(5.684341886080802e-14 0)"> + <image xlink:href="" id="icon_container" style="display: none;" x="0" y="0"/> + <g id="slogan" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" transform="translate(0 0)"> + <path id="path282162" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 266.17275,-13.12 v 3.872 h 3.584 v 1.824 c 0,1.984 -1.6,3.584 -3.584,3.584 -1.984,0 -3.584,-1.6 -3.584,-3.584 v -7.488 c 0,-1.472 0.896,-2.752 2.208,-3.328 1.344,-0.544 2.88,-0.224 3.904,0.8 l 2.752,-2.752 c -2.144,-2.112 -5.344,-2.752 -8.128,-1.6 -2.784,1.152 -4.608,3.84 -4.576,6.88 v 7.488 c 0,4.096 3.328,7.392 7.424,7.392 4.096,0 7.424,-3.296 7.456,-7.392 v -5.696 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(252.41685344999996 54.29372769) scale(0.91) translate(-258.74834 22.358541)"/> + <path id="path282164" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 281.40225,-14.08 c -0.448,0 -0.928,0.064 -1.376,0.192 v -0.192 h -3.84 V 0.032 h 3.84 v -8.864 c 0,-0.544 0.352,-1.056 0.864,-1.28 0.512,-0.192 1.088,-0.096 1.504,0.32 l 2.72,-2.752 c -0.992,-0.992 -2.304,-1.536 -3.712,-1.536 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(270.10535155 61.8272) scale(0.91) translate(-276.18625 14.08)"/> + <path id="path282166" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 292.28425,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(280.03966685000006 61.84307404) scale(0.91) translate(-285.10308 14.062556)"/> + <path id="path282168" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 308.15925,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(296.3059168500001 61.84307404) scale(0.91) translate(-300.97808 14.062556)"/> + <path id="path282170" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 323.36225,-14.08 c -0.8,0 -1.6,0.16 -2.336,0.448 v -0.448 h -3.84 V 0.032 h 3.84 v -7.936 c 0,-1.28 1.056,-2.304 2.336,-2.304 1.28,0 2.304,1.024 2.304,2.304 v 7.936 h 3.872 v -7.936 c 0,-3.392 -2.752,-6.176 -6.176,-6.176 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(312.87535155000006 61.8272) scale(0.91) translate(-317.18625 14.08)"/> + <path id="path282172" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 345.15125,0 v -14.048 h -3.84 V 0 Z m 0,-18.208 v -4.096 h -3.84 v 4.096 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(336.64910155000007 54.343360000000004) scale(0.91) translate(-341.31125 22.304)"/> + <path id="path282174" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 353.92475,-14.08 c -0.8,0 -1.6,0.16 -2.336,0.448 v -0.448 h -3.84 V 0.032 h 3.84 v -7.936 c 0,-1.28 1.056,-2.304 2.336,-2.304 1.28,0 2.304,1.024 2.304,2.304 v 7.936 h 3.872 v -7.936 c 0,-3.392 -2.752,-6.176 -6.176,-6.176 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(344.3272265500001 61.8272) scale(0.91) translate(-347.74875 14.08)"/> + <path id="path282176" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 373.37425,-14.08 -3.744,9.024 -3.744,-9.024 h -4.16 l 5.824,14.112 h 4.192 l 5.824,-14.112 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(358.8667515500001 61.8272) scale(0.91) translate(-361.72625 14.08)"/> + <path id="path282178" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 385.22175,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(375.53279185000014 61.84307404) scale(0.91) translate(-378.04058 14.062556)"/> + <path id="path282180" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 403.72075,-7.648 c -0.832,-0.864 -1.984,-1.312 -3.168,-1.312 h -2.304 c -0.352,0 -0.64,-0.288 -0.64,-0.64 0,-0.32 0.288,-0.608 0.64,-0.608 h 5.472 v -3.872 h -5.472 c -2.496,0 -4.512,2.016 -4.512,4.48 0,2.496 2.016,4.512 4.512,4.512 h 2.304 c 0.352,0 0.608,0.256 0.608,0.608 0,0.352 -0.256,0.608 -0.608,0.608 h -5.504 V 0 h 5.568 c 1.792,-0.032 3.424,-1.12 4.096,-2.784 0.672,-1.664 0.288,-3.584 -0.992,-4.864 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(391.63630655000014 61.8272) scale(0.91) translate(-393.73675 14.08)"/> + <path id="path282182" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 413.29975,-10.208 v -3.872 h -1.824 v -8.288 h -3.872 v 8.288 h -1.12 v 3.872 h 1.12 v 6.432 c 0,2.08 1.696,3.776 3.776,3.776 h 1.92 v -3.872 h -1.824 v -6.336 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(405.05607655000017 54.285120000000006) scale(0.91) translate(-406.48375 22.368)"/> + <path id="path282184" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 419.40125,0 v -14.048 h -3.84 V 0 Z m 0,-18.208 v -4.096 h -3.84 v 4.096 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(415.1366015500002 54.343360000000004) scale(0.91) translate(-415.56125 22.304)"/> + <path id="path282186" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 428.17475,-14.08 c -0.8,0 -1.6,0.16 -2.336,0.448 v -0.448 h -3.84 V 0.032 h 3.84 v -7.936 c 0,-1.28 1.056,-2.304 2.336,-2.304 1.28,0 2.304,1.024 2.304,2.304 v 7.936 h 3.872 v -7.936 c 0,-3.392 -2.752,-6.176 -6.176,-6.176 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(422.8147265500002 61.8272) scale(0.91) translate(-421.99875 14.08)"/> + <path id="path282188" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 450.60025,-14.08 c -0.992,0 -1.984,0.416 -2.688,1.12 -0.064,0.064 -0.128,0.128 -0.192,0.192 -2.816,-2.016 -6.752,-1.664 -9.152,0.864 -2.432,2.528 -2.624,6.432 -0.48,9.216 -1.376,1.216 -1.856,3.2 -1.184,4.928 0.672,1.728 2.336,2.88 4.16,2.88 h 5.152 c 0.32,0 0.608,0.256 0.608,0.608 0,0.32 -0.288,0.608 -0.608,0.608 h -4.512 v 3.872 h 4.512 c 2.464,0 4.48,-2.016 4.48,-4.48 0,-2.496 -2.016,-4.48 -4.48,-4.48 h -5.12 c -0.352,0 -0.64,-0.288 -0.64,-0.608 0,-0.352 0.288,-0.64 0.64,-0.64 h 2.56 c 1.856,0.032 3.648,-0.736 4.96,-2.048 1.312,-1.312 2.08,-3.104 2.08,-4.992 v -3.168 h 1.824 v -3.872 z m -6.944,3.872 c 1.28,0 2.432,0.768 2.912,1.984 0.48,1.184 0.224,2.528 -0.672,3.456 -0.928,0.896 -2.272,1.152 -3.456,0.672 -1.184,-0.48 -1.984,-1.632 -1.984,-2.944 0,-1.728 1.44,-3.168 3.2,-3.168 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(437.9245940500003 61.8272) scale(0.91) translate(-436.603 14.08)"/> + <path id="path282190" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 478.68675,-14.08 c -1.6,0 -3.104,0.608 -4.256,1.696 -1.76,-1.664 -4.32,-2.144 -6.592,-1.248 v -0.448 h -3.84 V 0 h 3.84 v -7.904 c 0,-1.28 1.056,-2.304 2.336,-2.304 1.28,0 2.304,1.024 2.304,2.304 v 7.936 h 3.872 v -7.936 c 0,-1.28 1.056,-2.304 2.336,-2.304 1.28,0 2.304,1.024 2.304,2.304 v 7.936 h 3.872 v -7.936 c 0,-3.392 -2.784,-6.176 -6.176,-6.176 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(464.67472655000023 61.8272) scale(0.91) translate(-463.99875 14.08)"/> + <path id="path282192" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 497.32425,-14.08 v 0.768 c -2.976,-1.504 -6.56,-0.704 -8.672,1.856 -2.08,2.592 -2.08,6.272 0,8.864 2.112,2.56 5.696,3.36 8.672,1.856 v 0.768 h 3.872 V -14.08 Z m -3.168,10.24 c -1.76,0 -3.168,-1.44 -3.168,-3.2 0,-1.728 1.408,-3.168 3.168,-3.168 1.728,0 3.168,1.44 3.168,3.168 0,1.76 -1.44,3.168 -3.168,3.2 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(487.5098115500003 61.8272) scale(0.91) translate(-487.09225 14.08)"/> + <path id="path282194" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 513.63675,-22.368 v 9.056 c -2.976,-1.504 -6.56,-0.704 -8.672,1.856 -2.08,2.592 -2.08,6.272 0,8.864 2.112,2.56 5.696,3.36 8.672,1.856 v 0.768 h 3.872 v -22.4 z m -3.168,18.528 c -1.76,0 -3.168,-1.44 -3.168,-3.2 0,-1.728 1.408,-3.168 3.168,-3.168 1.728,0 3.168,1.44 3.168,3.168 0,1.76 -1.44,3.168 -3.168,3.2 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(504.1741865500003 54.285120000000006) scale(0.91) translate(-503.40475 22.368)"/> + <path id="path282196" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 526.90925,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(520.8484168500004 61.84307404) scale(0.91) translate(-519.72808 14.062556)"/> + <path id="path282198" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 551.97175,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(545.4752918500004 61.84307404) scale(0.91) translate(-544.79058 14.062556)"/> + <path id="path282200" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 570.88675,-14.08 v 0.768 c -2.976,-1.504 -6.56,-0.704 -8.672,1.856 -2.08,2.592 -2.08,6.272 0,8.864 2.112,2.56 5.696,3.36 8.672,1.856 v 0.768 h 3.872 V -14.08 Z m -3.168,10.24 c -1.76,0 -3.168,-1.44 -3.168,-3.2 0,-1.728 1.408,-3.168 3.168,-3.168 1.728,0 3.168,1.44 3.168,3.168 0,1.76 -1.44,3.168 -3.168,3.2 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(561.7316865500004 61.8272) scale(0.91) translate(-560.65475 14.08)"/> + <path id="path282202" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 586.78325,-7.648 c -0.832,-0.864 -1.984,-1.312 -3.168,-1.312 h -2.304 c -0.352,0 -0.64,-0.288 -0.64,-0.64 0,-0.32 0.288,-0.608 0.64,-0.608 h 5.472 v -3.872 h -5.472 c -2.496,0 -4.512,2.016 -4.512,4.48 0,2.496 2.016,4.512 4.512,4.512 h 2.304 c 0.352,0 0.608,0.256 0.608,0.608 0,0.352 -0.256,0.608 -0.608,0.608 h -5.504 V 0 h 5.568 c 1.792,-0.032 3.424,-1.12 4.096,-2.784 0.672,-1.664 0.288,-3.584 -0.992,-4.864 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(578.2431815500004 61.8272) scale(0.91) translate(-576.79925 14.08)"/> + <path id="path282204" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 598.66625,-14.08 v 7.904 c 0,1.28 -1.024,2.336 -2.304,2.336 -1.28,0 -2.336,-1.056 -2.336,-2.336 v -7.904 h -3.872 v 7.904 c 0.032,2.048 1.024,3.968 2.752,5.12 1.696,1.152 3.872,1.376 5.76,0.608 v 3.616 c 0,1.28 -1.024,2.336 -2.304,2.336 -1.28,0 -2.336,-1.056 -2.336,-2.336 h -3.872 c 0,3.424 2.784,6.208 6.208,6.208 3.424,0 6.176,-2.784 6.176,-6.208 V -14.08 Z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(592.2162315500003 61.8272) scale(0.91) translate(-590.15425 14.08)"/> + <path id="path282206" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 618.15125,0.032 v -22.368 h -3.84 V 0.032 Z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(616.0191015500003 54.31424) scale(0.91) translate(-614.31125 22.336)"/> + <path id="path282208" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 624.58875,0 v -14.048 h -3.84 V 0 Z m 0,-18.208 v -4.096 h -3.84 v 4.096 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(623.6972265500003 54.343360000000004) scale(0.91) translate(-620.74875 22.304)"/> + <path id="path282210" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 638.61025,-14.048 h -3.872 v 1.92 c 0,1.024 -0.832,1.856 -1.856,1.856 -0.608,0 -1.248,0.096 -1.856,0.288 v -12.384 h -3.872 v 22.4 h 3.872 v -4.608 c 0,-0.64 0.352,-1.28 0.928,-1.6 0.576,-0.32 1.28,-0.32 1.856,0 0.576,0.32 0.928,0.96 0.928,1.6 v 4.608 h 3.872 v -4.608 c 0,-1.376 -0.512,-2.72 -1.44,-3.776 0.928,-1.024 1.44,-2.368 1.44,-3.776 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(631.3462315500004 54.285120000000006) scale(0.91) translate(-627.15425 22.368)"/> + <path id="path282212" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 648.03425,-3.936 c -0.576,0.032 -1.152,-0.128 -1.632,-0.416 l 4.288,-1.792 0.544,-0.224 3.904,-1.6 -0.768,-1.792 v -0.032 0 -0.032 h -0.032 v -0.032 0 -0.032 0 l -0.032,-0.032 c -0.064,-0.128 -0.128,-0.256 -0.192,-0.384 0,-0.032 -0.032,-0.096 -0.064,-0.128 -0.064,-0.096 -0.128,-0.192 -0.16,-0.32 -0.032,-0.032 -0.064,-0.096 -0.096,-0.128 -0.064,-0.064 -0.096,-0.16 -0.16,-0.224 v 0 c -1.952,-2.752 -5.568,-3.712 -8.64,-2.336 -3.04,1.376 -4.704,4.736 -3.968,8 v 0.032 c 0.16,0.576 0.352,1.12 0.608,1.632 v 0 c 0.224,0.384 0.448,0.736 0.704,1.088 v 0 c 1.408,1.728 3.552,2.72 5.76,2.688 2.112,0 3.808,-0.64 5.632,-2.24 l -2.496,-2.944 c -1.152,0.992 -2.048,1.248 -3.2,1.248 z m -3.104,-3.584 c 0.096,-1.056 0.8,-2.016 1.792,-2.432 0.32,-0.16 0.704,-0.256 1.088,-0.224 0.672,0 1.344,0.224 1.888,0.64 0,0 0.032,0.032 0.032,0.032 l -2.368,0.992 -2.304,0.96 -0.128,0.064 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(645.6321668500004 61.84307404) scale(0.91) translate(-640.85308 14.062556)"/> + <path id="path282214" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 673.28875,-14.08 c -1.12,0 -2.208,0.256 -3.2,0.768 v -9.056 h -3.84 V 0 h 3.84 v -0.736 c 2.624,1.312 5.76,0.896 7.904,-1.056 2.176,-1.984 2.912,-5.056 1.856,-7.776 -1.056,-2.72 -3.648,-4.512 -6.56,-4.512 z m 0,10.24 c -1.76,0 -3.2,-1.44 -3.2,-3.2 0,-1.728 1.44,-3.168 3.2,-3.168 1.728,0 3.168,1.44 3.168,3.168 0,1.76 -1.44,3.168 -3.168,3.2 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(670.5622265500004 54.285120000000006) scale(0.91) translate(-666.24875 22.368)"/> + <path id="path282216" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 687.77725,-14.08 c -0.448,0 -0.928,0.064 -1.376,0.192 v -0.192 h -3.84 V 0.032 h 3.84 v -8.864 c 0,-0.544 0.352,-1.056 0.864,-1.28 0.512,-0.192 1.088,-0.096 1.504,0.32 l 2.72,-2.752 c -0.992,-0.992 -2.304,-1.536 -3.712,-1.536 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(687.2266015500004 61.8272) scale(0.91) translate(-682.56125 14.08)"/> + <path id="path282218" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 698.65225,-14.08 c -0.448,0 -0.928,0.064 -1.376,0.192 v -0.192 h -3.84 V 0.032 h 3.84 v -8.864 c 0,-0.544 0.352,-1.056 0.864,-1.28 0.512,-0.192 1.088,-0.096 1.504,0.32 l 2.72,-2.752 c -0.992,-0.992 -2.304,-1.536 -3.712,-1.536 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(698.9428515500003 61.8272) scale(0.91) translate(-693.43625 14.08)"/> + <path id="path282220" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 720.53875,-14.08 c -1.12,0 -2.208,0.256 -3.2,0.768 v -9.056 h -3.84 V 0 h 3.84 v -0.736 c 2.624,1.312 5.76,0.896 7.904,-1.056 2.176,-1.984 2.912,-5.056 1.856,-7.776 -1.056,-2.72 -3.648,-4.512 -6.56,-4.512 z m 0,10.24 c -1.76,0 -3.2,-1.44 -3.2,-3.2 0,-1.728 1.44,-3.168 3.2,-3.168 1.728,0 3.168,1.44 3.168,3.168 0,1.76 -1.44,3.168 -3.168,3.2 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(719.0197265500003 54.285120000000006) scale(0.91) translate(-713.49875 22.368)"/> + <path id="path282222" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 735.02725,-14.08 c -0.448,0 -0.928,0.064 -1.376,0.192 v -0.192 h -3.84 V 0.032 h 3.84 v -8.864 c 0,-0.544 0.352,-1.056 0.864,-1.28 0.512,-0.192 1.088,-0.096 1.504,0.32 l 2.72,-2.752 c -0.992,-0.992 -2.304,-1.536 -3.712,-1.536 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(735.6841015500003 61.8272) scale(0.91) translate(-729.81125 14.08)"/> + <path id="path282224" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="m 745.90225,-14.08 c -0.448,0 -0.928,0.064 -1.376,0.192 v -0.192 h -3.84 V 0.032 h 3.84 v -8.864 c 0,-0.544 0.352,-1.056 0.864,-1.28 0.512,-0.192 1.088,-0.096 1.504,0.32 l 2.72,-2.752 c -0.992,-0.992 -2.304,-1.536 -3.712,-1.536 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(747.4003515500002 61.8272) scale(0.91) translate(-740.68625 14.08)"/> + <path id="path282226" style="font-style:normal;font-weight:700;font-size:32px;line-height:1;font-family:'Brandmark Sans 4 Spectrum';font-variant-ligatures:none;text-align:center;text-anchor:middle" d="M 765.26075,0.288 V -4.32 h -4.512 v 4.608 z m 0,-6.368 v -18.56 h -4.512 v 18.56 z" stroke-width="0" stroke-linejoin="miter" stroke-miterlimit="2" fill="#7bc74d" stroke="#7bc74d" transform="translate(-5.684341886080802e-14 384.32698) translate(767.4772265500002 52.217600000000004) scale(0.91) translate(-760.74875 24.64)"/> + </g> + <g id="title" style="font-style:normal;font-weight:400;font-size:72px;line-height:1;font-family:'Brandmark Dots 1 Color';font-variant-ligatures:normal;text-align:center;text-anchor:middle" transform="translate(0 0)"> + <g id="path282229" aria-label="P" transform="translate(0 283.34614) translate(66.20587072499995 18.536) scale(1.65) translate(-270.48781 50.4)"> <path class="c1" d="M116.52588,101.7917c-1.36816,2.23242-4.32031,2.95215-6.55273,1.51172 c-2.30469-1.36719-2.95215-4.32031-1.58398-6.55176c0.86426-1.44043,1.44043-3.09668,1.44043-4.6084 c0-1.87207-0.43262-7.77734-6.19336-7.77734H90.24365v36.0752c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.5918,2.16016-4.75195,4.75293-4.75195h18.2168 c10.80176,0,15.69727,9,15.69727,17.35352C119.3335,95.3835,118.32568,98.83955,116.52588,101.7917z" transform="translate(189.82131 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M99.74854,109.35225c-2.59277,0-4.75293-2.16016-4.75293-4.82422c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.75195,2.16016,4.75195,4.75195C104.50049,107.19209,102.4126,109.35225,99.74854,109.35225z" transform="translate(189.82131 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282231" aria-label="A" transform="translate(0 283.34614) translate(127.67317222499996 18.536) scale(1.65) translate(-307.74072 50.4)"> <path class="c1" d="M117.13755,104.6003l-12.81689-27.07422c-0.79199-1.65625-2.52051-2.73633-4.32031-2.73633 c-1.87207,0-3.52832,1.08008-4.32031,2.73633l-19.44189,40.82715c-1.08008,2.37598-0.07178,5.25684,2.3042,6.4082 c2.37598,1.08008,5.18457,0.07227,6.33643-2.30371l4.53662-9.50488h24.19385c1.65625,0,3.09619-0.86426,3.96045-2.16016 C119.22593,108.34444,117.92954,106.25655,117.13755,104.6003z M93.30357,104.74385l6.69678-14.11328l6.76855,14.11328H93.30357z" transform="translate(231.934901 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M124.19419,120.44112c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59229,0-4.75244-2.16016-4.75244-4.75293c0-2.66406,2.16016-4.82422,4.75244-4.82422 C122.03403,115.6169,124.19419,117.77705,124.19419,120.44112z" transform="translate(231.934901 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282233" aria-label="T" transform="translate(0 283.34614) translate(200.92322272499987 18.536) scale(1.65) translate(-352.13469 50.4)"> <path class="c1" d="M104.75244,120.44112c0,2.59277-2.08789,4.75293-4.75293,4.75293 c-2.66406,0-4.75195-2.16016-4.75195-4.75293v-36.0752H84.80615c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195h15.19336c2.88086,0,4.75293,2.16016,4.75293,4.75195V120.44112z" transform="translate(272.08049 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M119.9458,79.5417c0,2.66406-2.16016,4.82422-4.75293,4.82422c-2.66406,0-4.82422-2.16016-4.82422-4.82422 c0-2.5918,2.16016-4.75195,4.82422-4.75195C117.78564,74.78975,119.9458,76.94991,119.9458,79.5417z" transform="translate(272.08049 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282235" aria-label="A" transform="translate(0 283.34614) translate(261.55521172499994 18.536) scale(1.65) translate(-388.88135 50.4)"> <path class="c1" d="M117.13755,104.6003l-12.81689-27.07422c-0.79199-1.65625-2.52051-2.73633-4.32031-2.73633 c-1.87207,0-3.52832,1.08008-4.32031,2.73633l-19.44189,40.82715c-1.08008,2.37598-0.07178,5.25684,2.3042,6.4082 c2.37598,1.08008,5.18457,0.07227,6.33643-2.30371l4.53662-9.50488h24.19385c1.65625,0,3.09619-0.86426,3.96045-2.16016 C119.22593,108.34444,117.92954,106.25655,117.13755,104.6003z M93.30357,104.74385l6.69678-14.11328l6.76855,14.11328H93.30357z" transform="translate(313.075531 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M124.19419,120.44112c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59229,0-4.75244-2.16016-4.75244-4.75293c0-2.66406,2.16016-4.82422,4.75244-4.82422 C122.03403,115.6169,124.19419,117.77705,124.19419,120.44112z" transform="translate(313.075531 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282237" aria-label="P" transform="translate(0 283.34614) translate(355.5488477249999 18.536) scale(1.65) translate(-445.84719 50.4)"> <path class="c1" d="M116.52588,101.7917c-1.36816,2.23242-4.32031,2.95215-6.55273,1.51172 c-2.30469-1.36719-2.95215-4.32031-1.58398-6.55176c0.86426-1.44043,1.44043-3.09668,1.44043-4.6084 c0-1.87207-0.43262-7.77734-6.19336-7.77734H90.24365v36.0752c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.5918,2.16016-4.75195,4.75293-4.75195h18.2168 c10.80176,0,15.69727,9,15.69727,17.35352C119.3335,95.3835,118.32568,98.83955,116.52588,101.7917z" transform="translate(365.18069 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M99.74854,109.35225c-2.59277,0-4.75293-2.16016-4.75293-4.82422c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.75195,2.16016,4.75195,4.75195C104.50049,107.19209,102.4126,109.35225,99.74854,109.35225z" transform="translate(365.18069 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282239" aria-label="I" transform="translate(0 283.34614) translate(435.39543322499986 16.39760000000001) scale(1.65) translate(-494.23906 51.696)"> <path class="c3" d="M95.21143,78.2458c0-2.59277,2.16016-4.75293,4.75293-4.75293c2.66406,0,4.82422,2.16016,4.82422,4.75293 c0,2.66406-2.16016,4.82422-4.82422,4.82422C97.37158,83.07002,95.21143,80.90987,95.21143,78.2458z" transform="translate(399.02763 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> <path class="c1" d="M95.21143,120.44112V92.57491c0-2.5918,2.16016-4.75195,4.75293-4.75195 c2.66406,0,4.82422,2.16016,4.82422,4.75195v27.86621c0,2.59277-2.16016,4.75293-4.82422,4.75293 C97.37158,125.19405,95.21143,123.03389,95.21143,120.44112z" transform="translate(399.02763 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> </g> + <g id="path282241" aria-label="M" transform="translate(0 283.34614) translate(475.07278522499985 18.550630549999994) scale(1.65) translate(-518.28594 50.391133)"> <path class="c1" d="M117.89355,95.59932l-14.04102,27.14648c-0.86426,1.51172-2.44824,2.44824-4.17676,2.44824 c-1.72754,0-3.31152-0.93652-4.17578-2.52051L82.10645,96.03096v24.41016c0,2.59277-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.16016-4.75293-4.75293V79.5417c0-2.16016,1.44043-4.03223,3.60059-4.6084 c2.08789-0.50391,4.32031,0.43262,5.3291,2.37598l18.28906,34.70703l18.79395-34.77832 c1.00781-1.87207,3.24023-2.80859,5.32813-2.30469c2.08789,0.57617,3.60059,2.44824,3.60059,4.6084v26.57031 c0,2.59277-2.16016,4.75293-4.82422,4.75293c-2.59277,0-4.75293-2.16016-4.75293-4.75293V95.59932z" transform="translate(445.75664 -125.18939)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M122.64648,125.19405c-2.59277,0-4.75293-2.16016-4.75293-4.75293 c0-2.66406,2.16016-4.82422,4.75293-4.82422c2.66406,0,4.82422,2.16016,4.82422,4.82422 C127.4707,123.03389,125.31055,125.19405,122.64648,125.19405z" transform="translate(445.75664 -125.18939)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282243" aria-label="V" style="display: none;" transform="translate(0 404.62816) translate(624.0138635 18.536) scale(1.65) translate(-582.58775 50.4)"> <path class="c1" d="M104.3493,122.38545c-0.71973,1.72852-2.44824,2.87988-4.3916,2.87988 c-1.87207,0-3.60059-1.15137-4.32031-2.87988l-17.8584-40.89941c-1.08008-2.44824,0-5.25684,2.44824-6.26465 c2.37695-1.08008,5.18457,0,6.26465,2.44824l13.53711,31.10645l7.77734-18.00098c1.00781-2.37695,3.81641-3.45703,6.26465-2.44824 c2.37598,1.08008,3.52832,3.8877,2.44824,6.33594L104.3493,122.38545z" transform="translate(505.227103 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M122.63934,79.5417c0,2.66406-2.08789,4.82422-4.75293,4.82422c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195C120.55145,74.78975,122.63934,76.94991,122.63934,79.5417z" transform="translate(505.227103 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282245" aria-label="E" transform="translate(0 283.34614) translate(755.8694062749997 18.536) scale(1.65) translate(-636.53469 50.4)"> <path class="c1" d="M87.75879,125.19405c-2.5918,0-4.75195-2.16016-4.75195-4.75293V79.5417 c0-2.5918,2.16016-4.75195,4.75195-4.75195h9.9375c2.5918,0,4.75195,2.16016,4.75195,4.75195 c0,2.66406-2.16016,4.82422-4.75195,4.82422h-5.1123V95.3835h16.70508c2.66406,0,4.75195,1.94434,4.75195,4.6084 c0,2.5918-2.08789,4.53613-4.75195,4.53613H92.58398v11.08887h19.65723c2.66406,0,4.75195,2.16016,4.75195,4.82422 c0,2.59277-2.08789,4.75293-4.75195,4.75293H87.75879z" transform="translate(553.52785 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M112.02539,84.36592c-2.66406,0-4.82422-2.16016-4.82422-4.82422c0-2.5918,2.16016-4.75195,4.82422-4.75195 c2.5918,0,4.75195,2.16016,4.75195,4.75195C116.77734,82.20576,114.61719,84.36592,112.02539,84.36592z" transform="translate(553.52785 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + <g id="path282247" aria-label="S" transform="translate(0 283.34614) translate(825.0332812749998 16.39760000000001) scale(1.65) translate(-678.45219 51.696)"> <path class="c3" d="M90.67529,115.04073c0,2.66406-2.16016,4.75293-4.82422,4.75293 c-2.59277,0-4.75293-2.08887-4.75293-4.75293s2.16016-4.75195,4.75293-4.75195 C88.51514,110.28877,90.67529,112.37666,90.67529,115.04073z" transform="translate(597.35405 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> <path class="c1" d="M102.48486,126.48994c-3.74512,0-8.78516-1.44043-8.78516-5.1123 c0-3.02441,2.01563-4.68066,4.39258-4.53613c2.44824,0.14355,5.1123,1.00781,7.7041-0.2168 c2.0166-0.93555,3.52832-2.73633,3.52832-5.25586c0-3.02441-3.38379-4.96875-10.72852-7.63281 c-6.625-2.37598-16.56152-5.32813-16.63379-15.19336c0-3.74414,1.44043-7.27246,4.10449-10.00879 c2.30469-2.30469,6.55273-5.04102,13.96973-5.04102c8.42383,0,14.04102,4.46484,15.91309,7.12891 c1.2959,1.72852,1.08008,4.03223-0.21582,5.68848c-1.72852,2.16016-4.32031,1.94434-6.12109,0.36035 c-1.2959-1.15234-4.32031-4.17676-9.86426-4.17676c-4.8252,0-8.20898,2.52051-8.20898,6.04883 c0,2.30371,2.52051,4.24805,10.00879,7.12891c9.2168,3.52832,17.35352,7.41602,17.35352,16.27344 C118.90186,119.07295,113.93311,126.48994,102.48486,126.48994z" transform="translate(597.35405 -125.18887)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> </g> + <g id="path282249" aria-label="T" transform="translate(0 283.34614) translate(891.9789292749999 18.536) scale(1.65) translate(-719.02531 50.4)"> <path class="c1" d="M104.75244,120.44112c0,2.59277-2.08789,4.75293-4.75293,4.75293 c-2.66406,0-4.75195-2.16016-4.75195-4.75293v-36.0752H84.80615c-2.5918,0-4.75195-2.16016-4.75195-4.82422 c0-2.5918,2.16016-4.75195,4.75195-4.75195h15.19336c2.88086,0,4.75293,2.16016,4.75293,4.75195V120.44112z" transform="translate(638.97111 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#47a200" stroke="#47a200"/> <path class="c3" d="M119.9458,79.5417c0,2.66406-2.16016,4.82422-4.75293,4.82422c-2.66406,0-4.82422-2.16016-4.82422-4.82422 c0-2.5918,2.16016-4.75195,4.82422-4.75195C117.78564,74.78975,119.9458,76.94991,119.9458,79.5417z" transform="translate(638.97111 -125.18975)" stroke-width="0.8" stroke-linejoin="miter" stroke-miterlimit="2" fill="#6bff25" stroke="#6bff25"/> </g> + </g> + <image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAIoUlEQVR4Xu3dzXYTRxCG4TEn1wXsSe4I/Ad3BOxJ7ioblBHYHCWxpJnq6q6vul7W0z3VX9VzxMiyfLPwjwRI4GwCN2RDAiRwPgGAMB0kcCEBgDAeJAAQZoAEbAnwCmLLjVVFEgBIkUZzTFsCALHlxqoiCQCkSKM5pi0BgNhyY1WRBABSpNEc05YAQGy5sapIAgAp0miOaUsAILbcWFUkAYAUaTTHtCUAEFturCqSAECKNJpj2hIAiC03VhVJACBFGs0xbQkAxJYbq4okAJAijeaYtgQAYsuNVUUSAEiRRnNMWwIAseXGqiIJAKRIozmmLQGA2HJjVZEEAFKk0RzTlgBAbLmxqkgCACnSaI5pSwAgttxYVSQBgBRpNMe0JQAQW26sKpIAQIo02uuY94flN6+9MuwDkAxdEqrx3bflIFRO91IA0j3iuW4AkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cxjkBgDgHynZzJQCQufrJaZwTAIhzoGw3VwIAmaufnMY5AYA4B8p2cyUAkLn6yWmcEwCIc6BsN1cCAJmrn5zGOQGAOAfKdnMlAJC5+slpnBMAiHOgbDdXAgCZq5+cJmECv/+53B8Oy51C6fxOukIXqOFXAko4jkUBhOGUSUANB0BkRoNCFHEAhLmUSEAVB0AkxqN2Eco4AFJ7NsNPr44DIOEjUreADDgAUnc+Q0+eBQdAQsek5s0z4QBIzRkNO3U2HAAJG5V6N86IAyD15jTkxFlxACRkXGrdNDMOgNSa1eGnHY3j5mb5sH4K+KPnQfmwomea7PUrgQgcX94sn7x/XwUgDLV7AlE4jgcBiHs72dAzgUgcAPHsJHu5JxCNAyDuLWVDrwQUcMgAWcM4vlPw9/GhyCtg9smbgAoOCSBHHOtbae9/vEe8vq0GkryD7VH5u7+Wh+X7cuux15Y9rs1c6EP6KY7nw1wreMuhuSZnAmo4Ql9BXsIBkpyD7VG1Io4wIJdwgMRj3HLtoYojBMgWHCDJNeAt1SrjGA5kDw6QtIxdjrXqOIYCseAASY5Bt1SZAccwIC04foX/arn9+tr3k5WWxrKmPYEsOIYAccHx3BOQtE9n8A6ZcHQH4ooDJMGj3X77bDi6AumCAyTtUxq0Q0Yc3YB0xQGSoBG33zYrji5AhuAAiX1aB6/MjKMLkDWQD+uHzR6H9YEH92FR771RdhxdgPzYFCR7Z2m662fA0Q1IBJJXy3L3+e3AV67pRtrvQLPg6AoEJH4Dl2mnmXB0BwKSTKPdXutsOIYAAUn74GXYYTSOZdCbM8N+o3D0gzvPJONYzYqjR4IXvzgOJD0ij90THPvyv/rNiiDZF6jy1X98Wx6/L+vPvUb9G/Tfqp7HuQqEZ5Ke8Y/bGxy2rDcBiUCyflvK/fqVQg+2Y7HqNAFw2OdhMxCQ2EOOXAmOtvR3AQFJW9ijV4OjPfHdQEDSHvqIHcDhk7IJCEh8wu+1Czj8kjUDAYlfEzx3Aodnmuv3T7duN/rnJLy7db5j4Gid5v+vbwbCK4l/Uyw7gsOS2vU1LkCCkDysPye5v37E+a8AR78euwEBSb8mXdoZHH1zdwVyLHVt2O36eZ9hPwFfn0nKvpKAoy+O4+7uQEDSv2lPGfPBwwFRdwECkr6d45Wjb76nu3cDApI+TQRHn1zP7doVCEh8mwkO3zy37NYdCEi2tOH6NeC4nlGPK4YAAUlb68DRll/L6mFAQGJrEzhsuXmtGgoEJPvaBo59efW4ejgQkGxrIzi25dT7qhAgILnc1qF/kmIthe8kO9+PMCAgebkp4Oj9mrBv/1AgIPl3s8Cxb3hHXB0OBCQ/2wyOEeO+/x4SQKojAcf+wR21QgZIBJL16fTx6+vlblTYL90HHJHpX7+3FJBqSMBxfUCjr5ADUgUJOKJHf9v9JYHMjgQc24ZT4SpZILMiAYfC2G+vQRrI09ufd4fDwG8v6fjgDo7tg6lypTyQWZCAQ2Xk99WRAkh2JODYN5RKV6cBEoFk/RDfx89vl9uWhoGjJb34tamAZEMCjvgBb60gHZAsSMDROpoa61MCUUcCDo3h9qgiLRBVJODwGEudPVIDUUMCDp3B9qokPRAVJODwGkmtfaYAEo0EHFpD7VnNNEAikKx/euHT8b7rR2Heezbl0l58wcKopH/eZyogEUhGtgscI9OeFMisSMAxHseUryDPMa7PBWM/Bdyxf+DoGO6Vraf7L9bpeWdAwp+9jsMx9SvIDK8k4IjFUQJI1mcScMTjKAMkGxJwaOAoBSQLEnDo4CgH5AnJ/fqDvdAvizs3AuDQwlESiCoScOjhKAtEDQk4NHGUBqKCBBy6OMoDiUYCDm0cAHnqz/oT9+EP7uDQxwGQkx6NRAKOHDgA8p8+jUACjjw4APJCr3oiAUcuHAA5068eSMCRDwdALvTMEwk4cuIAyJW+eSABR14cANnQuxYk4NgQsPglU/9GoVf2FiTg8Eo/dh+AbMx/DxJwbAw1wWUA2dGkLUjAsSPQBJcCZGeTLiEBx84wE1wOEEOTXkICDkOQCZYAxNikUyTgMIaYYBlAGpp0RLIuP3x5szw0bMNS4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotPACDxPaAC4QQAItwcSotP4B/zpSHgk1qeLgAAAABJRU5ErkJggg==" id="icon" x="582" y="-73" width="97.20533333333334" height="97.20533333333334" transform="translate(0 283.34614) translate(581.1707717249999 18.536) scale(1.65) translate(-582.58775 50.4)"/> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/ping/frontend/svelte.config.js b/ping/frontend/svelte.config.js new file mode 100644 index 0000000..1295460 --- /dev/null +++ b/ping/frontend/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/ping/frontend/tsconfig.json b/ping/frontend/tsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/ping/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/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 +} diff --git a/ping/frontend/vite.config.ts b/ping/frontend/vite.config.ts new file mode 100644 index 0000000..ef26a3a --- /dev/null +++ b/ping/frontend/vite.config.ts @@ -0,0 +1,30 @@ +import tailwindcss from '@tailwindcss/vite'; +import devtoolsJson from 'vite-plugin-devtools-json'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + // @ts-expect-error process is loaded + const env = loadEnv(mode, process.cwd()); + + console.log("Found API URL:", env.VITE_BACKEND_URL); + + const API_URL = (env.VITE_BACKEND_URL ?? "http://127.0.1:8080"); + + return { + plugins: [tailwindcss(), sveltekit(), devtoolsJson()], + optimizeDeps: { + exclude: ['chart.js'] + }, + server: { + proxy: { + '/api': API_URL + }, + allowedHosts: [ + 'patapimvest.esteban-charvin.dev', + 'localhost', + '127.0.0.1' + ], + } + } +}); diff --git a/ping/ping/.env b/ping/ping/.env new file mode 100644 index 0000000..4808f47 --- /dev/null +++ b/ping/ping/.env @@ -0,0 +1,3 @@ +PROJECT_DEFAULT_PATH = ping +LOG_FILE=ping.log +ERROR_LOG_FILE=error.log diff --git a/ping/ping/pom.xml b/ping/ping/pom.xml new file mode 100644 index 0000000..cf9c1e3 --- /dev/null +++ b/ping/ping/pom.xml @@ -0,0 +1,307 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>fr.epita.assistants</groupId> + <artifactId>ping</artifactId> + <version>1.0</version> + + <properties> + <compiler-plugin.version>3.13.0</compiler-plugin.version> + + <maven.compiler.source>21</maven.compiler.source> + <maven.compiler.target>21</maven.compiler.target> + <maven.compiler.release>21</maven.compiler.release> + + <versions.maven-compiler-plugin>3.13.0</versions.maven-compiler-plugin> + <versions.maven-surefire-plugin>3.2.5</versions.maven-surefire-plugin> + <versions.maven-jar-plugin>3.4.1</versions.maven-jar-plugin> + <versions.maven-install-plugin>3.1.2</versions.maven-install-plugin> + + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + + <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> + <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> + <quarkus.platform.version>3.17.5</quarkus.platform.version> + + <skipITs>true</skipITs> + <surefire-plugin.version>3.2.5</surefire-plugin.version> + + <versions.lucene>9.2.0</versions.lucene> + <versions.lombok>1.18.30</versions.lombok> + <versions.jgit>6.1.0.202203080745-r</versions.jgit> + <versions.common.compress>1.21</versions.common.compress> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>${quarkus.platform.group-id}</groupId> + <artifactId>${quarkus.platform.artifact-id}</artifactId> + <version>${quarkus.platform.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-resteasy-jsonb</artifactId> + </dependency> + + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-arc</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit5</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.maven.surefire</groupId> + <artifactId>surefire-junit-platform</artifactId> + <version>${versions.maven-surefire-plugin}</version> + </dependency> + + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.eclipse.jgit</groupId> + <artifactId>org.eclipse.jgit</artifactId> + <version>${versions.jgit}</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.16.0</version> + </dependency> + + <dependency> + <groupId>org.apache.lucene</groupId> + <artifactId>lucene-core</artifactId> + <version>${versions.lucene}</version> + </dependency> + + <dependency> + <groupId>org.apache.lucene</groupId> + <artifactId>lucene-queryparser</artifactId> + <version>${versions.lucene}</version> + </dependency> + + <dependency> + <groupId>javax.validation</groupId> + <artifactId>validation-api</artifactId> + <version>2.0.1.Final</version> + </dependency> + + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <version>${versions.lombok}</version> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.24.2</version> + </dependency> + + <dependency> + <groupId>com.tngtech.archunit</groupId> + <artifactId>archunit-junit5</artifactId> + <version>1.3.0</version> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.6</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-yaml</artifactId> + <version>2.16.0</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>2.16.0</version> + </dependency> + + <dependency> + <groupId>org.yaml</groupId> + <artifactId>snakeyaml</artifactId> + <version>2.0</version> + </dependency> + + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>2.5</version> + </dependency> + + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-hibernate-orm-panache</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jdbc-postgresql</artifactId> + </dependency> + + + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-smallrye-openapi</artifactId> + </dependency> + + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-resteasy-jackson</artifactId> + </dependency> + + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-smallrye-jwt-build</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-smallrye-jwt</artifactId> + </dependency> + + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>4.4.0</version> + <scope>test</scope> + </dependency> + + + <!-- TEST DEPENDENCIES--> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>5.9.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <version>1.9.1</version> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit5-mockito</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-test-security-jwt</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-clean-plugin</artifactId> + <version>2.5</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>2.6</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>3.3.0</version> + </plugin> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <archive> + <manifest> + <mainClass>fully.qualified.MainClass</mainClass> + </manifest> + </archive> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + </plugin> + <plugin> + <groupId>${quarkus.platform.group-id}</groupId> + <artifactId>quarkus-maven-plugin</artifactId> + <version>${quarkus.platform.version}</version> + <extensions>true</extensions> + <executions> + <execution> + <goals> + <goal>build</goal> + <goal>generate-code</goal> + <goal>generate-code-tests</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>${compiler-plugin.version}</version> + <configuration> + <compilerArgs> + <arg>-parameters</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>${surefire-plugin.version}</version> + <configuration> + <systemPropertyVariables> + <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> + <maven.home>${maven.home}</maven.home> + </systemPropertyVariables> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>3.0.0</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + <version>3.1.0</version> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <skipITs>false</skipITs> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + </profile> + </profiles> +</project> diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java new file mode 100644 index 0000000..50f665c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java @@ -0,0 +1,20 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.lang.reflect.Array; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ExecFeatureRequest +{ + public String feature; + public String command; + public List<String> params; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java new file mode 100644 index 0000000..c88dc2c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java @@ -0,0 +1,16 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class LoginRequest +{ + public String login; + public String password; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java new file mode 100644 index 0000000..260d574 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class MoveRequest +{ + public String src; + public String dst; + +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java new file mode 100644 index 0000000..8033ecb --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class NewProjectRequest +{ + public String name; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java new file mode 100644 index 0000000..f7a3c6b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class NewTransactionRequest { + + public Float amount; + + public String currency; + + public String label = null; + + public String receiverLabel = null; + + public String receiverIban; + + public LocalDateTime operationDate; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java new file mode 100644 index 0000000..9023b91 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class NewUserRequest +{ + public String login; + public String password; + public Boolean isAdmin; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java new file mode 100644 index 0000000..a5fcb36 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class PathRequest +{ + public String relativePath = ""; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java new file mode 100644 index 0000000..94f872d --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java @@ -0,0 +1,16 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UpdateProjectRequest +{ + public String name; + public String newOwnerId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java new file mode 100644 index 0000000..58b90d0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java @@ -0,0 +1,8 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class UpdateTransactionRequest extends NewTransactionRequest {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java new file mode 100644 index 0000000..5bb4176 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UpdateUserRequest +{ + public String password; + public String displayName; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java new file mode 100644 index 0000000..9f30b84 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserProjectRequest +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String userId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java new file mode 100644 index 0000000..129f63c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java @@ -0,0 +1,9 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +@Data +public class EcoScoreResponse { + + public final Float score; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java new file mode 100644 index 0000000..e3443ed --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class FSEntryResponse +{ + public String name; + public String path; + public Boolean isDirectory; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java new file mode 100644 index 0000000..b59ede0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class LoginResponse +{ + public String token; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java new file mode 100644 index 0000000..100b996 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java @@ -0,0 +1,22 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ProjectResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String id; + public String name; + public List<UserSummaryResponse> members; + public UserSummaryResponse owner; +}
\ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java new file mode 100644 index 0000000..51d07f6 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java @@ -0,0 +1,30 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class TransactionResponse { + + public final UUID id; + + public final String label; + + public final Float amount; + + public final String currency; + + public final String receiverLabel; + + public final String receiverIban; + + public final LocalDateTime operationDate; + + public final Float eco_score; + + public final LocalDateTime creationDate; + + public final UUID createrId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java new file mode 100644 index 0000000..03efb1e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java @@ -0,0 +1,11 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +import java.util.List; + +@Data +public class TransactionsResponse { + + public final List<TransactionResponse> transactions; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java new file mode 100644 index 0000000..04c2a57 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.UUID; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public UUID id; + public String login; + public String displayName; + public Boolean isAdmin; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java new file mode 100644 index 0000000..228ebc7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +// The only purpose of this class is to be used by +// the ProjectResponse class +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserSummaryResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String id; + public String displayName; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java new file mode 100644 index 0000000..5db6e3b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.converters; + + +import java.util.List; + +// Maxence regarde j'ai écrit Teto :D +public interface IConverter<TFrom, TTo> { + + TTo convert(TFrom object); + + default List<TTo> convert(List<TFrom> objects) { + return objects + .stream() + .map(this::convert) + .toList(); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java new file mode 100644 index 0000000..99372b3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.NewTransactionRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.TransactionCreationEntity; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Currency; + +@ApplicationScoped +public class TransactionCreationEntityConverter implements IConverter<NewTransactionRequest, TransactionCreationEntity> { + + public TransactionCreationEntity convert(final NewTransactionRequest request) throws IllegalArgumentException { + return new TransactionCreationEntity( + request.getAmount(), + Currency.getInstance(request.getCurrency()), + request.getLabel(), + request.getReceiverLabel(), + request.getReceiverIban(), + request.getOperationDate() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java new file mode 100644 index 0000000..1448716 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.UpdateTransactionRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.TransactionUpdateEntity; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Currency; + +@ApplicationScoped +public class TransactionUpdateEntityConverter implements IConverter<UpdateTransactionRequest, TransactionUpdateEntity> { + + public TransactionUpdateEntity convert(final UpdateTransactionRequest request) throws IllegalArgumentException { + return new TransactionUpdateEntity( + request.getAmount(), + Currency.getInstance(request.getCurrency()), + request.getLabel(), + request.getReceiverLabel(), + request.getReceiverIban(), + request.getOperationDate() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java new file mode 100644 index 0000000..b452d9e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.NewUserRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.UserCreationEntity; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserCreationEntityConverter implements IConverter<NewUserRequest, UserCreationEntity> { + + public UserCreationEntity convert(final NewUserRequest request) { + return new UserCreationEntity( + request.getLogin(), + request.getPassword(), + request.getIsAdmin() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java new file mode 100644 index 0000000..3803a97 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.UpdateUserRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.UserUpdateEntity; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserUpdateEntityConverter implements IConverter<UpdateUserRequest, UserUpdateEntity> { + + public UserUpdateEntity convert(final UpdateUserRequest request) { + return new UserUpdateEntity( + request.getPassword(), + request.getDisplayName(), + request.getAvatar() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java new file mode 100644 index 0000000..b45c764 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.ProjectModel; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class ProjectResponseConverter implements IConverter<ProjectModel, ProjectResponse> { + + @Inject + UserSummaryResponseConverter userSummaryResponseConverter; + + public ProjectResponse convert(ProjectModel p) { + var owner = p.getOwner(); + + return new ProjectResponse( + p.getId().toString(), + p.getName(), + p.getMembers().stream().map(userSummaryResponseConverter::convert).toList(), + userSummaryResponseConverter.convert(owner) + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java new file mode 100644 index 0000000..daed47b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.TransactionResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.TransactionModel; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class TransactionResponseConverter implements IConverter<TransactionModel, TransactionResponse> { + + public TransactionResponse convert(TransactionModel transactionModel) { + return new TransactionResponse( + transactionModel.getId(), + transactionModel.getLabel(), + transactionModel.getAmount(), + transactionModel.getCurrency().getCurrencyCode(), + transactionModel.getReceiverLabel(), + transactionModel.getReceiverIban(), + transactionModel.getOperationDate(), + transactionModel.getEcoScore(), + transactionModel.getCreationTimestamp(), + transactionModel.getCreationUser().getId() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java new file mode 100644 index 0000000..ef251d0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.UserResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.UserModel; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserResponseConverter implements IConverter<UserModel, UserResponse> { + + public UserResponse convert(final UserModel userModel) { + return new UserResponse( + userModel.getId(), + userModel.getLogin(), + userModel.getDisplayName(), + userModel.getIsAdmin(), + userModel.getAvatar() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java new file mode 100644 index 0000000..aa4d1a4 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java @@ -0,0 +1,20 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.UserSummaryResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.UserModel; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserSummaryResponseConverter implements IConverter<UserModel, UserSummaryResponse> { + + public UserSummaryResponse convert(UserModel um) { + return new UserSummaryResponse( + um.getId().toString(), + um.getDisplayName(), + um.getAvatar() + ); + } + +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java new file mode 100644 index 0000000..f969cf7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java @@ -0,0 +1,43 @@ +package fr.epita.assistants.ping.data.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Table(name = "projects") +public class ProjectModel { + // TODO DONE + @Id + @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + @ManyToOne + @With + @JoinColumn(name = "owner_id", referencedColumnName = "id") + UserModel owner; + @With + String name; + @With + String path; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name="project_members", + joinColumns = @JoinColumn(name = "project_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + List<UserModel> members; + + public boolean hasMember(final UserModel user) { + return owner.getId().compareTo(user.getId()) == 0 || members + .stream() + .anyMatch(userModel -> userModel.getId().compareTo(user.getId()) == 0); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java new file mode 100644 index 0000000..08289f7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java @@ -0,0 +1,61 @@ +package fr.epita.assistants.ping.data.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Currency; +import java.util.UUID; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "transactions") +public class TransactionModel extends PanacheEntityBase { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + + @With + String label; + + @With + Float amount; + + @With + Currency currency; + + @With + @Nullable + @Column(name = "receiver_label") + String receiverLabel = null; + + @With + @Column(name = "receiver_iban") + String receiverIban; + + @With + @Column(name = "operation_date") + LocalDateTime operationDate; + + @With + @Column(name = "eco_score") + Float ecoScore; + + @With + @ManyToOne + @JoinColumn(name = "creation_user") + UserModel creationUser; + + @Column(name = "creation_timestamp") + LocalDateTime creationTimestamp = LocalDateTime.now(); + + @Nullable + @Column(name = "update_timestamp") + LocalDateTime updateTimestamp = null; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java new file mode 100644 index 0000000..07a54c8 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java @@ -0,0 +1,42 @@ +package fr.epita.assistants.ping.data.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Table(name = "users") +public class UserModel { + // TODO DONE + @Id @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + + @Column(name = "display_name") @With + String displayName; + + @Column(name = "is_admin") @With + Boolean isAdmin; + + @With + String avatar; + + @With + String login; + + @With + String password; + + @JsonIgnore + @ToString.Exclude + @ManyToMany(mappedBy = "members", fetch = FetchType.EAGER) + List<ProjectModel> projectMemberships; +}
\ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java new file mode 100644 index 0000000..31346f0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java @@ -0,0 +1,113 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class ProjectRepository implements PanacheRepository<ProjectModel> { + @ConfigProperty(name = "PROJECT_DEFAULT_PATH") + String defaultPath; + + @Inject + Logger logger; + + // MODIFIENT LA DB + @Transactional + public ProjectModel createProject(UserModel owner, String name) { + ProjectModel project = new ProjectModel().withName(name).withOwner(owner); + List<UserModel> members = new ArrayList<>(); + members.add(owner); + project.setMembers(members); + persist(project); + defaultPath = defaultPath.charAt(defaultPath.length() - 1) == '/' ? defaultPath : defaultPath + "/"; + project.setPath(defaultPath + project.getId().toString()); + persist(project); + logger.info("Projet " + project.getId() + " créé."); + return project; + } + + // UPDATE + // On admet que le projet existe -> utilisez getProject ! + @Transactional + public void updateName(UUID id, String name) { + update("name = ?1 where id = ?2", id, name); + logger.info("Projet " + id + " mis à jour (nouveau nom : " + name + ")."); + } + + @Transactional + public void updateOwner(UUID id, UserModel newOwner) { + update("owner = ?1 where id = ?2", id, newOwner); + logger.info("Projet " + id + " mis à jour (nouveau propriétaire : " + newOwner + ")."); + } + + @Transactional + public void addMember(UUID id, UserModel member) { + ProjectModel p = find("where id = ?1", id).firstResult(); + p.getMembers().add(member); + persist(p); + logger.info("Nouveau membre (utilisateur " + member.getDisplayName() + ") " + " ajouté au projet " + id + "."); + } + + @Transactional + public void removeMember(UUID id, UserModel member) { + ProjectModel p = find("where id = ?1", id).firstResult(); + p.getMembers().remove(member); + persist(p); + logger.info("Membre (utilisateur " + member + ") " + " retiré du projet " + id + "."); + } + + // DELETE + // Admis : + // - Le projet existe -> Utilisez getProject + public void deleteProject(UUID id) { + Optional<ProjectModel> optPm = getProject(id); + if(optPm.isEmpty()) return; + ProjectModel pm = optPm.get(); + // Les membres connaissent plus ce projet + pm.getMembers().forEach(m -> m.getProjectMemberships().remove(pm)); + // Le projet connait plus ses membres + pm.getMembers().clear(); + persist(pm); + // Le projet part dans le ventre de bombardiro crocodiro + delete("where id = ?1",id); + logger.info("Projet " + id + "supprimé."); + } + + // NE MODIFIENT PAS LA DB + @Transactional + public List<ProjectModel> getAllProjects() { + List<ProjectModel> res = findAll().stream().toList(); + logger.info("Récupération de tous les projets depuis la base de données `projects`."); + for (ProjectModel elem : res) + { + logger.info("Found project: " + elem.toString()); + } + return res; + } + + @Transactional + public List<ProjectModel> getAllOwnedProjects(UserModel owner) { + var res = find("where owner.id = ?1", owner.getId()).stream().toList(); + logger.info("Récupération de tous les projets de " + owner.getDisplayName() + " depuis la base de données `projects`."); + return res; + } + + @Transactional + public Optional<ProjectModel> getProject(UUID id) { + var res = find("where id = ?1", id).firstResultOptional(); + logger.info("Récupération du projet " + id + " depuis la base de données `projects`."); + return res; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java new file mode 100644 index 0000000..cc0193f --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java @@ -0,0 +1,32 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.TransactionModel; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class TransactionRepository implements PanacheRepository<TransactionModel> { + + public TransactionModel findById(final UUID identifier) { + return TransactionModel.findById(identifier); + } + + public Optional<TransactionModel> findByIdOptional(final UUID identifier) { + return find("where id = ?1", identifier).firstResultOptional(); + } + + public boolean deleteById(final UUID identifier) { + final Optional<TransactionModel> transaction = findByIdOptional(identifier); + + if (transaction.isEmpty()) { + return false; + } + + transaction.get().delete(); + + return true; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java new file mode 100644 index 0000000..599a8fc --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java @@ -0,0 +1,120 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@ApplicationScoped +public class UserRepository implements PanacheRepository<UserModel> { + @Inject + Logger logger; + // @Transactional être la voie + + // MODIFIENT LA DB + @Transactional + public UserModel createUser(String name, Boolean admin, String avatar, String login, String password) { + UserModel user = new UserModel() + .withAvatar(avatar) + .withLogin(login) + .withPassword(password) + .withIsAdmin(admin) + .withDisplayName(name); + + persist(user); + logger.info("Utilisateur " + user.getId() + " créé."); + return user; + } + + // Projets dans lesquels l'utilisateur est + // id = user + // ProjectModel = le projet + @Transactional + public void joinProject(UUID id, ProjectModel project) { + logger.info("Adding user (" + id + ") to project" + project.getName()); + UserModel user = find("id = ?1", id).firstResult(); + if (user != null) { + user.getProjectMemberships().add(project); + persist(user); + } + } + + @Transactional + public void leaveProject(UUID id, ProjectModel project) { + logger.info("Removing user (" + id + ") from project" + project.getName()); + UserModel user = find("id = ?1", id).firstResult(); + if (user != null) { + user.getProjectMemberships().remove(project); + persist(user); + } + } + + // UPDATE + // On admet que le user existe -> Utilisez getUser ! + + @Transactional + public void updatePassword(UUID id, String password) { + update("password = ?1 where id = ?2", password, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau mot de passe : " + password + ")."); + } + @Transactional + public void updateDisplayName(UUID id, String name) { + update("displayName = ?1 where id = ?2", name, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau nom : " + name + ")."); + } + @Transactional + public void updateAvatar(UUID id, String avatar) { + update("avatar = ?1 where id = ?2", avatar, id); + logger.info("Utilisateur " + id + " mis à jour (nouvel avatar : " + avatar + ")."); + } + @Transactional + public void updateLogin(UUID id, String login) { + update("login = ?1 where id = ?2", login, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau login : " + login + ")."); + } + @Transactional + public void updateIsAdmin(UUID id, Boolean admin) { + update("isAdmin = ?1 where id = ?2", admin, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau statut administrateur : " + admin + ")."); + } + + // DELETE + // Admis : + // - Le user existe -> Utilisez getUser + // - Le user n'est propriétaire d'aucun projet -> Utilisez ProjectRepository + @Transactional + public void deleteUser(UUID id) { + delete("id = ?1", id); + logger.info("Utilisateur " + id + "supprimé."); + } + + // NE MODIFIENT PAS LA DB + + @Transactional + public List<UserModel> getAllUsers() { + var res = findAll().stream().toList(); + logger.info("Récupération de tous les utilisateurs depuis la base de données `users`."); + return res; + } + + // Getters renvoie Optional<UserModel> si jamais le user n'existe pas + @Transactional + public Optional<UserModel> getUser(UUID id) { + var res = find("where id = ?1", id).firstResultOptional(); + logger.info("Récupération de l'utilisateur (" + id + " " + ((res.isEmpty())? "[NOT FOUND]" : res.get().getDisplayName()) + ") depuis la base de données `users`."); + return res; + } + public Optional<UserModel> getUser(String login) { + var res = find("where login = ?1", login).firstResultOptional(); + logger.info("Récupération de l'utilisateur (" + login + ") depuis la base de données `users`."); + return res; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java new file mode 100644 index 0000000..2f69c03 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.domain.entity; + +import java.time.LocalDateTime; +import java.util.Currency; + +public record TransactionCreationEntity( + + Float amount, + + Currency currency, + + String label, + + String receiver_label, + + String receiver_iban, + + LocalDateTime operation_date +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java new file mode 100644 index 0000000..a6c0c8d --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.domain.entity; + +import java.time.LocalDateTime; +import java.util.Currency; + +public record TransactionUpdateEntity( + + Float amount, + + Currency currency, + + String label, + + String receiverLabel, + + String receiverIban, + + LocalDateTime operationDate +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java new file mode 100644 index 0000000..7b87bc3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.domain.entity; + +public record UserCreationEntity( + String login, + String password, + Boolean isAdmin +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java new file mode 100644 index 0000000..ae237e1 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.domain.entity; + +public record UserUpdateEntity ( + String password, + String displayName, + String avatar +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java new file mode 100644 index 0000000..cf401d7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java @@ -0,0 +1,14 @@ +package fr.epita.assistants.ping.domain.entity.feature; + +import fr.epita.assistants.ping.errors.InvalidFeatureException; + +public enum Feature { + GIT; + + public static Feature fromString(final String string) { + return switch (string) { + case "git" -> GIT; + default -> throw new InvalidFeatureException("Invalid feature parameter: " + string); + }; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java new file mode 100644 index 0000000..c993e3b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java @@ -0,0 +1,18 @@ +package fr.epita.assistants.ping.domain.entity.feature; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class FeatureExecution { + public final Feature feature; + public final String command; + public final List<String> args; + + public FeatureExecution(final String feature, final String command, final List<String> args) { + this(Feature.fromString(feature), command, args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java new file mode 100644 index 0000000..f01ed3e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java @@ -0,0 +1,31 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.Feature; +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import io.quarkus.runtime.Startup; +import jakarta.enterprise.context.ApplicationScoped; + +import java.io.File; +import java.security.InvalidParameterException; +import java.util.HashMap; +import java.util.Map; + +@ApplicationScoped +public class FeatureDispatcher { + private final Map<Feature, FeatureExecutor> features = new HashMap<>(); + + @Startup + public void init() { + features.put(Feature.GIT, new GitFeatureExecutor()); + } + + public void execute(final File projectRoot, final FeatureExecution executionRequest) { + if (!features.containsKey(executionRequest.getFeature())) { + throw new InvalidParameterException( + "Feature " + executionRequest.getFeature() + " currently does not have any implementation" + ); + } + + features.get(executionRequest.getFeature()).execute(projectRoot, executionRequest); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java new file mode 100644 index 0000000..e67b075 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; + +import java.io.File; + +public interface FeatureExecutor { + /** + * unique tool name, e.g. "git" + */ + String name(); + + /** + * execute sub-command on the project root directory + */ + void execute(File projectRoot, FeatureExecution request); +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java new file mode 100644 index 0000000..098536a --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java @@ -0,0 +1,134 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import fr.epita.assistants.ping.errors.InvalidCommandException; +import org.eclipse.jgit.api.AddCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class GitFeatureExecutor implements FeatureExecutor { + + private List<String> validateAddArgs(final List<String> args) { + return args; + } + + private List<Path> gitAddRecursive(final File contextFolder, final Path rootFolder, final String pattern) { + final Pattern asPattern = Pattern.compile(pattern); + final File[] files = contextFolder.listFiles(); + final List<Path> result = new ArrayList<>(); + + assert files != null; + + for (final File child : files) { + final Path relativePath = rootFolder.relativize(Path.of(child.getPath())); + + if (asPattern.matcher(relativePath.toString()).matches()) { + result.add(relativePath); + } + + if (child.isDirectory()) { + result.addAll(gitAddRecursive(child, rootFolder, pattern)); + } + } + + return result; + } + + private Repository loadRepository(final File contextRoot) { + try { + return new FileRepositoryBuilder() + .setGitDir(contextRoot.toPath().resolve(".git").toFile()) + .build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void gitInit(final File contextRoot, final List<String> _args) { + try { + Git.init().setDirectory(contextRoot).call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + private void gitAdd(final File contextRoot, final List<String> args) { + final List<String> actualArgs = validateAddArgs(args); + + if (actualArgs.isEmpty()) { + throw new InvalidParameterException("Expected at least 1 argument but got none"); + } + + final Repository repository = loadRepository(contextRoot); + final Git git = new Git(repository); + AddCommand addCommand = git.add(); + final Path contextRootAsPath = contextRoot.toPath(); + final List<Path> filesToAdd = new ArrayList<>(); + + for (final String arg : actualArgs) { + if (arg == null) { + throw new InvalidParameterException("Git add patterns cannot include a null value"); + } + + final List<Path> correspondances = gitAddRecursive(contextRoot, contextRootAsPath, arg); + + if (correspondances.isEmpty()) { + throw new InvalidParameterException("Git add pattern " + arg + " does not match any file"); + } + + filesToAdd.addAll(correspondances); + } + + filesToAdd.forEach(path -> addCommand.addFilepattern(path.toString())); + + try { + addCommand.call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + private void gitCommit(final File contextRoot, final List<String> args) { + if (args.size() != 1) { + throw new InvalidParameterException("Expected 1 argument but got " + args.size()); + } + + final Repository repository = loadRepository(contextRoot); + final Git git = new Git(repository); + final String commitMessage = args.getFirst(); + + try { + git.commit().setMessage(commitMessage).call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + @Override + public String name() { + return "Git"; + } + + @Override + public void execute(File projectRoot, FeatureExecution request) { + final String command = request.getCommand(); + final List<String> args = request.getArgs(); + + switch (command) { + case "init" -> gitInit(projectRoot, args); + case "add" -> gitAdd(projectRoot, args); + case "commit" -> gitCommit(projectRoot, args); + default -> throw new InvalidCommandException(this, command); + } + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java new file mode 100644 index 0000000..2ee3959 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java @@ -0,0 +1,497 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.api.responses.FSEntryResponse; +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.UnreacheableCodeException; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.runtime.Startup; +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@ApplicationScoped +public class FileSystemService { + + @ConfigProperty(name = "PROJECT_DEFAULT_PATH") + String defaultPath; + + private Path projectRootPath; + + @Inject + UserService userService; + + @Inject + ProjectService projectService; + + @Inject + Logger logger; + + @Startup + void init() { + projectRootPath = Paths.get(defaultPath); + logger.info("Set project files root path to " + projectRootPath); + } + + private ProjectModel loadContextProject(final UUID contextUserId, final UUID projectId) { + logger.debug("Attempting to load context project " + projectId + " as " + contextUserId); + final UserModel contextUser = userService.loadContextUser(contextUserId); + return projectService.loadContextProject(projectId, contextUser); + } + + private Path resolvePath(final ProjectModel project, @Nullable final String relativePath) { + final Path thisProjectRoot = projectRootPath.resolve(project.getId().toString()); + Path resolvedPath = thisProjectRoot; + + if (relativePath != null && !relativePath.isBlank()) { + resolvedPath = resolvedPath.resolve(relativePath); + } + + resolvedPath = resolvedPath.normalize(); + + if (!resolvedPath.startsWith(thisProjectRoot)) { + ErrorsCode.FORBIDDEN.throwException("Access denied: Path attempts to access outside project directory"); + } + + return resolvedPath; + } + + public Response listFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Listing folder contents for: " + request.getRelativePath()); + + try { + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + logger.info("Listing folder contents for: " + request.getRelativePath()); + + if (!Files.exists(dirPath)) { + logger.error("Folder not found: " + dirPath); + ErrorsCode.NOT_FOUND.throwException("Folder not found"); + } + + if (!Files.isDirectory(dirPath)) { + logger.error("Path is not a directory: " + dirPath); + ErrorsCode.BAD_REQUEST.throwException("Path is not a directory"); + } + + List<FSEntryResponse> entries = Files.list(dirPath) + .map(path -> new FSEntryResponse( + path.getFileName().toString(), + //dirPath.relativize(path).toString(), + projectRootPath.resolve(contextProject.getId().toString()).relativize(path).toString(), + Files.isDirectory(path) + )) + .collect(Collectors.toList()); + + logger.info("Listing folder contents at path: " + request.getRelativePath()); + if (entries.isEmpty()) { + logger.info("Folder is empty"); + } + + logger.info("Found " + entries.size() + " entries in folder: " + request.getRelativePath()); + return Response.ok(entries).build(); + + } catch (IOException e) { + logger.error("Failed to list folder contents: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to list folder contents: " + e.getMessage()); + } + + logger.error("Unreachable code reached in listFolder method"); + throw new UnreacheableCodeException(); + } + + public Response deleteFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Deleting folder: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + Path projectRoot = projectRootPath.resolve(contextProject.getId().toString()); + + if (!Files.exists(dirPath)) { + logger.error("Folder not found: " + dirPath); + ErrorsCode.NOT_FOUND.throwException("Folder not found"); + } + + if (!Files.isDirectory(dirPath)) { + logger.error("Path is not a directory: " + dirPath); + ErrorsCode.BAD_REQUEST.throwException("Path is not a directory"); + } + + logger.info("Removing folder and its contents: " + dirPath); + + if (dirPath.equals(projectRoot)) { + logger.info("Emptying root folder contents without deleting root"); + Files.walk(dirPath) + .sorted((a, b) -> b.compareTo(a)) + .filter(path -> !path.equals(dirPath)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + logger.error("Failed to remove file: " + path + " - " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException( + "Failed to remove file " + path + ": " + e.getMessage()); + } + }); + } else { + Files.walk(dirPath) + .sorted((a, b) -> b.compareTo(a)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + logger.error("Failed to remove file: " + path + " - " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException( + "Failed to remove file " + path + ": " + e.getMessage()); + } + }); + } + + logger.info("Successfully deleted folder: " + dirPath); + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to delete folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to delete folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in deleteFolder method"); + throw new UnreacheableCodeException(); + } + + public Response createFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Creating folder: " + request.getRelativePath()); + + try { + logger.info("Attempting to create folder with path: " + request.getRelativePath()); + if (request.getRelativePath() == null) { + logger.error("Create folder failed: path is null"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null"); + } + if (request.getRelativePath().isEmpty()) { + logger.error("Create folder failed: path is empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be empty"); + } + + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + + if (Files.exists(dirPath)) { + logger.error("Folder already exists: " + dirPath); + ErrorsCode.CONFLICT.throwException("Folder already exists"); + } + + logger.info("Creating new folder at path: " + request.getRelativePath()); + Files.createDirectories(dirPath); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + dirPath.getFileName().toString(), + request.getRelativePath(), + true + )).build(); + + } catch (IOException e) { + logger.error("Failed to create folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to create folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in createFolder method"); + throw new UnreacheableCodeException(); + } + + public Response createFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Creating file: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null || request.getRelativePath().isEmpty()) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + logger.info("Creating new file at path: " + request.getRelativePath()); + if (Files.exists(filePath)) { + logger.error("Create file failed: file already exists at " + filePath); + ErrorsCode.CONFLICT.throwException("File already exists"); + } + + Files.createDirectories(filePath.getParent()); + Files.createFile(filePath); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + filePath.getFileName().toString(), + request.getRelativePath(), + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to create file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to create file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in createFile method"); + throw new UnreacheableCodeException(); + } + + public Response deleteFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Deleting file: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null || request.getRelativePath().isEmpty()) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, request.getRelativePath()); + Path projectRoot = projectRootPath.resolve(contextProject.getId().toString()); + + logger.info("Attempting to delete file: " + request.getRelativePath()); + if (filePath.equals(projectRoot)) { + logger.error("Cannot delete project root directory"); + ErrorsCode.FORBIDDEN.throwException("Cannot delete project root directory"); + } + + if (!Files.exists(filePath)) { + logger.error("Delete file failed: file not found at " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Removing file: " + filePath); + Files.delete(filePath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to delete file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to delete file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in deleteFile method"); + throw new UnreacheableCodeException(); + } + + public Response readFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Reading file: " + request.getRelativePath()); + + try { + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + logger.info("Downloading file from path: " + request.getRelativePath()); + if (!Files.exists(filePath)) { + logger.error("Download failed: file not found at " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Reading file content: " + filePath); + byte[] content = Files.readAllBytes(filePath); + + return Response.ok(content).build(); + + } catch (IOException e) { + logger.error("Failed to read file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to read file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in readFile method"); + throw new UnreacheableCodeException(); + } + + public Response updateFile(final UUID contextUserId, final UUID projectId, final PathRequest request, + final String content) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Trying to update file " + request.getRelativePath() + " from project " + projectId); + + try { + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + if (!Files.exists(filePath)) { + logger.error("File not found: " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Updating file content: " + filePath); + Files.writeString(filePath, content); + + return Response.ok(new FSEntryResponse( + filePath.getFileName().toString(), + request.getRelativePath(), + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to update file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to update file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in updateFile method"); + throw new UnreacheableCodeException(); + } + + public Response moveFile(final UUID contextUserId, final UUID projectId, final MoveRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Moving file from " + request.getSrc() + " to " + request.getDst() + " in project " + projectId); + + try { + if (request.getSrc() == null || request.getSrc().isEmpty() || + request.getDst() == null || request.getDst().isEmpty()) { + logger.error("Invalid path: source or destination is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Source and destination paths cannot be null or empty"); + } + + Path sourcePath = resolvePath(contextProject, request.getSrc()); + Path targetPath = resolvePath(contextProject, request.getDst()); + + logger.info("Moving file from " + request.getSrc() + " to " + request.getDst()); + if (!Files.exists(sourcePath)) { + logger.error("Move file failed: source file not found at " + sourcePath); + ErrorsCode.NOT_FOUND.throwException("Source file not found"); + } + + if (Files.isDirectory(sourcePath)) { + logger.error("Source path is a directory, not a file: " + sourcePath); + ErrorsCode.BAD_REQUEST.throwException("Source path is a directory, not a file"); + } + + if (Files.exists(targetPath)) { + logger.error("Target file already exists: " + targetPath); + ErrorsCode.CONFLICT.throwException("Target file already exists"); + } + + Files.createDirectories(targetPath.getParent()); + Files.move(sourcePath, targetPath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to move file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to move file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in moveFile method"); + throw new UnreacheableCodeException(); + } + + public Response moveFolder(final UUID contextUserId, final UUID projectId, final MoveRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Moving folder from " + request.getSrc() + " to " + request.getDst() + " in project " + projectId); + + try { + if (request.getSrc() == null || request.getSrc().isEmpty() || + request.getDst() == null || request.getDst().isEmpty()) { + logger.error("Invalid path: source or destination is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Source and destination paths cannot be null or empty"); + } + + Path sourcePath = resolvePath(contextProject, request.getSrc()); + Path targetPath = resolvePath(contextProject, request.getDst()); + + logger.info("Moving folder from " + request.getSrc() + " to " + request.getDst()); + if (!Files.exists(sourcePath)) { + logger.error("Source folder not found: " + sourcePath); + ErrorsCode.NOT_FOUND.throwException("Source folder not found"); + } + + if (!Files.isDirectory(sourcePath)) { + logger.error("Source path is not a directory: " + sourcePath); + ErrorsCode.BAD_REQUEST.throwException("Source path is not a directory"); + } + + if (Files.exists(targetPath)) { + logger.error("Target folder already exists: " + targetPath); + ErrorsCode.CONFLICT.throwException("Target folder already exists"); + } + + Files.createDirectories(targetPath.getParent()); + Files.move(sourcePath, targetPath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to move folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to move folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in moveFolder method"); + throw new UnreacheableCodeException(); + } + + public Response uploadFile(final UUID contextUserId, final UUID projectId, String path, InputStream fileContent) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Uploading file to " + projectId + "/" + path); + + try { + logger.info("Uploading file to path: " + path); + if (path == null || path.isEmpty()) { + logger.error("Upload failed: path is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, path); + + Files.createDirectories(filePath.getParent()); + + Files.copy(fileContent, filePath, StandardCopyOption.REPLACE_EXISTING); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + filePath.getFileName().toString(), + path, + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to upload file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to upload file: " + e.getMessage()); + } finally { + logger.info("Closing file content stream"); + try { + fileContent.close(); + } catch (IOException e) { + logger.error("Error closing file content stream: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Error happened while closing file: " + e.getMessage()); + } + } + + logger.error("Unreachable code reached in uploadFile method"); + throw new UnreacheableCodeException(); + } +}
\ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java new file mode 100644 index 0000000..b54ebfd --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java @@ -0,0 +1,451 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.converters.response.ProjectResponseConverter; +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import fr.epita.assistants.ping.domain.executor.FeatureDispatcher; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.InvalidFeatureException; +import fr.epita.assistants.ping.utils.Logger; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidParameterException; +import java.util.*; + +@ApplicationScoped +public class ProjectService { + + @Inject + FeatureDispatcher featureDispatcher; + + @Inject + ProjectRepository projectRepository; + + @Inject + UserRepository userRepository; + + @Inject + UserService userService; + + @Inject + ProjectResponseConverter projectResponseConverter; + + @ConfigProperty(name = "PROJECT_DEFAULT_PATH", defaultValue = "/var/www/projects/") + String PROJECT_DEFAULT_PATH; + + @Inject + Logger logger; + + private boolean isIgnorableString(final String string) { + return string == null || string.isBlank(); + } + + public ProjectModel loadContextProject(final UUID contextProjectId, final UserModel contextUser) { + Optional<ProjectModel> contextProjectQuery = projectRepository.getProject(contextProjectId); + + if (contextProjectQuery.isEmpty()) { + logger.error("Tried to process query with an invalid project id : " + contextProjectId); + ErrorsCode.NOT_FOUND.throwException("Invalid project id"); + } + + final ProjectModel contextProject = contextProjectQuery.get(); + + if (!contextUser.getIsAdmin() && !contextProject.hasMember(contextUser)) { + logger.error( + "Tried to process query implying project " + contextProjectId + " with an user who is not " + + "part of said project: " + contextUser.getId() + ); + ErrorsCode.FORBIDDEN.throwException("User is not allowed to access project " + contextProjectId); + } + + return contextProject; + } + + /** + * List projects of current user + * + * @param onlyOwned show only owned projects (not member ones) + * @return 200 - A list of projects + */ + @Transactional + public List<ProjectResponse> listProjectsOfUser(final UUID contextUuid, Boolean onlyOwned) { + final UserModel contextUser = userService.loadContextUser(contextUuid); + + boolean onlyowned_ = onlyOwned != null ? onlyOwned : false; + logger.info("Asking listProjectsOfUser : onlyOwned? " + onlyowned_); + + final List<ProjectModel> projects = onlyowned_ + ? projectRepository.getAllOwnedProjects(contextUser) + : new ArrayList<>(contextUser.getProjectMemberships()); + + return projectResponseConverter.convert(projects); + } + + /** + * Create a new project with a name + * + * @param projectName + * @return 200 - The created project + * @throws RuntimeException 400 - Project name is invalid + */ + @Transactional + public ProjectResponse createProject(final UUID contextUuid, String projectName) { + logger.info("Asking createProject"); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + + if (projectName == null || projectName.isEmpty()) { + logger.error("Project name null or empty"); + ErrorsCode.BAD_REQUEST.throwException("The project name is invalid (null or empty for example)"); + } + + ProjectModel pm = projectRepository.createProject(contextUser,projectName); + logger.log("New project " + projectName + " created"); + + userRepository.joinProject(contextUuid, pm); + + try { + Files.createDirectories(Path.of(PROJECT_DEFAULT_PATH,pm.getId().toString())); + } catch (IOException e) { + logger.error("Le gros paf de projet ne peut être construit : "+Path.of(PROJECT_DEFAULT_PATH,pm.getId().toString())); + } + + return projectResponseConverter.convert(pm); + } + + /** + * List all projects + * + * @return 200 - list of projects + * @throws 403 - the user is not an admin + */ + @Transactional + public List<ProjectResponse> listAllProjects() { + logger.info("Asking listAllProjects"); + return projectResponseConverter.convert(projectRepository.getAllProjects()); + } + + /** + * Update a project, changing at least name or owner + * + * @param projectId the current project id + * @param newProjectName the nullable new project name + * @param newProjectOwner the nullable new project owner + * @return 200 - the updated project + * @throws RuntimeException 400 - The name and owner are both null + * @throws RuntimeException 404 - Project with ID not found + */ + @Transactional + public ProjectResponse updateProject(final UUID contextUuid, String projectId, String newProjectName, String newProjectOwner) { + logger.info("Asking updateProject"); + final UserModel contextUser = userService.loadContextUser(contextUuid); + + if (newProjectName == null && newProjectOwner == null) { + logger.error("Both the name and the new owner are null"); + ErrorsCode.BAD_REQUEST.throwException("Both the name and the new owner are null"); + } + + Optional<ProjectModel> optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project not found (Bad UUID), while updating project"); + ErrorsCode.NOT_FOUND.throwException("Project not found (Bad UUID), while updating project"); + } + if (optProj.isEmpty()) { + logger.error("Project not found, while updating project"); + ErrorsCode.NOT_FOUND.throwException("Project not found, while updating project"); + } + var proj = optProj.get(); + + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + if (newProjectOwner != null && !newProjectOwner.equals(proj.getOwner().toString())) { + Optional<UserModel> optNewOwner = Optional.empty(); + try { + optNewOwner = userRepository.getUser(UUID.fromString(newProjectOwner)); + } catch (Exception e) { + logger.error("New owner not found (Bad UUID), while updating project"); + ErrorsCode.NOT_FOUND.throwException("New owner not found (Bad UUID), while updating project"); + } + + if(optNewOwner.isEmpty()) + { + logger.error("New owner not found, while updating project"); + ErrorsCode.NOT_FOUND.throwException("New owner not found, while updating project"); + } + + optNewOwner.ifPresent(u -> { + if(proj.getMembers().stream().noneMatch(pm -> u.getId().equals(pm.getId()))) + { + logger.error("The new owner isn't member of project, while updating project"); + ErrorsCode.NOT_FOUND.throwException("The new owner isn't member of project, while updating project"); + } + proj.setOwner(u); + }); + } + if (newProjectName != null) + proj.setName(newProjectName); + + projectRepository.persist(proj); + logger.info("Updating project !"); + return projectResponseConverter.convert(proj); + } + + /** + * Get a project + * + * @param contextUuid + * @param projectId UUID of the project + * @return 200 - The project + * @throws RuntimeException 404 - Project not found + */ + @Transactional + public ProjectResponse getProject(UUID contextUuid, String projectId) { + logger.info("Aking getProject"); + Optional<ProjectModel> optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + var proj = optProj.get(); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && proj.getMembers().stream().noneMatch(m -> m.getId() == contextUser.getId())) { + logger.error("The user is not allowed to access the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not allowed to access the project"); + } + logger.info("Got project !"); + return projectResponseConverter.convert(proj); + } + + /** + * Deletes a projet, if the user is owner or admin + * + * @param contextUuid + * @param projectId the UUID of the project + * @throws RuntimeException 404 - Project not found + */ + @Transactional + public void deleteProject(UUID contextUuid, String projectId) { + logger.info("Asking deleteProject"); + Optional<ProjectModel> optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + var proj = optProj.get(); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + + try { + Files.delete(Path.of(PROJECT_DEFAULT_PATH,proj.getId().toString())); + } catch (IOException e) { + logger.error( + "Le gros paf de projet ne peut être détruit : " + + Path.of(PROJECT_DEFAULT_PATH,proj.getId().toString()) + + ", raison: " + e.getMessage() + ); + ErrorsCode.INTERNAL_ERROR.throwException( + "Le gros paf de projet " + projectId + "ne peut être détruit. Raison : " + + e.getMessage() + ); + } + + projectRepository.deleteProject(proj.getId()); + logger.error("Kaboom deleted project"); + } + + /** + * Add a user as a project member. + * + * @param contextUuid + * @param projectId the UUID of project + * @param userId the UUID of user + * @throws RuntimeException 404 - User or project not found + * @throws RuntimeException 400 - userId is empty or null + * @throws RuntimeException 409 - The user is already a member + */ + @Transactional + public void addMember(UUID contextUuid, String projectId, String userId) { + logger.info("Asking addMember"); + Optional<ProjectModel> optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + ProjectModel proj = optProj.get(); + + if (userId == null || userId.isEmpty()) { + logger.error("The userId is invalid (null or empty for example)"); + ErrorsCode.BAD_REQUEST.throwException("The userId is invalid (null or empty for example)"); + } + + Optional<UserModel> optUser = Optional.empty(); + try { + optUser = userRepository.getUser(UUID.fromString(userId)); + } catch (Exception e) { + logger.error("User " + userId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("User " + userId + " not found (not an UUID)"); + } + + if (optUser.isEmpty()) { + logger.error("User " + userId + " not found"); + ErrorsCode.NOT_FOUND.throwException("User " + userId + " not found"); + } + var user = optUser.get(); + + if (proj.getMembers().stream().map(UserModel::getLogin).anyMatch(login -> login.equals(user.getLogin()))) { + logger.error("The user is already a member of the project"); + ErrorsCode.CONFLICT.throwException("The user is already a member of the project"); + } + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && proj.getMembers().stream().noneMatch(m -> m.getId() == contextUser.getId())) { + logger.error("The user is not allowed to access the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not allowed to access the project"); + } + projectRepository.addMember(proj.getId(),user); + logger.info("Welcome to the crew member !"); + } + + /** + * Execute a feature on the project. + * + * @param contextUuid + * @param projectId project UUID + * @param feature + * @param command + * @param params + * @throws RuntimeException 400 - feature is null or command is null + * @throws RuntimeException 404 - Project not found + */ + public void execute(final UUID contextUuid, final UUID projectId, String feature, String command, List<String> params) { + final UserModel contextUser = userService.loadContextUser(contextUuid); + final ProjectModel contextProject = loadContextProject(projectId, contextUser); + + if (isIgnorableString(feature)) { + logger.error("Attempted command execution with an empty feature"); + ErrorsCode.BAD_REQUEST.throwException("Attempted command execution with an empty feature"); + } + + if (isIgnorableString(command)) { + logger.error("Attempted command execution with an empty command"); + ErrorsCode.BAD_REQUEST.throwException("Attempted command execution with an empty command"); + } + + logger.info( + "Attempting command using feature " + feature + " on project " + projectId + ": " + command + + " with params " + String.join(" ", params) + ); + + try { + File commandSource = Path.of(contextProject.getPath()).toFile(); + FeatureExecution commandInstructions = new FeatureExecution(feature, command, params); + + featureDispatcher.execute(commandSource, commandInstructions); + } catch (InvalidFeatureException e) { + logger.error("Caught an InvalidFeatureException: " + e.getMessage()); + ErrorsCode.NOT_FOUND.throwException("Feature " + feature + " does not lead to any existing feature"); + } catch (InvalidParameterException e) { + logger.error("Caught an InvalidParameterException: " + e.getMessage()); + ErrorsCode.BAD_REQUEST.throwException("Invalid argument: " + e.getMessage()); + } catch (RuntimeException e) { + logger.error("Caught a RuntimeException: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to run command: " + e.getMessage()); + } + } + + @Transactional + public void removeMember(UUID contextUuid, String projectId, String userId) { + logger.info("Asking removeMember"); + Optional<ProjectModel> optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + ProjectModel proj = optProj.get(); + + if (userId == null || userId.isEmpty()) { + logger.error("The userId is invalid (null or empty for example)"); + ErrorsCode.BAD_REQUEST.throwException("The userId is invalid (null or empty for example)"); + } + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + + Optional<UserModel> optUser = Optional.empty(); + try { + optUser = userRepository.getUser(UUID.fromString(userId)); + } catch (Exception e) { + logger.error("The userId is invalid (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("The userId is invalid (not an UUID)"); + } + + if (optUser.isEmpty()) { + logger.error("The userId is invalid"); + ErrorsCode.NOT_FOUND.throwException("The userId is invalid"); + } + var user = optUser.get(); + + if (proj.getMembers().stream().map(UserModel::getLogin).noneMatch(login -> login.equals(user.getLogin()))) { + logger.error("The user is not a member of the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not a member of the project"); + } + + if(proj.getOwner().getId().toString().equals(user.getId().toString())) + { + logger.error("An owner can't remove himself from his project"); + ErrorsCode.FORBIDDEN.throwException("An owner can't remove himself from his project"); + } + + userRepository.leaveProject(user.getId(),proj); + projectRepository.removeMember(proj.getId(),user); + + logger.info("Farewell, bro"); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java new file mode 100644 index 0000000..69192cb --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java @@ -0,0 +1,119 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.data.model.TransactionModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.TransactionRepository; +import fr.epita.assistants.ping.domain.entity.TransactionCreationEntity; +import fr.epita.assistants.ping.domain.entity.TransactionUpdateEntity; +import fr.epita.assistants.ping.errors.ErrorsCode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class TransactionService { + + @Inject + UserService userService; + + @Inject + TransactionRepository transactionRepository; + + private TransactionModel retrieveTransaction(final UUID transactionId) { + final Optional<TransactionModel> transaction = transactionRepository.findByIdOptional(transactionId); + + if (transaction.isEmpty()) { + ErrorsCode.NOT_FOUND.throwException("transaction does not exist"); + } + + return transaction.get(); + } + + private float computeEcoScore(final TransactionModel transaction) { + /** + * On considere qu'une transaction peut consommer aux alentours de 2g + * de CO² enempreinte carbone. + * On considere aussi que la somme de la transaction a aussi un impact + * minime. + * Sources : daf-mag.fr, delubac.com + */ + return 1.0F + (float) (Math.random() * 2) + transaction.getAmount() * 0.000005F; + } + + public Float getAverageEcoScore(final UUID contextUserId) { + return (float) (getAllTransactions(contextUserId) + .stream() + .mapToDouble(transaction -> transaction.getEcoScore().doubleValue()) + .average() + .orElse(0)); + } + + public List<TransactionModel> getAllTransactions(final UUID contextUserId) { + userService.loadContextUser(contextUserId); + return transactionRepository.listAll(); + } + + public TransactionModel getTransaction(final UUID contextUserId, final UUID transactionId) { + userService.loadContextUser(contextUserId); + return retrieveTransaction(transactionId); + } + + @Transactional + public TransactionModel createTransaction(final UUID contextUserId, final TransactionCreationEntity request) { + final UserModel contextUser = userService.loadContextUser(contextUserId); + + final TransactionModel newTransaction = new TransactionModel() + .withLabel(request.label()) + .withAmount(request.amount()) + .withCurrency(request.currency()) + .withReceiverLabel(request.receiver_label()) + .withReceiverIban(request.receiver_iban()) + .withCreationUser(contextUser); + + final float transactionEcoScore = computeEcoScore(newTransaction); + + newTransaction.setEcoScore(transactionEcoScore); + + newTransaction.persist(); + + return newTransaction; + } + + @Transactional + public TransactionModel updateTransaction(final UUID contextUserId, final UUID updatedTransactionId, + final TransactionUpdateEntity request) { + userService.loadContextUser(contextUserId); + final TransactionModel transaction = retrieveTransaction(updatedTransactionId); + final float transactionEcoScore = computeEcoScore(transaction); + + transaction.setAmount(request.amount()); + transaction.setLabel(request.label()); + transaction.setCurrency(request.currency()); + transaction.setReceiverLabel(request.receiverLabel()); + transaction.setReceiverIban(request.receiverIban()); + transaction.setOperationDate(request.operationDate()); + + transaction.setEcoScore(transactionEcoScore); + transaction.setUpdateTimestamp(LocalDateTime.now()); + + transaction.persist(); + + return transaction; + } + + @Transactional + public TransactionModel deleteTransaction(final UUID contextUserId, final UUID deletedTransactionId) { + userService.loadContextUser(contextUserId); + + final TransactionModel transaction = retrieveTransaction(deletedTransactionId); + + transactionRepository.deleteById(deletedTransactionId); + + return transaction; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java new file mode 100644 index 0000000..42be213 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java @@ -0,0 +1,280 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import fr.epita.assistants.ping.domain.entity.UserCreationEntity; +import fr.epita.assistants.ping.domain.entity.UserUpdateEntity; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.utils.Logger; +import fr.epita.assistants.ping.utils.TokenFactory; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.WebApplicationException; +import org.apache.maven.surefire.shared.lang3.StringUtils; + +import java.util.*; + + +@ApplicationScoped +public class UserService { + + private class UserCreationValidator { + + private void validateUserLoginValid(final String login) throws WebApplicationException { + if (login == null) { + logger.error("Attempted to create a new user with an empty login"); + ErrorsCode.BAD_REQUEST.throwException("Username should not be empty or null"); + } + + int totalDots = StringUtils.countMatches(login, '.'); + int totalDashes = StringUtils.countMatches(login, '_'); + + if (totalDots + totalDashes != 1) { + logger.error("Attempted to create a new user with an invalid login: " + login); + ErrorsCode.BAD_REQUEST.throwException("Login must only have at most a dot or a dash"); + } + } + + private void validateUserLoginExistence(final String login) throws WebApplicationException { + if (userRepository.getUser(login).isPresent()) { + logger.error("Attempted to create a new user with an already existing login: " + login); + ErrorsCode.CONFLICT.throwException("User with login " + login + " already exists"); + } + } + + private void validateUserPasswordValid(final String password) throws WebApplicationException { + if (password == null) { + logger.error("Attempted to create a new user with an empty password"); + ErrorsCode.BAD_REQUEST.throwException("Password should not be empty or null"); + } + } + + public void validate(final UserCreationEntity request) throws WebApplicationException { + validateUserLoginValid(request.login()); + validateUserPasswordValid(request.password()); + validateUserLoginExistence(request.login()); + } + } + + UserCreationValidator userCreationValidator = new UserCreationValidator(); + + @Inject + Logger logger; + + @Inject + TokenFactory tokenFactory; + + @Inject + UserRepository userRepository; + + @Inject + ProjectRepository projectRepository; + + private boolean isIgnorableString(final String string) { + return string == null || string.isBlank(); + } + + private String generateDefaultName(final String login) { + final String clean_login = login + .replace('.', ' ') + .replace('_', ' '); + + if (Objects.equals(clean_login, login)) { + return login; + } + + final StringBuilder builder = new StringBuilder(); + + for (String word : clean_login.split(" ")) { + builder.append(StringUtils.capitalize(word)); + builder.append(' '); + } + + final String name = builder.toString(); + + return name.substring(0, name.length() - 1); + } + + public UserModel loadContextUser(final UUID userId) throws WebApplicationException { + final Optional<UserModel> user = userRepository.getUser(userId); + + if (user.isEmpty()) { + logger.error("Usage of JWT carrying an invalid user detected. UserId : " + userId); + + ErrorsCode.UNAUTHORIZED.throwException( + "Session JWT lead to an user who does not exist. You may need to login again." + ); + } + + return user.get(); + } + + public String login(final String username, final String password) throws WebApplicationException { + if (isIgnorableString(username)) { + logger.error("Connection attempted with an empty username"); + ErrorsCode.BAD_REQUEST.throwException("Username field is empty or null"); + } + if (isIgnorableString(password)) { + logger.error("Connection attempted with an empty password"); + ErrorsCode.BAD_REQUEST.throwException("Password field is empty or null"); + } + + logger.debug("Attempting a login as " + username); + + final Optional<UserModel> user_query = userRepository.getUser(username); + + if (user_query.isEmpty()) { + logger.error("Connection attempted with a non-existant user: " + username); + + ErrorsCode.UNAUTHORIZED.throwException("User does not exist"); + } + + final UserModel user = user_query.get(); + + if (!Objects.equals(user.getPassword(), password)) { + logger.error("Failed connection for username " + username); + + ErrorsCode.UNAUTHORIZED.throwException("Password is incorrect"); + } + + logger.debug("Logging " + " succeeded as " + user); + + String token = tokenFactory.generateLoginToken(user); + + logger.info("Created token " + token); + + return token; + } + + public String refreshToken(final UUID contextUserUuid) { + final UserModel contextUser = loadContextUser(contextUserUuid); + + final String token = tokenFactory.generateLoginToken(contextUser); + + logger.info("Refreshed token for user " + contextUserUuid + " with a new one: " + token); + + return token; + } + + public List<UserModel> getAllUsers() { + List<UserModel> users = userRepository.getAllUsers(); + + logger.debug("GetAllUsers query returned the following list : [\n"); + + users.forEach((user) -> logger.debug(user + ",\n")); + + logger.debug("]"); + + return users; + } + + public UserModel getUser(final UUID contextUserId, final UUID searchedUserId) throws WebApplicationException { + logger.debug("Attempting to retrieve user " + searchedUserId + " as " + contextUserId); + + final UserModel contextUser = loadContextUser(contextUserId); + final Optional<UserModel> userSearch = userRepository.getUser(searchedUserId); + + if (userSearch.isEmpty()) { + logger.error("Query to user " + searchedUserId + " leaded to a failing outcome"); + ErrorsCode.NOT_FOUND.throwException("User does not exist"); + } + + final UserModel user = userSearch.get(); + final boolean canAccess = contextUser.getIsAdmin() + || contextUser.getId().compareTo(user.getId()) == 0; + + if (!canAccess) { + logger.error( + "Query about user " + searchedUserId + " failed because user " + contextUserId + + " is not allowed to read their information." + ); + + ErrorsCode.FORBIDDEN.throwException( + "Only admins can read information from different users than the current one" + ); + } + + logger.debug("Returning information about user : " + user); + + return user; + } + + @Transactional + public UserModel createUser(final UUID contextUserId, final UserCreationEntity request) + throws WebApplicationException { + userCreationValidator.validate(request); + + final String defaultName = generateDefaultName(request.login()); + + final UserModel new_user = userRepository.createUser( + defaultName, + request.isAdmin(), + null, + request.login(), + request.password() + ); + + logger.info( + "Successfully created user as " + contextUserId + " with parameters " + + "id : " + new_user.getId() + ", " + + "login: " + request.login() + ", " + + "defaultName: " + defaultName + ", " + + "isAdmin: " + request.isAdmin() + ); + + return new_user; + } + + @Transactional + public UserModel updateUser(final UUID contextUserId, + final UUID subjectUserId, + final UserUpdateEntity updateUserRequest) throws WebApplicationException { + final UserModel safe_user = getUser(contextUserId, subjectUserId); + final UUID safe_id = safe_user.getId(); + + logger.info( + "Updating new user with the following information : " + + "avatar: " + updateUserRequest.avatar() + ", " + + "displayName: " + updateUserRequest.displayName() + ", " + + "is password changed ? " + isIgnorableString(updateUserRequest.avatar()) + ); + + final String newAvatar = isIgnorableString(updateUserRequest.avatar()) + ? null + : updateUserRequest.avatar(); + + userRepository.updateAvatar(safe_id, newAvatar); + safe_user.setAvatar(newAvatar); + + if (!isIgnorableString(updateUserRequest.displayName())) { + userRepository.updateDisplayName(safe_id, updateUserRequest.displayName()); + safe_user.setDisplayName(updateUserRequest.displayName()); + } + + if (!isIgnorableString(updateUserRequest.password())) { + userRepository.updatePassword(safe_id, updateUserRequest.password()); + safe_user.setPassword(updateUserRequest.password()); + } + + return safe_user; + } + + @Transactional + public UserModel deleteUser(final UUID contextUserId, final UUID subjectUserId) throws WebApplicationException { + final UserModel user = getUser(contextUserId, subjectUserId); + + logger.info("Deleting user " + subjectUserId + " as user " + contextUserId); + + if (!projectRepository.getAllOwnedProjects(user).isEmpty()) { + logger.error("User deletion for user " + subjectUserId + " because user has existing projects"); + + ErrorsCode.UNAUTHORIZED.throwException("A user cannot be removed if they still own projects"); + } + + userRepository.deleteUser(user.getId()); + + return user; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java new file mode 100644 index 0000000..cf0f8da --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java @@ -0,0 +1,36 @@ +package fr.epita.assistants.ping.errors; + +import fr.epita.assistants.ping.utils.HttpError; +import fr.epita.assistants.ping.utils.IHttpError; +import jakarta.ws.rs.core.Response.Status; +import lombok.Getter; + + +@Getter +public enum ErrorsCode implements IHttpError { + EXAMPLE_ERROR(Status.BAD_REQUEST, "Example error: %s"), + BAD_REQUEST(Status.BAD_REQUEST, "Bad request: %s"), + UNAUTHORIZED(Status.UNAUTHORIZED, "Unauthorized: %s"), + FORBIDDEN(Status.FORBIDDEN, "Forbidden: %s"), + NOT_FOUND(Status.NOT_FOUND, "Not found: %s"), + CONFLICT(Status.CONFLICT, "Conflict: %s"), + INTERNAL_ERROR(Status.INTERNAL_SERVER_ERROR, "Internal server error: %s"), + NOT_IMPLEMENTED(Status.NOT_IMPLEMENTED, "Not implemented") + ; + + private final HttpError error; + + ErrorsCode(Status status, String message) { + error = new HttpError(status, message); + } + + @Override + public RuntimeException get(Object... args) { + return error.get(args); + } + + @Override + public void throwException(Object... args) { + throw error.get(args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java new file mode 100644 index 0000000..ad5abe7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java @@ -0,0 +1,11 @@ +package fr.epita.assistants.ping.errors; + +import fr.epita.assistants.ping.domain.executor.FeatureExecutor; + +import java.security.InvalidParameterException; + +public class InvalidCommandException extends InvalidParameterException { + public InvalidCommandException(FeatureExecutor feature, String command) { + super("Invalid " + feature.name() + " command: " + command); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java new file mode 100644 index 0000000..37a594c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java @@ -0,0 +1,9 @@ +package fr.epita.assistants.ping.errors; + +import java.security.InvalidParameterException; + +public class InvalidFeatureException extends InvalidParameterException { + public InvalidFeatureException(final String feature) { + super("Invalid feature name: " + feature); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java new file mode 100644 index 0000000..4a6cf6a --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.errors;
+
+import lombok.Getter;
+
+@Getter
+public class PingServiceError extends RuntimeException {
+ private final Integer code;
+ private final String message;
+
+ public PingServiceError(Integer code, String message) {
+ super(message);
+ this.code = code;
+ this.message = message;
+ }
+
+ public String toJSON()
+ {
+ return "{\"error\":"+code+",\"message\":\""+message+"\"}";
+ }
+
+ @Override
+ public String toString() {
+ return toJSON();
+ }
+}
diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java new file mode 100644 index 0000000..eb7c1e8 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.errors; + +public class UnreacheableCodeException extends RuntimeException { + public UnreacheableCodeException() { + super("Unreacheable code reached during execution"); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java new file mode 100644 index 0000000..9b9ffd9 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java @@ -0,0 +1,64 @@ +package fr.epita.assistants.ping.presentation.rest;
+
+import fr.epita.assistants.ping.data.repository.UserRepository;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+
+@Path("/api")
+public class DebugResource {
+
+ @Inject
+ DataSource dataSource;
+ @Inject
+ UserRepository userRepository;
+
+ // c bo l'html dans les commentaires :)
+ /**
+ * <p>
+ * <code>GET</code> /dbstatus <br>
+ * Returns 200 <code>{"status":"OK"}</code> if connected to DB <br>
+ * Otherwise 400 <code>{"status":"KO","reason":"brr brr patapim (raison de l'erreur)"}</code>
+ * </p>
+ */
+ @GET
+ @Path("/dbstatus")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response dbStatus() {
+ try (Connection conn = dataSource.getConnection()) {
+ if (conn.isValid(2)) {
+ return Response.ok("{\"status\":\"OK\"}").build();
+ } else {
+ return Response.status(400).entity("{\"status\":\"KO\",\"reason\":\"Connection not valid\"}").build();
+ }
+ } catch(Exception e) {
+ return Response.status(400).entity("{\"status\":\"KO\",\"reason\":\"" + e.getMessage() + "\"}").build();
+ }
+ }
+
+ @GET
+ @Path("/dbdebuguser")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Transactional
+ public Response dbDebugUser() {
+ try {
+ if(userRepository.getUser("admin").isPresent() && userRepository.getUser("brrbrr").isPresent())
+ return Response.ok("{\"created\":false}").build();
+ var u1 = userRepository.createUser("admin",true,"/img/default-avatar.png","admin","admin");
+ var u2 = userRepository.createUser("brrbrr",false,"/img/default-avatar.png","brrbrr","brrbrr");
+ userRepository.persist(u1);
+ userRepository.persist(u2);
+ return Response.ok("{\"created\":true}").build();
+ } catch (Exception e){
+ return Response.status(409).entity("{\"error\":true}").build();
+ }
+
+ }
+}
diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java new file mode 100644 index 0000000..7dc29dc --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java @@ -0,0 +1,27 @@ +package fr.epita.assistants.ping.presentation.rest; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import static fr.epita.assistants.ping.errors.ErrorsCode.EXAMPLE_ERROR; + +@Path("/api") +public class HelloWorldResource { + + @GET + @Path("/hello") + @Produces(MediaType.TEXT_PLAIN) + public Response helloWorld() { + return Response.ok("Hello World !").build(); + } + + @GET + @Path("/error") + @Produces(MediaType.APPLICATION_JSON) + public Response error() { + EXAMPLE_ERROR.throwException("This is an error"); + // This line will never be reached + return Response.noContent().build(); + } +}
\ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java new file mode 100644 index 0000000..1cde188 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java @@ -0,0 +1,115 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.requests.NewTransactionRequest; +import fr.epita.assistants.ping.api.requests.UpdateTransactionRequest; +import fr.epita.assistants.ping.api.responses.EcoScoreResponse; +import fr.epita.assistants.ping.api.responses.TransactionResponse; +import fr.epita.assistants.ping.api.responses.TransactionsResponse; +import fr.epita.assistants.ping.converters.request.TransactionCreationEntityConverter; +import fr.epita.assistants.ping.converters.request.TransactionUpdateEntityConverter; +import fr.epita.assistants.ping.converters.response.TransactionResponseConverter; +import fr.epita.assistants.ping.domain.service.TransactionService; +import fr.epita.assistants.ping.errors.ErrorsCode; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/transactions") +@Authenticated +public class TransactionResource { + + @Inject + JsonWebToken jwt; + + @Inject + TransactionCreationEntityConverter transactionCreationEntityConverter; + + @Inject + TransactionUpdateEntityConverter transactionUpdateEntityConverter; + + @Inject + TransactionResponseConverter transactionResponseConverter; + + @Inject + TransactionService transactionService; + + @GET + public TransactionsResponse getAllTransactions() { + return new TransactionsResponse( + transactionResponseConverter.convert( + transactionService.getAllTransactions(UUID.fromString(jwt.getSubject())) + ) + ); + } + + @POST + public Response createNewTransaction(final NewTransactionRequest request) { + try { + return Response.status(201).entity(transactionResponseConverter.convert( + transactionService.createTransaction( + UUID.fromString(jwt.getSubject()), + transactionCreationEntityConverter.convert(request) + ) + )).build(); + } catch (IllegalArgumentException e) { + ErrorsCode.BAD_REQUEST.throwException("Currency is invalid"); + } + + return null; + } + + @Path("eco-score") + @GET + public EcoScoreResponse getTransactionsEcoScore() { + return new EcoScoreResponse( + transactionService.getAverageEcoScore( + UUID.fromString(jwt.getSubject()) + ) + ); + } + + @Path("{id}") + @GET + public TransactionResponse getTransaction(@PathParam("id") final UUID id) { + return transactionResponseConverter.convert( + transactionService.getTransaction( + UUID.fromString(jwt.getSubject()), + id + ) + ); + } + + @Path("{id}") + @PUT + public TransactionResponse updateTransaction(@PathParam("id") final UUID id, + final UpdateTransactionRequest request) { + try { + return transactionResponseConverter.convert( + transactionService.updateTransaction( + UUID.fromString(jwt.getSubject()), + id, + transactionUpdateEntityConverter.convert(request) + ) + ); + } catch (IllegalArgumentException e) { + ErrorsCode.BAD_REQUEST.throwException("Currency is invalid"); + } + + return null; + } + + @Path("{id}") + @DELETE + public TransactionResponse deleteTransaction(@PathParam("id") final UUID id) { + return transactionResponseConverter.convert( + transactionService.deleteTransaction( + UUID.fromString(jwt.getSubject()), + id + ) + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java new file mode 100644 index 0000000..1661b96 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.responses.EcoScoreResponse; +import fr.epita.assistants.ping.errors.ErrorsCode; +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api") +public class UtilsResource { + + @Path("/eco-score") + @GET + @Authenticated + public EcoScoreResponse getGlobalEcoScore() { + ErrorsCode.NOT_IMPLEMENTED.throwException(); + return null; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java new file mode 100644 index 0000000..a278b2b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java @@ -0,0 +1,108 @@ +package fr.epita.assistants.ping.presentation.rest.fileSystemEndpoints; + +import fr.epita.assistants.ping.domain.service.FileSystemService; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import io.smallrye.jwt.util.ResourceUtils; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.io.InputStream; +import java.util.UUID; + +@Path("/api/projects") +public class FileResource +{ + @Inject + JsonWebToken jwt; + + @Inject + FileSystemService fileSystemService; + + @Inject + Logger logger; + + // Seuls les membres du projet et les admins y ont access + // Quand on renvoit du octet stream: + @GET + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response getFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path) + { + //return Response.ok("Some stuff that is in the file").build(); + logger.log("Entering endpoint 'getFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | (Query)path: " + path); + return fileSystemService.readFile(UUID.fromString(jwt.getSubject()), id, new PathRequest(path)); + } + + // Seuls les membres du projet et les admins y ont access + @DELETE + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'deleteFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.deleteFile(UUID.fromString(jwt.getSubject()), id, path); + } + + // Seuls les membres du projet et les admins y ont access + @POST + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response createFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'createFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.createFile(UUID.fromString(jwt.getSubject()), id, path); + } + + // Seuls les membres du projet et les admins y ont access + @PUT + @Path("/{projectId}/files/move") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response moveFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + MoveRequest request) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'moveFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | request: " + request.toString()); + return fileSystemService.moveFile(UUID.fromString(jwt.getSubject()), id, request); + } + + // Seuls les membres du projet et les admins y ont access + // Probleme, beaucoup de sources semblent dire que consommer de l'application octet stream + // Necessite de lire/utiliser une variable speciale + // Aucune idee de ce que c'est ou comment ca marche + @POST + @Path("/{projectId}/files/upload") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + public Response uploadFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path, + InputStream content) + { + logger.log("Entering endpoint 'uploadFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path + " | content: " + content.toString()); + return fileSystemService.uploadFile(UUID.fromString(jwt.getSubject()), id, path, content); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java new file mode 100644 index 0000000..ee8e319 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java @@ -0,0 +1,80 @@ +package fr.epita.assistants.ping.presentation.rest.fileSystemEndpoints; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.domain.service.FileSystemService; + +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/projects") +public class FolderResource +{ + @Inject + JsonWebToken jwt; + + @Inject + FileSystemService fileSystemService; + + @Inject + Logger logger; + + // Seuls les membres du projet et les admins y ont access + @GET + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response listFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path) + { + logger.log("Entering endpoint 'listFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | (Query)path: " + path); + return fileSystemService.listFolder(UUID.fromString(jwt.getSubject()), id, new PathRequest(path)); + } + + // Seuls les membres du projet et les admins y ont access + @DELETE + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + logger.log("Entering endpoint 'deleteFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.deleteFolder(UUID.fromString(jwt.getSubject()), id, path); + } + + @POST + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response createFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + logger.log("Entering endpoint 'createFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.createFolder(UUID.fromString(jwt.getSubject()), id, path); + } + + @PUT + @Path("/{projectId}/folders/move") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response moveFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + MoveRequest request) + { + logger.log("Entering endpoint 'moveFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | request: " + request.toString()); + return fileSystemService.moveFolder(UUID.fromString(jwt.getSubject()), id, request); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java new file mode 100644 index 0000000..2d9a712 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java @@ -0,0 +1,154 @@ +package fr.epita.assistants.ping.presentation.rest.projectEndpoints; + +import fr.epita.assistants.ping.api.requests.ExecFeatureRequest; +import fr.epita.assistants.ping.api.requests.NewProjectRequest; +import fr.epita.assistants.ping.api.requests.UpdateProjectRequest; +import fr.epita.assistants.ping.api.requests.UserProjectRequest; +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.PingServiceError; +import fr.epita.assistants.ping.domain.service.ProjectService; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.List; +import java.util.UUID; + +@Path("/api/projects") +public class ProjectResource { + @Inject + JsonWebToken jwt; + + @Inject + ProjectService projectService; + + @Inject + Logger logger; + + // Si le QueryParam onlyOwned n'est pas precise, le considerer faux + @GET + @Path("/") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getMyProject(@Context SecurityContext ctx, @QueryParam("onlyOwned") Boolean owned) { + logger.log("Entering endpoint 'getMyProject' with parameters: | ctx: " + ctx.toString() + " | (QuerryParam) onlyOwned: " + ((owned) != null && owned)); + List<ProjectResponse> ls = projectService.listProjectsOfUser(UUID.fromString(jwt.getSubject()), owned); + return Response.ok(ls).build(); + } + + @POST + @Path("/") + @Authenticated + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createProject(@Context SecurityContext ctx, NewProjectRequest project) { + if(project == null || ctx == null) { + logger.error("Entering endpoint with 'createProject' (it will fail cuz project or ctx is null)"); + ErrorsCode.BAD_REQUEST.throwException("Invalid project creation form"); + } + + logger.log("Entering endpoint 'createProject' with parameters: | ctx: " + ctx.toString() + " | project: " + project.toString()); + ProjectResponse pr = projectService.createProject(UUID.fromString(jwt.getSubject()), project.getName()); + return Response.ok(pr).build(); + } + + @GET + @Path("/all") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response allProject(@Context SecurityContext ctx) { + logger.log("Entering endpoint 'allProject' with parameters: | ctx: " + ctx.toString()); + List<ProjectResponse> ls = projectService.listAllProjects(); + return Response.ok(ls).build(); + + } + + // Seul un admin ou le detenteur du projet a le droit de call ça + @PUT + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response updateProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UpdateProjectRequest update) { + logger.log("Entering endpoint 'updateProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | update: " + update.toString()); + ProjectResponse res = projectService.updateProject(UUID.fromString(jwt.getSubject()), id, update.getName(), update.getNewOwnerId()); + return Response.ok(res).build(); + + } + + // User y accede s'il est owner ou membre du projet + @GET + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getProject(@Context SecurityContext ctx, @PathParam("id") final String id) { + logger.log("Entering endpoint 'getProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id); + ProjectResponse res = projectService.getProject(UUID.fromString(jwt.getSubject()), id); + return Response.ok(res).build(); + + } + + // Seul admin et le detenteur du projet y ont acces + @DELETE + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteProject(@Context SecurityContext ctx, @PathParam("id") final String id) { + logger.log("Entering endpoint 'deleteProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id); + projectService.deleteProject(UUID.fromString(jwt.getSubject()), id); + return Response.noContent().build(); + } + + // Admin and members of the project can access this + @POST + @Path("/{id}/add-user") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response addUserProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UserProjectRequest project) { + logger.log("Entering endpoint 'addUserProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | project: " + project.toString()); + projectService.addMember(UUID.fromString(jwt.getSubject()), id, project.getUserId()); + return Response.noContent().build(); + } + + // Admin and members of the project can access this + @POST + @Path("/{id}/exec") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response execOnProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final ExecFeatureRequest request) { + logger.log("Entering endpoint 'execOnProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | request: " + request.toString()); + + projectService.execute( + UUID.fromString(jwt.getSubject()), UUID.fromString(id), request.getFeature(), request.getCommand(), + request.getParams() + ); + + return Response.noContent().build(); + + } + + @POST + @Path("/{id}/remove-user") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response removeUserProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UserProjectRequest project) { + logger.log("Entering endpoint 'updateProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | project: " + project.toString()); + projectService.removeMember(UUID.fromString(jwt.getSubject()), id, project.getUserId()); + return Response.noContent().build(); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java new file mode 100644 index 0000000..6b7ea77 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java @@ -0,0 +1,149 @@ +package fr.epita.assistants.ping.presentation.rest.userEndpoints; + +import fr.epita.assistants.ping.api.requests.LoginRequest; +import fr.epita.assistants.ping.api.requests.NewUserRequest; +import fr.epita.assistants.ping.api.requests.UpdateUserRequest; +import fr.epita.assistants.ping.api.responses.LoginResponse; +import fr.epita.assistants.ping.converters.request.UserCreationEntityConverter; +import fr.epita.assistants.ping.converters.request.UserUpdateEntityConverter; +import fr.epita.assistants.ping.converters.response.UserResponseConverter; +import fr.epita.assistants.ping.domain.service.UserService; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/user") +public class UserResource +{ + @Inject + JsonWebToken jwt; + + @Inject + UserService userService; + + @Inject + Logger logger; + + @Inject + UserResponseConverter userResponseConverter; + + @Inject + UserCreationEntityConverter userCreationEntityConverter; + + @Inject + UserUpdateEntityConverter userUpdateEntityConverter; + + // Si on n'a pas de token/token invalide, alors le RolesAllowed + // vont renvoyer un 401. + // Je suppose que ne pas etre admin va renvoyer 403 si le jwt est bien fait + @POST + @Path("/") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response createUser(@Context SecurityContext ctx, NewUserRequest user) + { + logger.log("Entering endpoint 'createUser' with parameters: | ctx: " + ctx.toString() + " | user: " + user.toString()); + return Response + .ok(userResponseConverter.convert( + userService.createUser( + UUID.fromString(jwt.getSubject()), + userCreationEntityConverter.convert(user) + ))) + .build(); + } + + @GET + @Path("/all") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response allUsers(@Context SecurityContext ctx) + { + logger.log("Entering endpoint 'allUser' with parameters: | ctx: " + ctx.toString()); + return Response + .ok(userResponseConverter.convert(userService.getAllUsers())) + .build(); + } + + // Le seul endpoint qui ne sera pas protege + @POST + @Path("/login") + @Produces(MediaType.APPLICATION_JSON) + public Response logInUser(final LoginRequest request) + { + logger.log("Entering endpoint 'logInUser' with parameters: | request: " + request.toString()); + return Response + .ok(new LoginResponse(userService.login(request.getLogin(), request.getPassword()))) + .build(); + } + + @GET + @Path("/refresh") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response refreshToken(@Context SecurityContext ctx) + { + logger.log("Entering endpoint 'refreshToken' with parameters: | ctx: " + ctx.toString()); + return Response + .ok(new LoginResponse(userService.refreshToken(UUID.fromString(jwt.getSubject())))) + .build(); + } + + // Admin peut tout toucher + // User peut toucher que a son profil + @PUT + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response updateUser(@Context SecurityContext ctx, + @PathParam("id") final UUID id, + UpdateUserRequest user) + { + logger.log("Entering endpoint 'updateUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString() + " | user: " + user.toString()); + return Response + .ok(userResponseConverter.convert( + userService.updateUser( + UUID.fromString(jwt.getSubject()), + id, + userUpdateEntityConverter.convert(user)) + )) + .build(); + } + + // Admin peut tout toucher + // User peut toucher que a son profil + @GET + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getUser(@Context SecurityContext ctx, @PathParam("id") final UUID id) + { + logger.log("Entering endpoint 'getUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString()); + return Response + .ok(userResponseConverter.convert(userService.getUser(UUID.fromString(jwt.getSubject()), id))) + .build(); + } + + // Seul Admin peut delete + // Cependant un user avec des projects ne peut pas etre delete + @DELETE + @Path("/{id}") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteUser(@Context SecurityContext ctx, @PathParam("id") final UUID id) + { + logger.log("Entering endpoint 'getUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString()); + return Response + .status(204) + .entity(userResponseConverter.convert(userService.deleteUser(UUID.fromString(jwt.getSubject()), id))) + .build(); + } +}
\ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java new file mode 100644 index 0000000..0255e86 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java @@ -0,0 +1,14 @@ +package fr.epita.assistants.ping.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@AllArgsConstructor +@Setter +@NoArgsConstructor +public class ErrorInfo { + private String message; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java new file mode 100644 index 0000000..3ec0c86 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java @@ -0,0 +1,40 @@ +package fr.epita.assistants.ping.utils; + +import java.util.function.Supplier; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.Status; + +import static jakarta.ws.rs.core.Response.status; + +public class HttpError implements Supplier<RuntimeException>, IHttpError { + private final Status status; + private final String message; + + public HttpError(Status status, String message) { + this.status = status; + this.message = message; + } + + private RuntimeException createError(Object... args) { + return new WebApplicationException( + status(status) + .entity(new ErrorInfo(String.format(message, args))) + .build()); + } + + @Override + public RuntimeException get() { + return createError(); + } + + @Override + public RuntimeException get(Object... args) { + return createError(args); + } + + @Override + public void throwException(Object... args) { + throw get(args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java new file mode 100644 index 0000000..6616166 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.utils; + +public interface IHttpError { + RuntimeException get(Object... args); + + void throwException(Object... args); +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java new file mode 100644 index 0000000..a3010b3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java @@ -0,0 +1,129 @@ +package fr.epita.assistants.ping.utils; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.Console; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +@ApplicationScoped +public class Logger { + private static final String RESET_TEXT = "\u001B[0m"; + private static final String RED_TEXT = "\u001B[31m"; + private static final String GREEN_TEXT = "\u001B[32m"; + private static final String YELLOW_TEXT = "\u001B[33m"; + private static final String BLUE_TEXT = "\u001B[36m"; + private static final String PURPLE_TEXT = "\u001B[35m"; + + @ConfigProperty(name="LOG_FILE", defaultValue="ping.log") + String logFile; + + @ConfigProperty(name="ERROR_LOG_FILE", defaultValue="error.log") + String errorFile; + + // null = file inexistante + private static File logPath = null; + private static File errorPath = null; + + public void checkFiles() + { + if (logPath == null) + { + logPath = new File(logFile); + + try + { + logPath.createNewFile(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + + if (errorPath == null) + { + errorPath = new File(errorFile); + try + { + errorPath.createNewFile(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + } + + public void log(String text) + { + text = "[" + timestamp() + "] [LOG] " + text; + writeToFile("normal", text); + System.out.println(PURPLE_TEXT + text + RESET_TEXT); + } + public void debug(String text) + { + text = "[" + timestamp() + "] [DEBUG] " + text; + writeToFile("normal", text); + System.out.println(BLUE_TEXT + text + RESET_TEXT); + } + public void info(String text) + { + text = "[" + timestamp() + "] [INFO] " + text; + writeToFile("normal", text); + System.out.println(text); + } + public void warn(String text) + { + text = "[" + timestamp() + "] [!WARN!] " + text; + writeToFile("normal", text); + System.out.println(YELLOW_TEXT + text + RESET_TEXT); + } + public void error(String text) + { + text = "[" + timestamp() + "] [!!!ERROR!!!] " + text; + writeToFile("error", text); + System.out.println(RED_TEXT + text + RESET_TEXT); + } + + private void writeToFile(String type, String text) + { + checkFiles(); + if (type.equals("normal") && logFile != null) + { + try + { + // On est en Java, j'en ai rien a faire + FileWriter writer = new FileWriter(logFile, true); + writer.write(text + "\n"); + writer.close(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + if (!type.equals("normal") && errorFile != null) + { + try + { + FileWriter writer = new FileWriter(errorFile, true); + writer.write(text + "\n"); + writer.close(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + } + + private static String timestamp() { + return new SimpleDateFormat("dd/MM/yy - HH:mm:ss") + .format(Calendar.getInstance().getTime()); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java new file mode 100644 index 0000000..1059733 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java @@ -0,0 +1,27 @@ +package fr.epita.assistants.ping.utils; + +import fr.epita.assistants.ping.data.model.UserModel; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class TokenFactory { + + private String deduceTokenRole(UserModel user) { + return user.getIsAdmin() ? "admin" : "user"; + } + + private JwtClaimsBuilder buildClaims(UserModel user) { + return Jwt.claims() + .subject(user.getId().toString()) + .upn(user.getLogin()) + .preferredUserName(user.getLogin()) + .groups(deduceTokenRole(user)); + } + + public String generateLoginToken(UserModel user) { + return buildClaims(user).sign(); + } +} diff --git a/ping/ping/src/main/resources/application.properties b/ping/ping/src/main/resources/application.properties new file mode 100644 index 0000000..31ac487 --- /dev/null +++ b/ping/ping/src/main/resources/application.properties @@ -0,0 +1,21 @@ +# Enable HTTP +quarkus.http.insecure-requests=enabled +# Setup PostgreSQL +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=postgres +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/ping +quarkus.transaction-manager.default-transaction-timeout=3000s +quarkus.hibernate-orm.log.queries-slower-than-ms=200 +quarkus.hibernate-orm.database.generation=update +# Set port +quarkus.http.port=8080 +quarkus.http.cors.enabled=false +quarkus.devservices.enabled=true +%test.quarkus.devservices.enabled=false +# JWT configuration +smallrye.jwt.new-token.signature-algorithm=RS256 +smallrye.jwt.sign.key.location=privateKey.pem +smallrye.jwt.verify.key.location=publicKey.pem +smallrye.jwt.new-token.lifespan=3600 +smallrye.jwt.new-token.issuer=PatapimVest-API +PROJECT_DEFAULT_PATH=/var/www/projects/
\ No newline at end of file diff --git a/ping/ping/src/main/resources/openapi.yml b/ping/ping/src/main/resources/openapi.yml new file mode 100644 index 0000000..104ba5e --- /dev/null +++ b/ping/ping/src/main/resources/openapi.yml @@ -0,0 +1,1065 @@ +--- +openapi: 3.1.0 +paths: + /api/error: + get: + responses: + "200": + description: OK + summary: Error + tags: + - Hello World Resource + /api/hello: + get: + responses: + "200": + description: OK + summary: Hello World + tags: + - Hello World Resource + /api/projects: + get: + summary: List projects of user + description: | + List all the projects of the logged in user. + + The param onlyOwned specifies if the response should return all the projects the user is a member of, + or only those the user owns. + + By default onlyOwned is considered as false. + + Any logged in user can access this endpoint. + tags: + - Project + parameters: + - name: onlyOwned + in: query + schema: + type: boolean + responses: + "200": + description: List of the projects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectResponse' + "403": + description: Should not happen (this is automatically generated) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a project + description: | + Create a new project with a name, the owner should also be in the list of members. + + Any logged in user can access this endpoint. + tags: + - Project + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewProjectRequest' + required: true + responses: + "200": + description: The created project + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "400": + description: The project name is invalid (null or empty for example) + "403": + description: Should not happen (this is automatically generated) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/all: + get: + summary: List all the projects + description: | + List all the projects available, be careful the response should not contain all the information about the members or the owner. + + Only an admin can access this endpoint. + tags: + - Project + responses: + "200": + description: List of the projects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectResponse' + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/projects/{id}: + put: + summary: Update a project + description: | + Update a project, by changing its name or the owner of the project. + + If the new owner id or the new name are the same as the current one, it should not change anything and it is not an error. + + You should only update the fields that are not null in the request. + If some fields are null, they should not be updated. + + If all the fields are null, it should return a bad request. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProjectRequest' + required: true + responses: + "200": + description: The project was modified + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "404": + description: "The project could not be found, or the new owner is not a\ + \ member of the project" + "403": + description: The user is not allowed to access the project + "400": + description: Both the name and the new owner are null + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + get: + summary: Get a project + description: |- + Get a specific project based on its id. + + A user can access this endpoint if he is a member of the project or the owner. + + An admin can access this endpoint in any case. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "200": + description: The project + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "404": + description: Project not found + "403": + description: The user is not allowed to access the project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a project + description: | + Delete a project. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "204": + description: The project was deleted + "404": + description: The project could not be found + "403": + description: The user is not allowed to access this project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/add-user: + post: + summary: Add a member + description: | + Add a member to a project. + + Any member of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserProjectRequest' + required: true + responses: + "204": + description: The user was added to the project + "404": + description: "The project, or the user could not be found" + "403": + description: The user is not allowed to access the project + "400": + description: The userId is invalid (null or empty for example) + "409": + description: The user is already a member of the project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/exec: + post: + summary: Execute a feature + description: | + Execute a feature on the project. + + For git features: + + If the project is not a git repository, it should return a bad request (except if the feature is git init). + + + For git add, if a file does not exist it should return a bad request. + + Any other error should be returned as an internal server error. + + Any member of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecFeatureRequest' + required: true + responses: + "204": + description: The feature was successfully executed + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project + "400": + description: "Any parameter of the request is invalid (null or non-existent\ + \ feature)" + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/remove-user: + post: + summary: Remove a member + description: | + Remove a member from a project. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserProjectRequest' + required: true + responses: + "204": + description: The user was removed from the project + "404": + description: "The project, or the user could not be found, the user to remove\ + \ is not a member of the project" + "403": + description: "The user is not allowed to access the project, or the user\ + \ to remove is the owner of the project" + "400": + description: The userId is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files: + get: + summary: Get a file + description: | + Retrieve the content of a file. + + It should return a byte array of the content of the file. + + The content type must be application/octet-stream. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + default: "" + responses: + "200": + description: Content of the file + "404": + description: The project or the relative path could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a file + description: | + Delete a file from the file system and all its content, be careful if the file is the root + you should only empty it and not remove the root file of the project. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "204": + description: The file was deleted + "404": + description: The project or the file could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a file + description: | + Create a new file on the file system. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "201": + description: The file was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The file already exists + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files/move: + put: + summary: Move a file + description: | + Move a file to a new location or rename it if the destination is in the same parent directory + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MoveRequest' + required: true + responses: + "204": + description: The file was renamed + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The file already exists + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files/upload: + post: + summary: Upload a new file + description: | + Upload data to create or modify a file, it should write the content + of the body to the file and create it if it does not exist. + + The content type received must be application/octet-stream. + + You can retrieve the data of the body using the InputStream type for your parameter. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + required: true + responses: + "201": + description: The file was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/folders: + get: + summary: List a folder + description: | + List the content of the folder located at the given path + in the project with the given id. By default it lists the root folder. + + It is NOT a recursive listing, it only lists the immediate children of the folder. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + default: "" + responses: + "200": + description: List of folder entries + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FSEntryResponse' + "404": + description: The project or the relative path could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a folder + description: | + Delete a folder from the file system and all its content, + be careful if the folder is the root you should only empty it and not remove the root folder of the project. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "204": + description: The folder was deleted + "404": + description: The project or the folder could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a folder + description: | + Create a new folder on the file system. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "201": + description: The folder was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The folder already exists + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/folders/move: + put: + summary: Move a folder + description: | + Move a folder to a new location, or rename it if the destination is in the same parent directory. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MoveRequest' + required: true + responses: + "204": + description: The folder was renamed + "404": + description: The project could not be found or the source folder could not + be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The folder already exists + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/user: + post: + summary: Create a user + description: | + Create a new user, its login should be unique and contain at most + one '.' or '_'. The name should be initialized to the login split and capitalized on the separator. + + Only an admin can access this endpoint. + tags: + - User + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewUserRequest' + required: true + responses: + "200": + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "400": + description: The login or the password is invalid + "409": + description: The login is already taken + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/user/all: + get: + summary: List all the users + description: | + List all the users available, be careful the response should not contain all the + information about the members or the owner. + + Only an admin can access this endpoint. + tags: + - User + responses: + "200": + description: List of the users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserResponse' + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/user/login: + post: + summary: Login a user + description: | + Login a user with its login and password. + + It should return a token if the login/password combination is valid. + tags: + - User + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + required: true + responses: + "200": + description: The token of the user + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + "401": + description: The login/password combination is invalid + "400": + description: The login or the password is null + servers: + - url: http://localhost:8080 + /api/user/refresh: + get: + summary: Refresh a token + description: | + Refresh the token of a user by returning a new one. + + Only a logged in user can access this endpoint. + tags: + - User + responses: + "200": + description: The new token + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + "404": + description: The user could not be found + "401": + description: Not Authorized + "403": + description: Not Allowed + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/user/{id}: + put: + summary: Update a user + description: | + Update a user + + For the sake of simplicity the password can be modified. + + Any blank value should be ignored (except for the avatar). + + Only the same user or an admin can access this endpoint. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + required: true + responses: + "200": + description: The modified user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "404": + description: The user could not be found + "403": + description: The user is not allowed + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + get: + summary: Get a user + description: | + Get the data of a user. + + A user can only access its data while an admin can access any user data. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "200": + description: The user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "404": + description: User not found + "403": + description: The user is not allowed to access this user + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a user + description: | + Delete a user. + + If the user owns projects, he cannot be deleted and a forbidden response should be returned. + + Only an admin can access this endpoint. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "204": + description: The user was deleted + "404": + description: The user could not be found + "403": + description: "The user is not allowed to access this endpoint, or the user\ + \ owns projects" + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin +components: + schemas: + ExecFeatureRequest: + type: object + properties: + feature: + type: string + command: + type: string + params: + type: array + items: + type: string + FSEntryResponse: + type: object + properties: + name: + type: string + path: + type: string + isDirectory: + type: boolean + LoginRequest: + type: object + properties: + login: + type: string + password: + type: string + LoginResponse: + type: object + properties: + token: + type: string + MoveRequest: + type: object + properties: + src: + type: string + dst: + type: string + NewProjectRequest: + type: object + properties: + name: + type: string + NewUserRequest: + type: object + properties: + login: + type: string + password: + type: string + isAdmin: + type: boolean + PathRequest: + type: object + properties: + relativePath: + type: string + ProjectResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + name: + type: string + members: + type: array + items: + $ref: '#/components/schemas/UserSummaryResponse' + owner: + $ref: '#/components/schemas/UserSummaryResponse' + UUID: + type: string + format: uuid + pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + UpdateProjectRequest: + type: object + properties: + name: + type: string + newOwnerId: + $ref: '#/components/schemas/UUID' + UpdateUserRequest: + type: object + properties: + password: + type: string + displayName: + type: string + avatar: + type: string + UserProjectRequest: + type: object + properties: + userId: + $ref: '#/components/schemas/UUID' + UserResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + login: + type: string + displayName: + type: string + isAdmin: + type: boolean + avatar: + type: string + UserSummaryResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + displayName: + type: string + avatar: + type: string + securitySchemes: + SecurityScheme: + type: http + scheme: bearer + bearerFormat: JWT + description: Authentication +tags: +- name: File + description: File API +- name: Folder + description: Folder API +- name: Project + description: Project API +- name: User + description: User API +info: + title: ping API + version: "1.0" diff --git a/ping/ping/src/main/resources/privateKey.pem b/ping/ping/src/main/resources/privateKey.pem new file mode 100644 index 0000000..72fef7b --- /dev/null +++ b/ping/ping/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDM3vt0cpH3XNFa +ZpZDkwa6QLV45Z5nB3qd9dF+NgoP+Kzar/VAUndnTaf8YDK4J90bc3jwaepvsE7y +0Wy3KGEwNEDx5A+rIhWFx3UfoPvZLbPzkwpMpCP+tU22OF+EB/mdKRMzl2VX6nOG +fo/P88xmF5wCVTCscM7PQmzEuMTn+MZCnI2N6StrBpJDf2g6lrN3JOq3wpqcIAxP +y2AkrHN2E2Ihut1cbLE9dmhDdyygM2XtzCjFs2foBycFc0u/GpWwqA5VfHs7fww9 +saAtXuBVrZ1A9fvZbXxHbLNVgjsRCoAhk299cRv0h06lamqD/xvfO8UqpIxKAVeQ +3kVReoM/AgMBAAECggEAHnT77wjlWF/tI8JijnM6KdBa6jlAvlJnUTptRZUlE9DF +adGE5WYdKDdsROHMSIJspZsaHKuyVZ5hbkniHqybHj52dT+RcMClcHkBmY0mQ9g+ +i3SNsG0qiMBNaEyjLo689jdYCnB0eYh6WkFQgfrdpUPXRqWBkfFHXZqjbu+FHnMi +VWLspMGetabvEcMDAcDWWrVE+JKuXaBFwJBfXdwNNeo0og4F785YlckymdCoYSBl +9Xv2P7GUilF6PRaNGmSN5gLw9bpJWev7hxPIZK2I+h8fAZOigpPdw0KkpnWFP/Lo +zUT9e4yUeeO2XrWdpY4YEJFcQY1xC47AI62b7pZnoQKBgQDzlGp6DpzANR1frjvS +cwxRqgGssW0ADbuAX7vNDfNGpnbhlSJglWkYIRmP9ylHTjLKz8ju9pmGJ1DU5ADW +B3UBGA3rEOJdqW3pu2Pfc49vZY8XUnUb9iEcjWEIx/bli8md/AoZTb+p+PWHBtOa +EQaicCqYc4HGtLcy40t6rQwNjwKBgQDXUUeK0lkEyEzDDXHqGl1DumMSx5/b+TzO ++GMG00ODNHifO1yNwXZpJ6qwOln6MEVQQxbO0zsWb/fCBa3+nO8QkwJ6RiwL2wZM +LhVT1DgGnGdUHyX8cYrz48KSHT1AaL2Xpso+PEBG57oG06OnTCs4zjmXNhscKQO7 +xp7PQve3UQJ/DkWj94LjrF9wWSrcVnhnQGG1t2it+lLTqbHyTxGCL6GRVaZnXUFj +5wYJeWYCBSuPzlJec6H/XNzw34h8hA3rzSSbrCpRGSvURN21/GHJBb3tpHAwbHL/ +fazLAri0vr5yEc+yx8jGci8kM8UyoNRejyYDWOtrrzjngxPOWGHLUwKBgDdItEDN +j3Ot5h+yxQvhpw6rvaJlxQLa9KUDm5F2DwCSKWJhAHuZL8kCGU5UPSQC3tkj0Khc +DfDN4ORdRvZ33t33kQxKFicZHn3S20EB3Ty9Tb6InN07Xf0O/rc7cK2n9v4IIidm +mu5ZsybrkjpzUSmtsZXEm6NYWJAPCFmZDTdxAoGAPZUhdtFBDHcq1lexfTZuLQXg +/V0aSDO8ptagb/foHuWEnqxwsldMplMjx3naOkLrRYA+BJ/zdItv0cJrbl5xpXI4 +S5AHyerNXzZr3ptYGwOUyulNsmA+9WGBTdREhDnzToFL4u0ZZn5+ENJFPK8rygqh +M6PGLdqKHiXGAsGQz18= +-----END PRIVATE KEY----- diff --git a/ping/ping/src/main/resources/publicKey.pem b/ping/ping/src/main/resources/publicKey.pem new file mode 100644 index 0000000..49676c3 --- /dev/null +++ b/ping/ping/src/main/resources/publicKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzN77dHKR91zRWmaWQ5MG +ukC1eOWeZwd6nfXRfjYKD/is2q/1QFJ3Z02n/GAyuCfdG3N48Gnqb7BO8tFstyhh +MDRA8eQPqyIVhcd1H6D72S2z85MKTKQj/rVNtjhfhAf5nSkTM5dlV+pzhn6Pz/PM +ZhecAlUwrHDOz0JsxLjE5/jGQpyNjekrawaSQ39oOpazdyTqt8KanCAMT8tgJKxz +dhNiIbrdXGyxPXZoQ3csoDNl7cwoxbNn6AcnBXNLvxqVsKgOVXx7O38MPbGgLV7g +Va2dQPX72W18R2yzVYI7EQqAIZNvfXEb9IdOpWpqg/8b3zvFKqSMSgFXkN5FUXqD +PwIDAQAB +-----END PUBLIC KEY----- diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java new file mode 100644 index 0000000..d9a1e69 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java @@ -0,0 +1,111 @@ +package fr.epita.assistants.ping.data.repositories; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; + +import java.nio.file.Path; +import java.util.*; + + +public class ProjectFakeRepository extends ProjectRepository { + + private final Path defaultPath = Path.of("."); + + private final Map<UUID, ProjectModel> database = new HashMap<>(); + + @Override + public long count() { + return database.size(); + } + + @Override + public ProjectModel createProject(UserModel owner, String name) { + ProjectModel project = new ProjectModel() + .withName(name) + .withOwner(owner); + + project.setId(UUID.randomUUID()); + project.setPath(defaultPath + project.getId().toString()); + + database.put(project.getId(), project); + + return project; + } + + @Override + public List<ProjectModel> getAllProjects() { + return database.values().stream().toList(); + } + + @Override + public Optional<ProjectModel> getProject(UUID id) { + return Optional.ofNullable(database.get(id)); + } + + @Override + public List<ProjectModel> getAllOwnedProjects(UserModel owner) { + return database + .values() + .stream() + .filter( + project -> project + .getOwner() + .getId() + .compareTo(owner.getId()) == 0 + ).toList(); + } + + @Override + public void updateName(UUID id, String name) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.setName(name); + database.put(id, project); + } + + @Override + public void updateOwner(UUID id, UserModel newOwner) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.setOwner(newOwner); + database.put(id, project); + } + + @Override + public void addMember(UUID id, UserModel member) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.getMembers().add(member); + database.put(id, project); + } + + @Override + public void removeMember(UUID id, UserModel member) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.getMembers().remove(member); + database.put(id, project); + } + + @Override + public void deleteProject(UUID id) { + database.remove(id); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java new file mode 100644 index 0000000..bd4861a --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java @@ -0,0 +1,121 @@ +package fr.epita.assistants.ping.data.repositories; + +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.UserRepository; + +import java.util.*; + + +public class UserFakeRepository extends UserRepository { + + private final Map<UUID, UserModel> database = new HashMap<>(); + + @Override + public long count() { + return database.size(); + } + + public UserModel createUser(UserModel user) { + database.put(user.getId(), user); + + return user; + } + + @Override + public UserModel createUser(String name, Boolean admin, String avatar, String login, String password) { + UserModel user = new UserModel() + .withAvatar(avatar) + .withLogin(login) + .withPassword(password) + .withIsAdmin(admin) + .withDisplayName(name); + + user.setId(UUID.randomUUID()); + + return createUser(user); + } + + @Override + public List<UserModel> getAllUsers() { + return database.values().stream().toList(); + } + + @Override + public Optional<UserModel> getUser(UUID id) { + return Optional.ofNullable(database.get(id)); + } + + @Override + public Optional<UserModel> getUser(String login) { + return database + .values() + .stream() + .filter(userModel -> Objects.equals(userModel.getLogin(), login)) + .findFirst(); + } + + @Override + public void updateLogin(UUID id, String login) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setLogin(login); + database.put(id, user); + } + + @Override + public void updatePassword(UUID id, String password) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setPassword(password); + database.put(id, user); + } + + @Override + public void updateIsAdmin(UUID id, Boolean admin) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setIsAdmin(admin); + database.put(id, user); + } + + @Override + public void updateAvatar(UUID id, String avatar) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setAvatar(avatar); + database.put(id, user); + } + + @Override + public void updateDisplayName(UUID id, String name) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setDisplayName(name); + database.put(id, user); + } + + @Override + public void deleteUser(UUID id) { + database.remove(id); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java new file mode 100644 index 0000000..1368cf3 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java @@ -0,0 +1,31 @@ +package fr.epita.assistants.ping.presentation.rest; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +@QuarkusTest +@TestHTTPEndpoint(HelloWorldResource.class) +public class TestHelloWorldResource { + + @Test + public void testHelloWorldEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(equalTo("Hello World !")); + } + + @Test + public void testErrorEndpoint() { + given() + .when().get("/error") + .then() + .statusCode(400) + .body("message", equalTo("Example error: This is an error")); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java new file mode 100644 index 0000000..54bf30c --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java @@ -0,0 +1,32 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.data.repository.UserRepository; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +public class TestProjectResource { + + @BeforeAll + public static void setup() + { + // TODO : Create the two users, store their JWT + } + + @Nested + class GetMyProject { + + @Test + public void GetNonExistent() + { + given().when().get("/api/hello").then().statusCode(200); + } + + } + +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java new file mode 100644 index 0000000..1d08df6 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java @@ -0,0 +1,431 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.responses.UserResponse; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repositories.UserFakeRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import io.quarkus.test.junit.QuarkusMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.junit.jupiter.api.*; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +public class TestUserResource { + + UserFakeRepository userFakeRepository; + + private static final String testUserUUIDStr = "5ed796ba-e0b9-476d-b4e0-6924ae1144be"; + private static final UUID testUserUUID = UUID.fromString(testUserUUIDStr); + private static final String testUserUsername = "test_user"; + private static final String testUserPassword = "bombardillo"; + private static final UserModel testUser = new UserModel() + .withLogin(testUserUsername) + .withPassword(testUserPassword) + .withDisplayName(testUserUsername) + .withAvatar(null) + .withIsAdmin(false); + + private static final String testAdminUUIDStr = "d962b6b0-79fb-40a7-a12d-fdbb416aa7b2"; + private static final UUID testAdminUUID = UUID.fromString(testAdminUUIDStr); + private static final String testAdminUsername = "test_admin"; + private static final String testAdminPassword = "crocodillo"; + private static final UserModel testAdmin = new UserModel() + .withLogin(testAdminUsername) + .withPassword(testAdminPassword) + .withDisplayName(testAdminUsername) + .withAvatar(null) + .withIsAdmin(true); + + @BeforeEach + void setupEach() { + userFakeRepository = new UserFakeRepository(); + + testUser.setId(testUserUUID); + testAdmin.setId(testAdminUUID); + + userFakeRepository.createUser(testUser); + userFakeRepository.createUser(testAdmin); + + QuarkusMock.installMockForType(userFakeRepository, UserRepository.class); + } + + @Nested + class TestSession { + + @Nested + class TestLogin { + + @Test + public void testLoginGood() { + final String testPayload = "{" + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(200) + .body("token", anything()); + } + + @Test + public void testLoginBadPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "_123\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("Password is incorrect")); + } + + @Test + public void testLoginBadUsername() { + final String testPayload = "{" + + "\"login\": \"" + testUserUsername + "_123\", " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("User does not exist")); + } + + @Test + public void testLoginNonexistant() { + final String testPayload = "{ " + + "\"login\": \"" + "bad_user" + "\", " + + "\"password\": \"" + "wiwiwi" + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("User does not exist")); + } + + @Test + public void testLoginInvalidUsername() { + final String testPayload = "{ " + + "\"login\": \"\", " + + "\"password\": \"" + "wiwiwi" + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Username field is empty or null")); + } + + @Test + public void testLoginInvalidPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Password field is empty or null")); + } + + @Test + public void testLoginNullUsername() { + final String testPayload = "{ " + + "\"login\": null, " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Username field is empty or null")); + } + + @Test + public void testLoginNullPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": null " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Password field is empty or null")); + } + } + + @Nested + class TestRefresh { + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testRefreshGood() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(200) + .body("token", anything()); + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = "32516464-07c7-4aad-9585-d248e45dbdf8") + } + ) + public void testRefreshBad() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(401) + .body("message", containsString( + "Session JWT lead to an user who does not exist." + + " You may need to login again.") + ); + } + + @Test + public void testRefreshNotLogged() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(401) + .body(emptyString()); + } + } + } + + @Nested + class TestUser { + + @Nested + class TestAllUsers { + + @Test + @TestSecurity(user = testAdminUsername, roles = { "admin" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testAdminUUIDStr) + } + ) + public void testAllUsersAuthorized() { + given() + .when().get("/api/user/all") + .then() + .statusCode(200) + .extract() + .as(UserResponse[].class); + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testAllUsersUnauthorized() { + given() + .when().get("/api/user/all") + .then() + .statusCode(403) + .body(emptyString()); + } + + @Test + public void testAllUsersNotConnected() { + given() + .when().get("/api/user/all") + .then() + .statusCode(401) + .body(emptyString()); + } + } + + @Nested + class TestCreateUser { + + private Response givenTemplate(final String body) { + return given() + .contentType(ContentType.JSON) + .body(body) + .when().post("/api/user"); + } + + @Nested + @TestSecurity(user = testAdminUsername, roles = { "admin" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testAdminUUIDStr) + } + ) + class TestCreateUserAuthenticated { + + @Test + public void testCreateUserValid() { + final String newUsername = "jammy.oiaia"; + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": \"" + newUsername + "\", " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + final long oldCount = userFakeRepository.count(); + + givenTemplate(testPayload) + .then() + .statusCode(200) + .body("id", matchesPattern("^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$")) + .body("login", equalTo(newUsername)) + .body("displayName", equalTo("Jammy Oiaia")) + .body("isAdmin", equalTo(false)) + .body("avatar", equalTo(null)); + + Assertions.assertEquals(userFakeRepository.count(), oldCount + 1); + } + + @Test + public void testCreateUserInvalid() { + final String newUsername = "jammy"; + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": \"" + newUsername + "\", " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Login must only have at most a dot or a dash")); + } + + @Test + public void testCreateUserLoginExistant() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": false" + + " }"; + + givenTemplate(testPayload) + .then() + .statusCode(409) + .body("message", containsString("User with login " + testUserUsername + " already exists")); + } + + @Test + public void testCreateUserNull() { + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": null, " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Username should not be empty or null")); + } + + @Test + public void testCreatePasswordNull() { + final String newLogin = "fred.pp"; + final String testPayload = "{ " + + "\"login\": \"" + newLogin + "\", " + + "\"password\": null, " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Password should not be empty or null")); + } + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testCreateUserNotAnAdmin() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "_2\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": false\"\"" + + "}"; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .then() + .statusCode(403) + .body(emptyString()); + } + + @Test + public void testCreateUserNotConnected() { + final String testPayload = "{ " + + "\"login\": " + testUserUsername + "_2\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(401) + .body(emptyString()); + } + } + } +} diff --git a/ping/ping/src/test/resources/application.properties b/ping/ping/src/test/resources/application.properties new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ping/ping/src/test/resources/application.properties |
