diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..bb28cbb16ed2cc2e3111ec3d031d709e80643963 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Dependency directories +/node_modules + +# Distribution directories +/dist +/build + +# Environment files +.env.* + +# Editor directories and files +.vscode +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Cache directories +/.cache + +# Test directories +/coverage +/cypress/videos/ +/cypress/screenshots/ + +# Temporary files +*.temp + +# System files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..6617ed2a88898f11fd513893024133b50cc14c7a --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,26 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + 'extends': [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript', + '@vue/eslint-config-prettier/skip-formatting' + ], + overrides: [ + { + files: [ + 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}', + 'cypress/support/**/*.{js,ts,jsx,tsx}' + ], + 'extends': [ + 'plugin:cypress/recommended' + ] + } + ], + parserOptions: { + ecmaVersion: 'latest' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8ee54e8d343e466a213c8c30aa04be77126b170d --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..10032958b39ddcdf8800fb2191a028ed56336cc0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,65 @@ +image: node:20-slim + +stages: + - install + - lint_and_format + - build + - test + - security_scan + +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - node_modules/ + +install: + stage: install + script: + - npm install + artifacts: + paths: + - node_modules/ + +format-code: + stage: lint_and_format + script: + - npm run format-test + +lint-code: + stage: lint_and_format + script: + - npm run lint + +type-check: + stage: build + script: + - npm run type-check + - npm run build + +unit-tests: + stage: test + script: + - npm run test:unit + +test:e2e: + image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge + stage: test + script: + - npm ci --cache .npm --prefer-offline + - npx cypress verify + - npm run test:e2e + dependencies: + - install + +test-coverage: + stage: test + script: + - npm run test:coverage + +include: + - template: SAST.gitlab-ci.yml + +sast: + stage: security_scan + script: + - echo "Running SAST..." \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000000000000000000000000000000000..34f62fb090b60fba4944a1cd0ddaf1505e0d45e4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 4, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "none", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000000000000000000000000000000..93ea3e7838b1b661bb7052023576ffa6deb2e7a1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a61fa0595dc86c1c85276f06baefd2b01f827abf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:21.5.0 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD [ "npm", "run", "dev" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..c483e0b12516af8ea4d80e3cf72efbf340d9a9a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: build run run-dev unit e2e clean-docker + +build-docker: + docker build -t sparesti_frontend . + +run-docker: + docker run --rm --name sparesti_frontend_container -p 5173:5173 sparesti_frontend + +clean-docker: + -docker stop sparesti_frontend_container + -docker rm sparesti_frontend_container + +run: + make build-docker + make clean-docker + make run-docker + +run-dev: + npm run dev + +unit: + npm run test:unit + +e2e: + npm run test:e2e diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d1a779a4752196653f8658c6d493f3e14e6619c5 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# idatt2106_2024_02_frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Run Unit Tests with [Vitest](https://vitest.dev/) + +```sh +npm run test:unit +``` + +### Run End-to-End Tests with [Cypress](https://www.cypress.io/) + +```sh +npm run test:e2e:dev +``` + +This runs the end-to-end tests against the Vite development server. +It is much faster than the production build. + +But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): + +```sh +npm run build +npm run test:e2e +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b0082d8d060e60816c857dafd22ef7e42c74320 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:4173/', + video: false, + }, + env: { + apiUrl: 'http://localhost:8080/', + }, +}); \ No newline at end of file diff --git a/cypress/e2e/homeView.cy.ts b/cypress/e2e/homeView.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..35a44b1610d94856fd49f5618e12e1dcc1e49e4d --- /dev/null +++ b/cypress/e2e/homeView.cy.ts @@ -0,0 +1,136 @@ +/*import { useUserStore } from '../../src/stores/userStore' + +describe('Goals and Challenges Page Load', () => { + let userStore; + + beforeEach(() => { + // Add console log to trace API calls + cy.on('window:before:load', (win) => { + cy.spy(win.console, 'log'); + }); + + cy.window().then((win) => { + win.sessionStorage.setItem('accessToken', 'validAccessToken'); + win.localStorage.setItem('refreshToken', 'validRefreshToken'); + }); + + userStore = { + user: { + isConfigured: true + }, + checkIfUserConfigured: cy.stub().resolves(), + }; + + cy.stub(window, useUserStore()).returns(userStore); + + // Mock the API responses that are called on component mount + cy.intercept('GET', '/goals', { + statusCode: 200, + body: { + content: [ + { id: 1, title: 'gaming', saved: 150, target: 1000, completion: 15 }, + ], + }, + }).as('fetchGoals'); + // Mock the POST request for renewing the token if it's not implemented in the backend + cy.intercept('POST', '/auth/renewToken', { + statusCode: 200, + body: { + accessToken: 'newlyRenewedAccessToken' + } + }).as('renewToken'); + + cy.intercept('GET', '/challenges', { + statusCode: 200, + body: { + content: [ + { id: 1, title: 'Coffee Challenge', type:'coffee',perPurchase: 20, saved: 60, target: 100, completion: 60 }, + ], + }, + }).as('fetchChallenges'); + + cy.intercept('GET', '/profile/streak', { + statusCode: 200, + body: { + content: [ + { streak: 1, startDate: "2026-04-29T12:10:38.308Z" }, + ], + }, + }).as('fetchChallenges'); + // Visit the component that triggers these requests in `onMounted` + cy.visit('/hjem'); + }); + + it('loads and displays goals and challenges after onMounted', () => { + // Wait for API calls made during `onMounted` to complete + cy.wait(['@fetchGoals', '@fetchChallenges']); + // Mock the POST request for renewing the token if it's not implemented in the backend + cy.intercept('POST', '/auth/renewToken', { + statusCode: 200, + body: { + accessToken: 'newlyRenewedAccessToken' + } + }).as('renewToken'); + + // Check console logs for any errors or warnings that might indicate issues + cy.window().then((win) => { + expect(win.console.log).to.be.calledWithMatch(/Goals:/); // Adjust based on actual logging in your Vue app + }); + + // Assertions to verify the DOM is updated correctly + cy.get('[data-cy=goal-title]').should('exist').and('contain', 'gaming'); + cy.get('[data-cy=challenge-title]').should('exist').and('contain', 'Coffee Challenge'); + }); + it('Should increment a challenges progress when the increment button is clicked', () => { + cy.wait('@fetchChallenges'); + // Separate aliases for clarity + cy.intercept('PUT', '/challenges/1', { + statusCode: 200, + body: { + id: 1, + title: 'Coffee Challenge', + type: 'coffee', + perPurchase: 20, + saved: 80, // this is the updated amount + target: 100, + completion: 80, + }, + }).as('incrementChallenge1'); + + + cy.intercept('PUT', '/goals/1', { + statusCode: 200, + body: { id: 1, title: 'gaming', saved: 170, target: 1000, completion: 15 }, + }).as('incrementChallenge'); + + // Mock the POST request for renewing the token if it's not implemented in the backend + cy.intercept('POST', '/auth/renewToken', { + statusCode: 200, + body: { + accessToken: 'newlyRenewedAccessToken' + } + }).as('renewToken'); + cy.get('[data-cy=increment-challenge1]').click(); + cy.wait('@incrementChallenge1'); // Wait for the specific challenge update intercept + + // Check if the progress bar reflects the right percentage + cy.get('[data-cy=challenge-progress]') + .invoke('attr', 'style') + .should('contain', 'width: 80%'); // Directly check the style attribute for the width + }); + it('Should navigate to the spare challenges page when adding a new challenge', () => { + // Mock the routing to the spare challenges page + cy.intercept('GET', '/spareutfordringer', { + statusCode: 200, + body: { content: 'Spare Challenges Page' } + }).as('spareChallenges'); + + // Trigger the route change + cy.get('[data-cy=challenge-icon-1]').click(); + + // Assert that navigation has occurred + cy.url().should('include', '/spareutfordringer/rediger/1'); + }); + +}); +*/ \ No newline at end of file diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..3dba281cd4821c44dbe43c8600cf71e3c8ca1302 --- /dev/null +++ b/cypress/e2e/login.cy.ts @@ -0,0 +1,50 @@ +describe('Login', () => { + beforeEach(() => { + cy.visit('/logginn') + }) + + function fullInput() { + cy.get('input[name=username]').type('test') + cy.get('input[name=password]').type('test') + } + + it('visits the login page as default', () => { + cy.contains('button', 'Logg inn') + }) + + it('disables the login button when no input', () => { + cy.contains('button', 'Logg inn').should('be.disabled') + }) + + it('disables the login button when only username is input', () => { + cy.get('input[name=username]').type('test') + cy.contains('button', 'Logg inn').should('be.disabled') + }) + + it('disables the login button when only password is input', () => { + cy.get('input[name=password]').type('test') + cy.contains('button', 'Logg inn').should('be.disabled') + }) + + it('enables the login button when both username and password is input', () => { + fullInput() + cy.contains('button', 'Logg inn').should('not.be.disabled') + }) + + it('pushes the the user to root page on successful login', () => { + cy.intercept('POST', 'http://localhost:8080/auth/login', { + body: { + accessToken: 'fakeToken', + refreshToken: 'fakeToken' + } + }).as('login') + + fullInput() + + cy.get('button[name=submit]').click() + + cy.wait('@login') + + cy.url().should('include', '/') + }) +}) diff --git a/cypress/e2e/register.cy.ts b/cypress/e2e/register.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..44724e1d00affa893cc9760dcf64019c82f7bf77 --- /dev/null +++ b/cypress/e2e/register.cy.ts @@ -0,0 +1,45 @@ +describe('Register', () => { + beforeEach(() => { + cy.visit('/registrer') + cy.contains('h3', 'Registrer deg').click() + }) + + function fullInput() { + cy.get('input[name="firstName"]').type('firstName') + cy.get('input[name="lastName"]').type('lastName') + cy.get('input[name="email"]').type('email@test.work') + cy.get('input[name="username"]').type('username') + cy.get('input[name="password"]').type('Password123!') + cy.get('input[name="confirm"]').type('Password123!') + } + + it('visits the register page when clicked', () => { + cy.contains('button[name="submit"]', 'Registrer deg') + }) + + it('disables the login button when no input', () => { + cy.get('button[name="submit"]').should('be.disabled') + }) + + it('enable the login button when all inputs are filled and l', () => { + fullInput() + + cy.get('button[name="submit"]').should('not.be.disabled') + }) + + it('pushes the user to the root page on successful register', () => { + cy.intercept('POST', 'http://localhost:8080/auth/register', { + body: { + accessToken: 'fakeToken', + refreshToken: 'fakeToken' + } + }).as('register') + + fullInput() + + cy.get('button[name="submit"]').click() + + cy.wait('@register') + cy.url().should('include', '/') + }) +}) diff --git a/cypress/e2e/tsconfig.json b/cypress/e2e/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..37748feb7f2d4d4ae6959e38d27ad35d96b7d1d3 --- /dev/null +++ b/cypress/e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["./**/*", "../support/**/*"], + "compilerOptions": { + "isolatedModules": false, + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + } +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000000000000000000000000000000000000..02e4254378e9785f013be7cc8d94a8229dcbcbb7 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b7bb8e2584cf20c9f578c69dada5f6024b4b042 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,39 @@ +/// <reference types="cypress" /> +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable<void> +// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element> +// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element> +// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element> +// } +// } +// } + +export {} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..d68db96df2697e0835f5c490db0c2cc81673f407 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..11f02fe2a0061d6e6e1f271b21da95423b448b32 --- /dev/null +++ b/env.d.ts @@ -0,0 +1 @@ +/// <reference types="vite/client" /> diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..39387a42927fef2354002b09708ac4f557e65175 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <link rel="icon" href="/favicon.png"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>SpareSti</title> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.ts"></script> + </body> +</html> diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..cdbcb6f750f1327fdc33e3e3d5fc21a2a8970dbd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7569 @@ +{ + "name": "idatt2106_2024_02_frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "idatt2106_2024_02_frontend", + "version": "0.0.0", + "dependencies": { + "animejs": "^3.2.2", + "canvas-confetti": "^1.9.2", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.1", + "vue3-flip-countdown": "^0.1.6", + "vuedraggable": "^4.1.0" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/animejs": "^3.1.12", + "@types/canvas-confetti": "^1.6.4", + "@types/jsdom": "^21.1.6", + "@types/node": "^20.12.5", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vitest/coverage-v8": "^1.5.0", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.5", + "@vue/tsconfig": "^0.5.1", + "autoprefixer": "^10.4.19", + "axios-mock-adapter": "^1.22.0", + "cypress": "^13.7.3", + "eslint": "^8.57.0", + "eslint-plugin-cypress": "^2.15.1", + "eslint-plugin-vue": "^9.23.0", + "jsdom": "^24.0.0", + "npm-run-all2": "^6.1.2", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "start-server-and-test": "^2.0.3", + "tailwindcss": "^3.4.3", + "typescript": "~5.4.0", + "vite": "^5.2.8", + "vitest": "^1.4.0", + "vue-tsc": "^2.0.11" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "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, + "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, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "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, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", + "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", + "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", + "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", + "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", + "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", + "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", + "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", + "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", + "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", + "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", + "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", + "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", + "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", + "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", + "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", + "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz", + "integrity": "sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==", + "dev": true + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true + }, + "node_modules/@types/animejs": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@types/animejs/-/animejs-3.1.12.tgz", + "integrity": "sha512-fpdH+ZtlO0kqjTOqRaBdsEmvpRNOayI8k4EVkEtitL5l6wducDOXk0rgQgfZqWf/ZX9DzXrHf257S5i9xTcISQ==", + "dev": true + }, + "node_modules/@types/canvas-confetti": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.6.4.tgz", + "integrity": "sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/jsdom": { + "version": "21.1.6", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.6.tgz", + "integrity": "sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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 + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.7.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", + "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.1.tgz", + "integrity": "sha512-Zx+dYEDcZg+44ksjIWvWosIGlPLJB1PPpN3O8+Xrh/1qa7WSFA6Y8H7lsZJTYrxu4G2unk9tvP5TgjIGDliF1w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.5.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.1.tgz", + "integrity": "sha512-w3Bn+VUMqku+oWmxvPhTE86uMTbfmBl35aGaIPlwVW7Q89ZREC/icfo2HBsEZ3AAW6YR9lObfZKPEzstw9tJOQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.5.1", + "@vitest/utils": "1.5.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.1.tgz", + "integrity": "sha512-mt372zsz0vFR7L1xF/ert4t+teD66oSuXoTyaZbl0eJgilvyzCKP1tJ21gVa8cDklkBOM3DLnkE1ljj/BskyEw==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.5.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.1.tgz", + "integrity": "sha512-h/1SGaZYXmjn6hULRBOlqam2z4oTlEe6WwARRzLErAPBqljAs6eX7tfdyN0K+MpipIwSZ5sZsubDWkCPAiVXZQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.1.tgz", + "integrity": "sha512-vsqczk6uPJjmPLy6AEtqfbFqgLYcGBe9BTY+XL8L6y8vrGOhyE23CJN9P/hPimKXnScbqiZ/r/UtUSOQ2jIDGg==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.1.tgz", + "integrity": "sha512-92pE17bBXUxA0Y7goPcvnATMCuq4NQLOmqsG0e2BtzRi7KLwZB5jpiELi/8ybY8IQNWemKjSD5rMoO7xTdv8ug==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.10.tgz", + "integrity": "sha512-njVJLtpu0zMvDaEk7K5q4BRpOgbyEUljU++un9TfJoJNhxG0z/hWwpwgTRImO42EKvwIxF3XUzeMk+qatAFy7Q==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.2.0-alpha.10" + } + }, + "node_modules/@volar/source-map": { + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.10.tgz", + "integrity": "sha512-nrdWApVkP5cksAnDEyy1JD9rKdwOJsEq1B+seWO4vNXmZNcxQQCx4DULLBvKt7AzRUAQiAuw5aQkb9RBaSqdVA==", + "dev": true, + "dependencies": { + "muggle-string": "^0.4.0" + } + }, + "node_modules/@volar/typescript": { + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.10.tgz", + "integrity": "sha512-GCa0vTVVdA9ULUsu2Rx7jwsIuyZQPvPVT9o3NrANTbYv+523Ao1gv3glC5vzNSDPM6bUl37r94HbCj7KINQr+g==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.2.0-alpha.10", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.25.tgz", + "integrity": "sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==", + "dependencies": { + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz", + "integrity": "sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==", + "dependencies": { + "@vue/compiler-core": "3.4.25", + "@vue/shared": "3.4.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz", + "integrity": "sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ==", + "dependencies": { + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.25", + "@vue/compiler-dom": "3.4.25", + "@vue/compiler-ssr": "3.4.25", + "@vue/shared": "3.4.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz", + "integrity": "sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ==", + "dependencies": { + "@vue/compiler-dom": "3.4.25", + "@vue/shared": "3.4.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "dev": true, + "dependencies": { + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": ">=4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.14.tgz", + "integrity": "sha512-3q8mHSNcGTR7sfp2X6jZdcb4yt8AjBXAfKk0qkZIh7GAJxOnoZ10h5HToZglw4ToFvAnq+xu/Z2FFbglh9Icag==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.2.0-alpha.10", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.25.tgz", + "integrity": "sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ==", + "dependencies": { + "@vue/shared": "3.4.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.25.tgz", + "integrity": "sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q==", + "dependencies": { + "@vue/reactivity": "3.4.25", + "@vue/shared": "3.4.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz", + "integrity": "sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA==", + "dependencies": { + "@vue/runtime-core": "3.4.25", + "@vue/shared": "3.4.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.25.tgz", + "integrity": "sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ==", + "dependencies": { + "@vue/compiler-ssr": "3.4.25", + "@vue/shared": "3.4.25" + }, + "peerDependencies": { + "vue": "3.4.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.25.tgz", + "integrity": "sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz", + "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==", + "dev": true, + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "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, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "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/animejs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz", + "integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "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" + } + ] + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "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 + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "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 + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/canvas-confetti": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.2.tgz", + "integrity": "sha512-6Xi7aHHzKwxZsem4mCKoqP6YwUG3HamaHHAlz1hTNQPCqXhARFpSXnkC9TWlahHY5CG6hSL5XexNjxK8irVErg==", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "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/chalk/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", + "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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 + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "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 + }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "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, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cypress": { + "version": "13.8.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.1.tgz", + "integrity": "sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "dev": true + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.747", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.747.tgz", + "integrity": "sha512-+FnSWZIAvFHbsNVmUxhEqWiaOiPMcfum1GQzlWCg/wLigVtshOsjXHyEFfmt6cFK6+HkS3QOJBv6/3OPumbBfw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.2.tgz", + "integrity": "sha512-CtcFEQTDKyftpI22FVGpx8bkpKyYXBlNge6zSo0pl5/qJvBAnzaD76Vu2AsP16d6mTj478Ldn2mhgrWV+Xr0vQ==", + "dev": true, + "dependencies": { + "globals": "^13.20.0" + }, + "peerDependencies": { + "eslint": ">= 3.2.1" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.25.0.tgz", + "integrity": "sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.2", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "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, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.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 + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "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, + "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 + }, + "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 + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "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, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "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" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.0.tgz", + "integrity": "sha512-9qcrTyoBmFZRNHeVP4edKqIUEgFzq7MHvTNSDuHSqkpOPtiBkgNgcmTSqmiw1kw9tdKaiddvIDv/eCJDxmqWCA==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/jsdom": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", + "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "dev": true, + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.7", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.16.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "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 + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "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 + }, + "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 + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "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, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "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 + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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 + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.1.2.tgz", + "integrity": "sha512-WwwnS8Ft+RpXve6T2EIEVpFLSqN+ORHRvgNk3H9N62SZXjmzKoRhMFg3I17TK3oMaAEr+XFbRirWS2Fn3BCPSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.3", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^3.0.2", + "shell-quote": "^1.7.3" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0", + "npm": ">= 8" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz", + "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "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, + "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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", + "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.6.1", + "pathe": "^1.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "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/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "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" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "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==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", + "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.16.4", + "@rollup/rollup-android-arm64": "4.16.4", + "@rollup/rollup-darwin-arm64": "4.16.4", + "@rollup/rollup-darwin-x64": "4.16.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", + "@rollup/rollup-linux-arm-musleabihf": "4.16.4", + "@rollup/rollup-linux-arm64-gnu": "4.16.4", + "@rollup/rollup-linux-arm64-musl": "4.16.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", + "@rollup/rollup-linux-riscv64-gnu": "4.16.4", + "@rollup/rollup-linux-s390x-gnu": "4.16.4", + "@rollup/rollup-linux-x64-gnu": "4.16.4", + "@rollup/rollup-linux-x64-musl": "4.16.4", + "@rollup/rollup-win32-arm64-msvc": "4.16.4", + "@rollup/rollup-win32-ia32-msvc": "4.16.4", + "@rollup/rollup-win32-x64-msvc": "4.16.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/start-server-and-test": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.3.tgz", + "integrity": "sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.3.4", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "7.2.0" + }, + "bin": { + "server-test": "src/bin/start.js", + "start-server-and-test": "src/bin/start.js", + "start-test": "src/bin/start.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/start-server-and-test/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/start-server-and-test/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/start-server-and-test/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "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/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==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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, + "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==", + "dev": true, + "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 + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", + "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.1.tgz", + "integrity": "sha512-HNpfV7BrAsjkYVNWIcPleJwvJmydJqqJRrRbpoQ/U7QDwJKyEzNa4g5aYg8MjXJyKsk29IUCcMLFRcsEvqUIsA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.1.tgz", + "integrity": "sha512-3GvBMpoRnUNbZRX1L3mJCv3Ou3NAobb4dM48y8k9ZGwDofePpclTOyO+lqJFKSQpubH1V8tEcAEw/Y3mJKGJQQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.5.1", + "@vitest/runner": "1.5.1", + "@vitest/snapshot": "1.5.1", + "@vitest/spy": "1.5.1", + "@vitest/utils": "1.5.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.5.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.5.1", + "@vitest/ui": "1.5.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vue": { + "version": "3.4.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.25.tgz", + "integrity": "sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg==", + "dependencies": { + "@vue/compiler-dom": "3.4.25", + "@vue/compiler-sfc": "3.4.25", + "@vue/runtime-dom": "3.4.25", + "@vue/server-renderer": "3.4.25", + "@vue/shared": "3.4.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.14.tgz", + "integrity": "sha512-DInfgOyXlMyliyqAAD9frK28tTfch0+tMi4qoWJcZlRxUf+NFAtraJBnAsKLep+FOyLMiajkhfyEb3xLK08i7w==", + "dev": true + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", + "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", + "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.14.tgz", + "integrity": "sha512-DgAO3U1cnCHOUO7yB35LENbkapeRsBZ7Ugq5hGz/QOHny0+1VQN8eSwSBjYbjLVPfvfw6EY7sNPjbuHHUhckcg==", + "dev": true, + "dependencies": { + "@volar/typescript": "2.2.0-alpha.10", + "@vue/language-core": "2.0.14", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/vue3-flip-countdown": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/vue3-flip-countdown/-/vue3-flip-countdown-0.1.6.tgz", + "integrity": "sha512-RRz+iZ7Zvr1U9mrZRya7I5815jboDyRJz9vzgILq8ZCc2fQ6SxZPYwOr3pD5oWCDBprAEsPF9x4fsTtEitSmXw==", + "dependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3d1c8853a603f9b7c4fb8d0e0f161bdd8d57c335 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "idatt2106_2024_02_frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "test:unit": "vitest", + "test:e2e": "start-server-and-test 'vite dev --port 4173' :4173 'cypress run --e2e'", + "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "test:coverage": "vitest run --coverage --environment jsdom", + "format": "prettier --write src/", + "format-test": "prettier --check src/" + }, + "dependencies": { + "animejs": "^3.2.2", + "canvas-confetti": "^1.9.2", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.1", + "vue3-flip-countdown": "^0.1.6", + "vuedraggable": "^4.1.0" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/animejs": "^3.1.12", + "@types/canvas-confetti": "^1.6.4", + "@types/jsdom": "^21.1.6", + "@types/node": "^20.12.5", + "@typescript-eslint/eslint-plugin": "^7.7.0", + "@typescript-eslint/parser": "^7.7.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vitest/coverage-v8": "^1.5.0", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.5", + "@vue/tsconfig": "^0.5.1", + "autoprefixer": "^10.4.19", + "axios-mock-adapter": "^1.22.0", + "cypress": "^13.7.3", + "eslint": "^8.57.0", + "eslint-plugin-cypress": "^2.15.1", + "eslint-plugin-vue": "^9.23.0", + "jsdom": "^24.0.0", + "npm-run-all2": "^6.1.2", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "start-server-and-test": "^2.0.3", + "tailwindcss": "^3.4.3", + "typescript": "~5.4.0", + "vite": "^5.2.8", + "vitest": "^1.4.0", + "vue-tsc": "^2.0.11" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/avatar1.png b/public/avatar1.png new file mode 100644 index 0000000000000000000000000000000000000000..0f9f57b5faea8f110ee1e080c9eec6627425c1cc Binary files /dev/null and b/public/avatar1.png differ diff --git a/public/avatar2.png b/public/avatar2.png new file mode 100644 index 0000000000000000000000000000000000000000..911b457e282e6a0fc3254b3c75d11268a99549a0 Binary files /dev/null and b/public/avatar2.png differ diff --git a/public/avatar3.png b/public/avatar3.png new file mode 100644 index 0000000000000000000000000000000000000000..9d3cc3ede8d28baa1f2ca2f7dff2a6b0280b619b Binary files /dev/null and b/public/avatar3.png differ diff --git a/public/avatar4.png b/public/avatar4.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd29cefa858ba8b4fa190eda94460b0ed8758d6 Binary files /dev/null and b/public/avatar4.png differ diff --git a/public/avatar5.png b/public/avatar5.png new file mode 100644 index 0000000000000000000000000000000000000000..dfd4a3e4bb5cc039d5c7761a5ed10554eaeea685 Binary files /dev/null and b/public/avatar5.png differ diff --git a/public/avatar6.png b/public/avatar6.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d7e19307e16f04afeda356e6833e8eebeab4a0 Binary files /dev/null and b/public/avatar6.png differ diff --git a/public/avatar7.png b/public/avatar7.png new file mode 100644 index 0000000000000000000000000000000000000000..a58f2ac0a551498b617991602a24d31fc92408b6 Binary files /dev/null and b/public/avatar7.png differ diff --git a/public/avatar8.png b/public/avatar8.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b0f8fb11fdfdd8061380a02ae70df30c0de379 Binary files /dev/null and b/public/avatar8.png differ diff --git a/public/avatar9.png b/public/avatar9.png new file mode 100644 index 0000000000000000000000000000000000000000..1268037d1bfe19964d14ad784a951d1bca3fda34 Binary files /dev/null and b/public/avatar9.png differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..fac8de3bf283197bf71ebb8d839b12a5b34263ac Binary files /dev/null and b/public/favicon.png differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..77cb1f689169df792ed4d3f3a3431e247d4e29f0 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,174 @@ +<script setup lang="ts"> +import NavBarComponent from '@/components/NavBarComponent.vue' +import { RouterView, useRoute } from 'vue-router' +import { computed } from 'vue' +import HelpComponent from '@/components/HelpComponent.vue' + +const route = useRoute() + +const showNavBar = computed(() => { + return !( + route.path == '/' || + route.path == '/registrer' || + route.path.startsWith('/logginn') || + route.path == '/forgotPassword' || + route.path.startsWith('/konfigurasjon') + ) +}) + +const backgroundImageStyle = computed(() => { + if (showSti.value) { + return { + backgroundImage: "url('src/assets/sti.png')" + } + } else { + return { + backgroundImage: 'none' + } + } +}) + +const showSti = computed(() => { + return !( + route.path == '/' || + route.path == '/registrer' || + route.path == '/logginn' || + route.path == '/forgotPassword' || + route.path.startsWith('/konfigurasjon') || + route.path == '/hjem' || + route.path == '/profil' || + route.path.startsWith('/loggin') + ) +}) + +const showHelp = computed(() => { + return !( + route.path == '/' || + route.path == '/registrer' || + route.path == '/logginn' || + route.path == '/forgotPassword' || + route.path.startsWith('/konfigurasjon') + ) +}) + +const helpMessages = computed(() => { + let messages = [] + + if (route.path == '/hjem') { + messages.push('Heisann, jeg er Spare!') + messages.push('Jeg skal hjelpe deg med Ã¥ spare penger 💵') + messages.push('Du kan legge til sparemÃ¥l og spareutfordringer!') + messages.push('Sammen kan vi spare penger og nÃ¥ dine mÃ¥l! 🚀') + } else if (route.path == '/profil') { + messages.push('Du har kommet til profilen din ðŸ·') + messages.push('Her kan du se en oversikt over dine profilinstillinger âš™ï¸') + messages.push('Du kan ogsÃ¥ se dine fullførte sparemÃ¥l og utfordringer!') + messages.push('Du kan redigere profilen din ved Ã¥ trykke pÃ¥ "Rediger bruker" 💎') + } else if (route.path == '/profil/rediger') { + messages.push('ï¸Her kan du se og redigere dine profil-instillinger 🪄') + messages.push('For Ã¥ lagre endringene dine, trykk pÃ¥ "Lagre endringer" i høyre hjørne') + messages.push( + 'Husk at passordet ditt mÃ¥ være minst 8 tegn langt, og inneholde minst ett tall, en stor bokstav, en liten bokstav, og et spesialtegn' + ) + } else if (route.path == '/sparemaal') { + messages.push('Du har kommet til sparemÃ¥lene dine 🎯') + messages.push( + 'Et sparemÃ¥l kan være noe du ønsker Ã¥ spare penger til, for eksempel en ferie ðŸ–ï¸ eller en ny sykkel 🚴ðŸ»' + ) + messages.push( + 'Du kan lage nye sparemÃ¥l ved Ã¥ trykke pÃ¥ knappen "Opprett et nytt sparemÃ¥l".' + ) + messages.push( + 'Du kan ogsÃ¥ endre rekkefølgen pÃ¥ sparemÃ¥lene dine ved Ã¥ trykke pÃ¥ "Endre rekkefølge".' + ) + messages.push( + 'NÃ¥r du har fullført et sparemÃ¥l, vil det dukke opp under "Fullførte sparemÃ¥l".' + ) + messages.push('Lykke til med mÃ¥lene dine! 🎀') + } else if (route.path == '/spareutfordringer') { + messages.push('Du har kommet til spareutfordringene dine 💰') + messages.push( + 'En spareutfordring er en mÃ¥te Ã¥ bli kvitt dÃ¥rlige vaner, samtidig spare penger for Ã¥ nÃ¥ dine mÃ¥l ✨' + ) + messages.push('Du kan opprette en ny utfordring ved Ã¥ trykke pÃ¥ "Opprett en ny utfordring"') + messages.push( + 'Du kan ogsÃ¥ endre rekkefølgen pÃ¥ utfordringene dine ved Ã¥ trykke pÃ¥ "Endre rekkefølge".' + ) + messages.push( + 'NÃ¥r du har fullført en utfordring, vil den dukke opp under "Fullførte utfordringer".' + ) + messages.push('Lykke til med utfordringene dine ðŸ†') + } else if (route.path.startsWith('/sparemaal/oversikt')) { + messages.push('Her har du en oversikt over sparemÃ¥let ditt 🗽') + messages.push('Du kan redigere mÃ¥let, markere det som ferdig eller slette det') + messages.push( + 'Du kan ogsÃ¥ se hvor mye du har spart av mÃ¥let ditt, og hvor mye du har igjen' + ) + } else if (route.path.startsWith('/spareutfordringer/oversikt')) { + messages.push('Her har du en oversikt over spareutfordringen din ðŸ”ï¸') + messages.push('Du kan redigere utfordringen, markere det som ferdig eller slette det') + messages.push( + 'Du kan ogsÃ¥ se hvor mye du har spart av utfordringen din, og hvor mye du har igjen' + ) + } else if (route.path.startsWith('/sparemaal/rediger/ny')) { + messages.push('Her kan du opprette et nytt sparemÃ¥l 🌸') + messages.push( + 'Tittel er navnet pÃ¥ sparemÃ¥let, og beskrivelse er en kort forklaring pÃ¥ hva sparemÃ¥let gÃ¥r ut pÃ¥.' + ) + messages.push( + 'Kroner spart er hvor mye du har spart til nÃ¥, og av mÃ¥lbeløp er hvor mye du ønsker Ã¥ spare.' + ) + messages.push('Forfallsdato er datoen du ønsker Ã¥ ha nÃ¥dd sparemÃ¥let ditt.') + messages.push('Lykke til med sparingen! 🌴') + } else if (route.path.startsWith('/spareutfordringer/ny')) { + messages.push('Her kan du opprette en ny utfordring ☕ï¸') + messages.push( + 'Tittel er navnet pÃ¥ utfordringen, og beskrivelse er en kort forklaring pÃ¥ hva utfordringen gÃ¥r ut pÃ¥.' + ) + messages.push( + 'Pris per sparing er hvor mye du sparer hver gang du sparer, og antall sparinger er hvor mange ganger du har spart.' + ) + messages.push( + 'Av mÃ¥lbeløp er hvor mye du har spart til nÃ¥, og forfallsdato er nÃ¥r utfordringen skal være fullført.' + ) + messages.push('Du kan selvsagt endre pÃ¥ dette senere!') + messages.push('Lykke til med utfordringen din! 🎉') + } else { + messages.push('Hei! Jeg er Spare ðŸ·') + messages.push('Jeg er her for Ã¥ hjelpe deg med sparingen din 💰') + messages.push('Kom igang nÃ¥ 🔥') + } + return messages +}) +</script> + +<template> + <HelpComponent v-if="showHelp" :speech="helpMessages" /> + <div + class="min-h-screen bg-left-bottom bg-phone md:bg-pc bg-no-repeat" + :style="backgroundImageStyle" + > + <NavBarComponent v-if="showNavBar" /> + + <main class="mb-10"> + <RouterView /> + </main> + </div> +</template> + +<style> +nav { + display: flex; + justify-content: center; + gap: 1rem; + margin: 1rem 0; +} + +nav a.router-link-exact-active { + color: var(--color-text); +} + +nav a.router-link-exact-active:hover { + background-color: transparent; +} +</style> diff --git a/src/assets/archerSpare.gif b/src/assets/archerSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef0e3fa6fd0fcdd700e0e8cb233c4195b90b1355 Binary files /dev/null and b/src/assets/archerSpare.gif differ diff --git a/src/assets/backgroundSavingsPath.png b/src/assets/backgroundSavingsPath.png new file mode 100644 index 0000000000000000000000000000000000000000..43cfde5161f862f862f14f8612077b10bbcfb4e2 Binary files /dev/null and b/src/assets/backgroundSavingsPath.png differ diff --git a/src/assets/bakgrunn.png b/src/assets/bakgrunn.png new file mode 100644 index 0000000000000000000000000000000000000000..7c002fbb47bcef42217ac561c68abca83362d828 Binary files /dev/null and b/src/assets/bakgrunn.png differ diff --git a/src/assets/base.css b/src/assets/base.css new file mode 100644 index 0000000000000000000000000000000000000000..db174147f881bd99eff24edb10a0ec0322d2a2c6 --- /dev/null +++ b/src/assets/base.css @@ -0,0 +1,66 @@ +@import url('https://fonts.googleapis.com/css2?family=Karla:wght@400;700&display=swap'); + +:root { + --black: #363739; + --white: #ffffff; + --grey: #cbcbcb; + --light-grey: #f2f2f2; + --green: #95e35d; + --red: #ef9691; + --light-green: #b3f385; + + --bright: #f7da7c; + + --accent1: #f4bab9; + --accent2: #95e35d; + --accent3: #ef9691; +} + +:root { + --color-background: var(--white); + --color-text: var(--black); + --color-button: var(--green); + --color-button-disabled: var(--grey); + --color-nav-hover: var(--light-grey); + --color-button-edit: var(--grey); + --color-button-hover: var(--light-green); + --color-button-danger: var(--accent3); + --color-button-danger-hover: var(--accent1); + + --color-link: var(--accent3); + --color-border: var(--black); + + --section-gap: 160px; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; /* Firefox */ +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: 'Karla', sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/assets/bioAuthFace.png b/src/assets/bioAuthFace.png new file mode 100644 index 0000000000000000000000000000000000000000..b03bb5d400029bcbc6d7bea9f5657da31e3e63a0 Binary files /dev/null and b/src/assets/bioAuthFace.png differ diff --git a/src/assets/bioAuthTouch.png b/src/assets/bioAuthTouch.png new file mode 100644 index 0000000000000000000000000000000000000000..363627b9c744b2a8dcabc9d2a69485d055045f43 Binary files /dev/null and b/src/assets/bioAuthTouch.png differ diff --git a/src/assets/boatSpare.gif b/src/assets/boatSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0aaaa991168e07de3f56e5b129ae082094499f3 Binary files /dev/null and b/src/assets/boatSpare.gif differ diff --git a/src/assets/borderImage.png b/src/assets/borderImage.png new file mode 100644 index 0000000000000000000000000000000000000000..1c0ac6cdc5e2ef366679ecd0fb85f51c73ccc65f Binary files /dev/null and b/src/assets/borderImage.png differ diff --git a/src/assets/coffee.png b/src/assets/coffee.png new file mode 100644 index 0000000000000000000000000000000000000000..425ef51ab494a199c6fa075adce42e48cf35a44f Binary files /dev/null and b/src/assets/coffee.png differ diff --git a/src/assets/completed.png b/src/assets/completed.png new file mode 100644 index 0000000000000000000000000000000000000000..943dca5a129d1c703c037cfcceac092276de40d4 Binary files /dev/null and b/src/assets/completed.png differ diff --git a/src/assets/cowboySpare.gif b/src/assets/cowboySpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff0d6f0b3d077811bd1871ad7c7889fd11a7164c Binary files /dev/null and b/src/assets/cowboySpare.gif differ diff --git a/src/assets/farmerSpare.gif b/src/assets/farmerSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f471d607f720952491c22b815486d4b58425b76 Binary files /dev/null and b/src/assets/farmerSpare.gif differ diff --git a/src/assets/finishLine.png b/src/assets/finishLine.png new file mode 100644 index 0000000000000000000000000000000000000000..9394bd3c85fed058ab7862e207667d0c9df00ff7 Binary files /dev/null and b/src/assets/finishLine.png differ diff --git a/src/assets/finishline2.png b/src/assets/finishline2.png new file mode 100644 index 0000000000000000000000000000000000000000..251d099d41b7887bde06385be18e548d727d0ec6 Binary files /dev/null and b/src/assets/finishline2.png differ diff --git a/src/assets/flower.png b/src/assets/flower.png new file mode 100644 index 0000000000000000000000000000000000000000..c7481c94d5c7f137f0bfaad68fbf20b4d9396f9c Binary files /dev/null and b/src/assets/flower.png differ diff --git a/src/assets/frozenStreak.png b/src/assets/frozenStreak.png new file mode 100644 index 0000000000000000000000000000000000000000..9d60b296b4432f7ea2f469b0309c1ebae9b5699f Binary files /dev/null and b/src/assets/frozenStreak.png differ diff --git a/src/assets/gaming.png b/src/assets/gaming.png new file mode 100644 index 0000000000000000000000000000000000000000..b02ffa01a3b3a7267925a1eeb8084ac70c2f132c Binary files /dev/null and b/src/assets/gaming.png differ diff --git a/src/assets/golfSpare.gif b/src/assets/golfSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..b67cad84e2dc45e7b95d31b665eae3af1ebc3eb9 Binary files /dev/null and b/src/assets/golfSpare.gif differ diff --git a/src/assets/head.png b/src/assets/head.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6edb76b40cb4f25b9fa7eef99734d0d7bb90d3 Binary files /dev/null and b/src/assets/head.png differ diff --git a/src/assets/hjelp.png b/src/assets/hjelp.png new file mode 100644 index 0000000000000000000000000000000000000000..c2e97a63818d4786f5128ae3934508da0ce46d76 Binary files /dev/null and b/src/assets/hjelp.png differ diff --git a/src/assets/hotAirBalloonSpare.gif b/src/assets/hotAirBalloonSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..78f999f0295efdd779e47f0e74cb33bcd1e8a1ba Binary files /dev/null and b/src/assets/hotAirBalloonSpare.gif differ diff --git a/src/assets/infoIcon.png b/src/assets/infoIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..1aef35a260e16d428c804b38c584651fe42a1487 Binary files /dev/null and b/src/assets/infoIcon.png differ diff --git a/src/assets/litt.png b/src/assets/litt.png new file mode 100644 index 0000000000000000000000000000000000000000..6add972367e016606dbea3a785abd0cc8b398134 Binary files /dev/null and b/src/assets/litt.png differ diff --git a/src/assets/lock.png b/src/assets/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..d4598c4c7e310202edd3b81af5edc13fcecdaf76 Binary files /dev/null and b/src/assets/lock.png differ diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000000000000000000000000000000000000..6aea95d7ffc247b4457e16f54cacbf0462f3af7a --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,110 @@ +@import './base.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +#app { + margin: 0 auto auto auto; + width: 100%; + font-weight: normal; + + display: flex; + flex-direction: column; +} + +h1, +h2, +h3 { + font-weight: bold; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem; + margin-bottom: 1rem; +} + +h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +button.primary { + background-color: transparent; + border-color: var(--color-button); + color: var(--color-text); + padding: 0.2rem 1rem; + font-weight: bold; + border-radius: 1rem; + border-width: 2px; + cursor: pointer; + transition: 0.4s; +} + +button.primary:disabled { + background-color: var(--color-button-disabled); + border-color: var(--color-button-disabled); + cursor: not-allowed; +} +button.primary:hover { + border-color: var(--color-button-hover); + transition: 0.7s; +} + +button.secondary { + border-color: var(--color-button-edit); +} + +button.secondary:hover { + border-color: black; + transition: 0.7s; +} + +button.danger { + background-color: var(--color-button-danger); + border-color: transparent; +} + +button.danger:hover { + background-color: var(--color-button-danger-hover); + border-color: var(--color-button-danger); + transition: 0.7s; +} + +button.logout { + background-color: var(--color-button); +} + +a { + text-decoration: none; + color: var(--color-text); + font-weight: bold; + transition: 0.4s; +} + +input { + border: 1px solid var(--color-border); + padding: 0.5rem 1rem; + border-radius: 2rem; +} + +textarea { + border: 1px solid var(--color-border); + padding: 0.5rem; + border-radius: 1rem; +} + +@media (hover: hover) { + a:hover { + background-color: var(--color-nav-hover); + transition: 0.5s; + text-decoration: none; + padding: 3px 3px; + border-radius: 8px; + } +} diff --git a/src/assets/mat.png b/src/assets/mat.png new file mode 100644 index 0000000000000000000000000000000000000000..4c1dff6b49b35761785ab5eb0d1ba6997925995c Binary files /dev/null and b/src/assets/mat.png differ diff --git a/src/assets/nose.png b/src/assets/nose.png new file mode 100644 index 0000000000000000000000000000000000000000..2845ed5555b18c3c03e97d1c193dad506bc63852 Binary files /dev/null and b/src/assets/nose.png differ diff --git a/src/assets/passe.png b/src/assets/passe.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce43e9f28288fe4b8d8a2c9372d9cf8e8269fb7 Binary files /dev/null and b/src/assets/passe.png differ diff --git a/src/assets/pending.png b/src/assets/pending.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb5be6856258672869a652316bc9363cb0e236e Binary files /dev/null and b/src/assets/pending.png differ diff --git a/src/assets/penger.png b/src/assets/penger.png new file mode 100644 index 0000000000000000000000000000000000000000..9588e23b80536441ee8f2703d4459c8557ff361a Binary files /dev/null and b/src/assets/penger.png differ diff --git a/src/assets/pengesekkStreak.png b/src/assets/pengesekkStreak.png new file mode 100644 index 0000000000000000000000000000000000000000..54565d0fd1ad2795edeaab7eeb614e538d618dc2 Binary files /dev/null and b/src/assets/pengesekkStreak.png differ diff --git a/src/assets/pig.png b/src/assets/pig.png new file mode 100644 index 0000000000000000000000000000000000000000..0a10f0f9dabc23d2d2db0654c465448024bd9a66 Binary files /dev/null and b/src/assets/pig.png differ diff --git a/src/assets/pigSteps.png b/src/assets/pigSteps.png new file mode 100644 index 0000000000000000000000000000000000000000..45309466a7ec3d8b163f5ca4e74671750fc628f2 Binary files /dev/null and b/src/assets/pigSteps.png differ diff --git a/src/assets/sleepingSpare.gif b/src/assets/sleepingSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..3ea7d3dcc3f1efaa6c9eb90fa75f70bd9b8856ff Binary files /dev/null and b/src/assets/sleepingSpare.gif differ diff --git a/src/assets/snacks.png b/src/assets/snacks.png new file mode 100644 index 0000000000000000000000000000000000000000..45ad39383c62ca25334634bd5747bccab7a4dac3 Binary files /dev/null and b/src/assets/snacks.png differ diff --git a/src/assets/spare.png b/src/assets/spare.png new file mode 100644 index 0000000000000000000000000000000000000000..fe48be94be6ede6652ce0a94d50f581710d7ef07 Binary files /dev/null and b/src/assets/spare.png differ diff --git a/src/assets/spareSti.png b/src/assets/spareSti.png new file mode 100644 index 0000000000000000000000000000000000000000..90765d5805679e487b8bbdce11d620e7a7a137fb Binary files /dev/null and b/src/assets/spareSti.png differ diff --git a/src/assets/spare_og_sti.png b/src/assets/spare_og_sti.png new file mode 100644 index 0000000000000000000000000000000000000000..9716e1ab8e85e9302c7d616e17f486d610855ed3 Binary files /dev/null and b/src/assets/spare_og_sti.png differ diff --git a/src/assets/star.png b/src/assets/star.png new file mode 100644 index 0000000000000000000000000000000000000000..c30b55ac723af14e961770418addce0b5ef8d6e3 Binary files /dev/null and b/src/assets/star.png differ diff --git a/src/assets/start-sign.png b/src/assets/start-sign.png new file mode 100644 index 0000000000000000000000000000000000000000..ece9e3aab84d9a320ed803d9db25b04723fcfa06 Binary files /dev/null and b/src/assets/start-sign.png differ diff --git a/src/assets/start.png b/src/assets/start.png new file mode 100644 index 0000000000000000000000000000000000000000..16b9d574fcf47a1b740dc177c68b35bc45b637cf Binary files /dev/null and b/src/assets/start.png differ diff --git a/src/assets/start_page/fly.png b/src/assets/start_page/fly.png new file mode 100644 index 0000000000000000000000000000000000000000..7a0e912c5f15b835f5a84e0d23a024889271d0f8 Binary files /dev/null and b/src/assets/start_page/fly.png differ diff --git a/src/assets/start_page/skyer.png b/src/assets/start_page/skyer.png new file mode 100644 index 0000000000000000000000000000000000000000..3519c8e0487cdc271167ee1ce3fb3860641e6e23 Binary files /dev/null and b/src/assets/start_page/skyer.png differ diff --git a/src/assets/start_page/sti.png b/src/assets/start_page/sti.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e5dc7e9fb2fc9eddffb78da135fab557846433 Binary files /dev/null and b/src/assets/start_page/sti.png differ diff --git a/src/assets/start_page/strand.png b/src/assets/start_page/strand.png new file mode 100644 index 0000000000000000000000000000000000000000..597dc0f8398b5d408cae890bb4a8b45f522879f2 Binary files /dev/null and b/src/assets/start_page/strand.png differ diff --git a/src/assets/sti.png b/src/assets/sti.png new file mode 100644 index 0000000000000000000000000000000000000000..2076cc2d098f14608f4ba0f7d758f816471d5377 Binary files /dev/null and b/src/assets/sti.png differ diff --git a/src/assets/store.png b/src/assets/store.png new file mode 100644 index 0000000000000000000000000000000000000000..54bfd38639ed5e7b57f220bf7330750b08e74544 Binary files /dev/null and b/src/assets/store.png differ diff --git a/src/assets/streak.png b/src/assets/streak.png new file mode 100644 index 0000000000000000000000000000000000000000..16a0e93231f89c033cfb25938f0a97b1fccbeae6 Binary files /dev/null and b/src/assets/streak.png differ diff --git a/src/assets/streakFlame.png b/src/assets/streakFlame.png new file mode 100644 index 0000000000000000000000000000000000000000..196614ff02ea39e9679190af3a8d0096661a6442 Binary files /dev/null and b/src/assets/streakFlame.png differ diff --git a/src/components/ButtonAddGoalOrChallenge.vue b/src/components/ButtonAddGoalOrChallenge.vue new file mode 100644 index 0000000000000000000000000000000000000000..b45122455c7ab63798231acee20c92ea9198f9e5 --- /dev/null +++ b/src/components/ButtonAddGoalOrChallenge.vue @@ -0,0 +1,49 @@ +<template> + <button + class="primary w-full max-w-64 py-2 flex items-center justify-start pl-4 space-x-2 focus:outline-none focus:ring-2 focus:ring-opacity-50 shadow-md text-xs md:text-sm lg:text-base" + @click="routeToGoalOrChallenge" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M12 4v16m8-8H4" + /> + </svg> + <span class="truncate">{{ btnText }}</span> + </button> +</template> + +<script setup lang="ts"> +import { defineProps, ref } from 'vue' +import { useRouter } from 'vue-router' + +const props = defineProps({ + buttonText: String, + type: String, + showModal: Boolean +}) + +const emit = defineEmits(['update:showModal']) + +const router = useRouter() + +const btnText = ref(props.buttonText) + +const routeToGoalOrChallenge = () => { + if (props.type === 'goal') { + router.push('/sparemaal') + } else if (props.type === 'challenge') { + router.push('/spareutfordringer') + } else if (props.type === 'generatedChallenge') { + emit('update:showModal', true) + } +} +</script> diff --git a/src/components/ButtonDisplayStreak.vue b/src/components/ButtonDisplayStreak.vue new file mode 100644 index 0000000000000000000000000000000000000000..883fcea10530af3c8288adea0220652b26ce7f10 --- /dev/null +++ b/src/components/ButtonDisplayStreak.vue @@ -0,0 +1,133 @@ +<template> + <div class="flex flex-col items-center relative"> + <button + @mouseover="display" + @mouseleave="hide" + class="cursor-pointer bg-transparent hover:bg-transparent hover:scale-150" + > + <img + src="@/assets/pengesekkStreak.png" + alt="streak" + class="mx-auto w-6 h-6 md:w-12 md:h-12" + /> + </button> + + <div + v-if="displayStreakCard" + class="w-[30vh] h-[20vh] md:w-auto md:h-auto group z-50 bg-opacity-50 overflow-hidden absolute right-[-4rem] top-14 md:top-20 flex flex-col justify-evenly text-wrap" + > + <div + class="flex flex-col justify-evenly w-full h-full py-2 px-4 md:py-0 bg-white rounded-2xl border-4 border-green-300" + > + <span class="text-xs md:text-2xl font-bold text-black" + >{{ currentStreak + }}{{ currentStreak === 1 ? ' utfordring fullført' : ' utfordringer fullført' }} + </span> + <p class="text-black text-xs md:text-1xl md:font-bold my-2"> + {{ + currentStreak! > 0 + ? 'Bra jobba du har fullført ' + currentStreak + ' utfordringer pÃ¥ rad!' + : 'Du har ikke fullført en utfordring det siste. Fullfør en nÃ¥ for Ã¥ starte en streak!' + }} + </p> + <Countdown + v-if="screenSize > 768 && currentStreak! > 0" + class="flex flex-row" + countdownSize="1.4rem" + labelSize="0.8rem" + mainColor="black" + secondFlipColor="black" + mainFlipBackgroundColor="#30ab0e" + secondFlipBackgroundColor="#9af781" + :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" + :deadlineISO="deadline" + ></Countdown> + <Countdown + v-if="screenSize <= 768 && currentStreak! > 0" + class="flex flex-row" + countdownSize="1.1rem" + labelSize=".6rem" + mainColor="black" + secondFlipColor="black" + mainFlipBackgroundColor="#30ab0e" + secondFlipBackgroundColor="#9af781" + :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" + :deadlineISO="deadline" + ></Countdown> + <!-- Row component with horizontal padding and auto margins for centering --> + <div + class="flex flex-row items-center mx-auto h-20 w-4/5 md:w-full bg-black-400 gap-4" + > + <div class="flex flex-1 overflow-x-auto"> + <div v-for="index in 6" :key="index" class="min-w-max mx-auto"> + <div class="flex flex-col justify-around items-center"> + <!-- Display the current streak day number adjusted by index --> + <span class="text-black text-xs md:text-1xl font-bold"> + {{ currentStreak! - ((currentStreak! % 7) - index) }} + </span> + <!-- Display images based on completion --> + <img + src="@/assets/pengesekkStreak.png" + :alt=" + index <= currentStreak! % 7 + ? 'challenge completed' + : 'challenge not completed' + " + :class="{ + 'max-h-6 max-w-6 md:max-h-10 md:max-w-10': true, + grayscale: index > currentStreak! % 7 + }" + /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted, onUnmounted, ref } from 'vue' +import { useUserStore } from '@/stores/userStore' +// @ts-ignore +import { Countdown } from 'vue3-flip-countdown' + +const userStore = useUserStore() +const currentStreak = ref<number>() +const deadline = ref<string>() +onMounted(async () => { + userStore.getUserStreak() + if (userStore.streak) { + currentStreak.value = userStore.streak?.streak + deadline.value = userStore.streak?.firstDue + } + console.log('Streak:', currentStreak.value) + if (typeof window !== 'undefined') { + window.addEventListener('resize', handleWindowSizeChange) + } + handleWindowSizeChange() +}) + +const screenSize = ref<number>(window.innerWidth) + +onUnmounted(() => { + window.removeEventListener('resize', handleWindowSizeChange) +}) +const handleWindowSizeChange = () => { + screenSize.value = window.innerWidth +} + +const displayStreakCard = ref(false) + +const display = () => { + displayStreakCard.value = true + userStore.getUserStreak() + currentStreak.value = userStore.streak?.streak + deadline.value = userStore.streak?.firstDue +} + +const hide = () => { + displayStreakCard.value = false +} +</script> diff --git a/src/components/CardChallenge.vue b/src/components/CardChallenge.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2637c77298f2ffa6be437a72c4327152d856ec0 --- /dev/null +++ b/src/components/CardChallenge.vue @@ -0,0 +1,36 @@ +<script lang="ts" setup> +import { computed, type PropType } from 'vue' +import ProgressBar from '@/components/ProgressBar.vue' +import router from '@/router' +import type { Challenge } from '@/types/challenge' + +const props = defineProps({ + challengeInstance: { + type: Object as PropType<Challenge>, + required: true + } +}) + +const challengeInstance = props.challengeInstance +const displayDate = computed(() => challengeInstance.due?.slice(0, 16).split('T').join(' ')) +const isCompleted = computed(() => challengeInstance.completedOn != null) + +const handleCardClick = () => { + router.push({ name: 'view-challenge', params: { id: challengeInstance.id } }) +} +</script> + +<template> + <div + :class="{ 'cursor-default': isCompleted }" + class="border-2 border-lime-400 rounded-xl p-4 flex flex-col items-center gap-2 cursor-pointer w-52 overflow-hidden transition-transform duration-100 ease-in-out hover:scale-105 hover:opacity-90" + @click="handleCardClick" + > + <h3 class="my-0 mx-6">{{ challengeInstance.title }}</h3> + <p>{{ challengeInstance.saved }}kr / {{ challengeInstance.target }}kr</p> + <ProgressBar :completion="challengeInstance.completion" /> + <p>{{ displayDate }}</p> + </div> +</template> + +<style scoped></style> diff --git a/src/components/CardChallengeSavingsPath.vue b/src/components/CardChallengeSavingsPath.vue new file mode 100644 index 0000000000000000000000000000000000000000..3acd26ccedfea9407170c929ca5f8e7733adae52 --- /dev/null +++ b/src/components/CardChallengeSavingsPath.vue @@ -0,0 +1,125 @@ +<template> + <!-- Challenge Icon and Details --> + <div + v-if="challenge" + class="flex items-center justify-center shadow-black min-w-24 w-full h-auto md:max-h-full min-h-24 max-w-32 max-h-32 md:min-h-32 md:min-w-32 md:max-w-48 overflow-hidden" + > + <!-- Challenge Icon --> + <div class="flex flex-col items-center mx-auto md:mx-2 my-auto"> + <div class="flex flex-col flex-nowrap self-center"> + <!-- Check Icon --> + <div + v-if="challenge.completion !== undefined && challenge.completion >= 100" + class="min-w-6 min-h-6 max-w-6 max-h-6 md:min-h-8 md:max-h-8 md:min-w-8 md:max-w-8 ml-20 md:ml-32 p-1 basis-1/4 self-end" + > + <img src="@/assets/completed.png" alt="" />ï¸ + </div> + <div + v-else + class="min-w-6 min-h-6 max-w-6 max-h-6 md:min-h-8 md:max-h-8 md:min-w-8 md:max-w-8 ml-20 md:ml-32 p-1 basis-1/4 self-end" + > + <img src="@/assets/pending.png" alt="" />ï¸ + </div> + <div class="basis-3/4"> + <p + class="text-center text-wrap text-xs lg:text-lg md:text-md" + data-cy="challenge-title" + > + {{ challenge.title }} + </p> + </div> + </div> + <img + @click="editChallenge(challenge)" + :data-cy="'challenge-icon-' + challenge.id" + :src="challengeImageUrl" + class="max-w-8 max-h-12 md:max-h-8 md:max-w-8 lg:max-w-10 lg:max-h-10 cursor-pointer hover:scale-125 rounded-sm" + :alt="challenge.title" + /> + <!-- Progress Bar, if the challenge is not complete --> + <div + v-if="challenge.completion != undefined && challenge.completion < 100" + class="flex-grow w-full mt-2" + > + <div class="flex flex-row ml-5 md:ml-10 justify-center"> + <div class="flex flex-col"> + <div class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700"> + <div + class="bg-lime-400 h-2.5 rounded-full" + data-cy="challenge-progress" + :style="{ + width: (challenge.saved / challenge.target) * 100 + '%' + }" + ></div> + </div> + <div class="text-center text-nowrap text-xs md:text-base"> + {{ challenge.saved }}kr / {{ challenge.target }}kr + </div> + <button + @click="incrementSaved(challenge)" + :data-cy="'increment-challenge' + challenge.id" + type="button" + class="primary text-xs ml-2 z-10 relative" + > + + {{ challenge.perPurchase }}kr pÃ¥ {{ challenge.title }} + </button> + </div> + </div> + </div> + <span v-else class="text-center text-xs md:text-base" + >Ferdig: {{ challenge.saved }}</span + > + </div> + </div> +</template> + +<script setup lang="ts"> +import type { Challenge } from '@/types/challenge' +import { useChallengeStore } from '@/stores/challengeStore' +import router from '@/router' +import { onMounted, ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' + +const challengeStore = useChallengeStore() +const challengeImageUrl = ref('/src/assets/star.png') // Default or placeholder image +const props = defineProps<{ challenge: Challenge }>() + +const emit = defineEmits(['update-challenge', 'complete-challenge']) + +// Increment saved amount +// In your incrementSaved function in the child component +const incrementSaved = async (challenge: Challenge) => { + challenge.saved += challenge.perPurchase + // Trigger the update in the store + + const updatedChallenge = (await challengeStore.editUserChallenge(challenge)) as Challenge + + console.log('updated challenge in child: ', updatedChallenge) + + // Emit an event to inform the parent component of the update + emit('update-challenge', updatedChallenge) +} + +const editChallenge = (challenge: Challenge) => { + router.push(`/spareutfordringer/rediger/${challenge.id}`) +} + +const getChallengeIcon = async (challengeId: number) => { + try { + const imageResponse = await authInterceptor.get(`/challenges/picture?id=${challengeId}`, { + responseType: 'blob' + }) + challengeImageUrl.value = URL.createObjectURL(imageResponse.data) + } catch (error) { + challengeImageUrl.value = '/src/assets/star.png' // Fallback on error + } +} + +onMounted(() => { + if (props.challenge?.id) { + getChallengeIcon(props.challenge.id) + } else { + console.error('Challenge id is undefined') + } +}) +</script> diff --git a/src/components/CardGoal.vue b/src/components/CardGoal.vue new file mode 100644 index 0000000000000000000000000000000000000000..458cf1fb6b426e5a02c6ef4c32024bd385511f14 --- /dev/null +++ b/src/components/CardGoal.vue @@ -0,0 +1,42 @@ +<script lang="ts" setup> +import type { Goal } from '@/types/goal' +import { computed, type PropType } from 'vue' +import ProgressBar from '@/components/ProgressBar.vue' +import router from '@/router' + +const props = defineProps({ + goalInstance: { + type: Object as PropType<Goal>, + required: true + }, + isClickable: { + type: Boolean, + default: true + } +}) + +const goalInstance = props.goalInstance +const displayDate = computed(() => goalInstance.due?.slice(0, 16).split('T').join(' ')) +const isCompleted = computed(() => goalInstance.completedOn != null) + +const handleCardClick = () => { + if (props.isClickable) { + router.push({ name: 'view-goal', params: { id: goalInstance.id } }) + } +} +</script> + +<template> + <div + :class="{ 'cursor-default': isCompleted }" + class="border-2 border-lime-400 rounded-xl p-4 flex flex-col items-center gap-2 cursor-pointer w-52 overflow-hidden transition-transform duration-100 ease-in-out hover:scale-105 hover:opacity-90" + @click="handleCardClick" + > + <h3 class="my-0 mx-6">{{ goalInstance.title }}</h3> + <p>{{ goalInstance.saved }}kr / {{ goalInstance.target }}kr</p> + <ProgressBar :completion="goalInstance.completion" /> + <p>{{ displayDate }}</p> + </div> +</template> + +<style scoped></style> diff --git a/src/components/CardTemplate.vue b/src/components/CardTemplate.vue new file mode 100644 index 0000000000000000000000000000000000000000..6fa731e67776d9e923ea99427b9a23893f3de385 --- /dev/null +++ b/src/components/CardTemplate.vue @@ -0,0 +1,9 @@ +<script lang="ts" setup></script> + +<template> + <div class="border rounded-xl shadow-lg overflow-hidden"> + <slot></slot> + </div> +</template> + +<style scoped></style> diff --git a/src/components/ContinueButtonComponent.vue b/src/components/ContinueButtonComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee47f3e99a46fd0a3e7eb438fe4fabcb56a466e6 --- /dev/null +++ b/src/components/ContinueButtonComponent.vue @@ -0,0 +1,33 @@ +<template> + <button + :class="{ 'opacity-60 cursor-not-allowed': disabled }" + :disabled="disabled" + @click="handleClick" + class="p-3 px-20 text-lg rounded-lg cursor-pointer transition-all font-bold bg-[var(--green)] hover:brightness-90 active:brightness-75" + > + Fortsett + </button> +</template> + +<script setup lang="ts"> +import { defineEmits, defineProps } from 'vue' + +const props = defineProps({ + disabled: { + type: Boolean, + default: false + }, + text: { + type: String, + default: 'Fortsett' + } +}) + +const emit = defineEmits(['click']) + +const handleClick = () => { + if (!props.disabled) { + emit('click') + } +} +</script> diff --git a/src/components/DisplayInfoForChallengeOrGoal.vue b/src/components/DisplayInfoForChallengeOrGoal.vue new file mode 100644 index 0000000000000000000000000000000000000000..b20221e248f67478e41e6df76fd9e8459645cc7d --- /dev/null +++ b/src/components/DisplayInfoForChallengeOrGoal.vue @@ -0,0 +1,87 @@ +<template> + <button @click="display" class="bg-transparent relative p-0 hover:bg-transparent"> + <img src="@/assets/infoIcon.png" alt="i" class="max-h-4 max-w-4 ml-1" /> + </button> + <div + v-if="displayInfoCard" + class="w-[40vh] h-[20vh]md:w-60 md:h-40 group z-50 bg-opacity-50 overflow-hidden absolute mt-8 md:mt-4 md:mr-0 flex flex-col justify-evenly text-wrap" + > + <div + class="flex flex-col justify-around w-3/4 md:w-full h-[80%] py-2 px-4 md:py-0 bg-white rounded-2xl border-4 border-green-300 overflow-auto" + > + <p class="text-base md:text-lg text-wrap text-bold">{{ title.toUpperCase() }}</p> + <p class="text-xs md:text-sm text-wrap mb-2">Beskrivelse: {{ description }}</p> + <p v-if="completion !== 100" class="text-xs md:text-sm text-nowrap text-green-800"> + Utløper om: + </p> + <Countdown + v-if="completion !== 100 && screenSize > 763" + class="flex flex-row" + countdownSize="1.3rem" + labelSize=".8rem" + mainColor="white" + secondFlipColor="white" + mainFlipBackgroundColor="#30ab0e" + secondFlipBackgroundColor="#9af781" + :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" + :deadlineISO="deadline" + ></Countdown> + <Countdown + v-else-if="completion !== 100 && screenSize <= 763" + class="flex flex-row" + countdownSize="1.0rem" + labelSize=".6rem" + mainColor="white" + secondFlipColor="white" + mainFlipBackgroundColor="#30ab0e" + secondFlipBackgroundColor="#9af781" + :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" + :deadlineISO="deadline" + ></Countdown> + <p class="text-nowrap text-xs md:text.sm" v-else> + Utfordring fullført.<br /> + Totalt spart: {{ amountSaved }}kr + </p> + </div> + </div> +</template> + +<script setup lang="ts"> +import type { Challenge } from '@/types/challenge' +import type { Goal } from '@/types/goal' +import { onUnmounted, ref } from 'vue' +// @ts-ignore +import { Countdown } from 'vue3-flip-countdown' + +interface Props { + challenge: Challenge | null | undefined + goal: Goal | null | undefined + isChallenge: boolean +} +const props = defineProps<Props>() + +const description = ref<string>( + props.isChallenge ? props.challenge!.description : props.goal!.description +) +const title = ref<string>(props.isChallenge ? props.challenge!.title : props.goal!.title) +const amountSaved = ref<number>(props.isChallenge ? props.challenge!.saved : props.goal!.saved) +const completion = ref<number>( + props.isChallenge ? props.challenge?.completion ?? 0 : props.goal?.completion ?? 0 +) +const deadline = ref<string>(props.isChallenge ? props.challenge!.due : props.goal!.due) + +const displayInfoCard = ref(false) + +const display = () => { + displayInfoCard.value = !displayInfoCard.value +} + +const screenSize = ref<number>(window.innerWidth) + +onUnmounted(() => { + window.removeEventListener('resize', handleWindowSizeChange) +}) +const handleWindowSizeChange = () => { + screenSize.value = window.innerWidth +} +</script> diff --git a/src/components/FormLogin.vue b/src/components/FormLogin.vue new file mode 100644 index 0000000000000000000000000000000000000000..df5c5f49ba17cf284a3f6d40a6b6de3b5e123479 --- /dev/null +++ b/src/components/FormLogin.vue @@ -0,0 +1,165 @@ +<script lang="ts" setup> +import { computed, ref, watch } from 'vue' +import { useUserStore } from '@/stores/userStore' +import ModalComponent from '@/components/ModalComponent.vue' +import axios from 'axios' + +const username = ref<string>('') +const password = ref<string>('') +const showPassword = ref<boolean>(false) +const errorMessage = ref<string>('') +const isModalOpen = ref<boolean>(false) +const resetEmail = ref<string>('') +const emailRegex = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/ + +const userStore = useUserStore() + +const isEmailValid = computed(() => emailRegex.test(resetEmail.value)) +const isSendingEmail = ref(false) +const successMessage = ref<string | null>(null) +const modalErrorMessage = ref<string | null>(null) + +const submitForm = () => { + userStore.login(username.value, password.value) +} + +const toggleShowPassword = () => { + showPassword.value = !showPassword.value +} + +const submitReset = async () => { + isSendingEmail.value = true + + await axios + .post('http://localhost:8080/forgotPassword/changePasswordRequest', { + email: resetEmail.value + }) + .then(() => { + successMessage.value = + 'E-posten er sendt. Vennligst sjekk innboksen din for instrukser. OBS: E-posten kan havne i spam-mappen' + }) + .catch((error) => { + console.error(error) + modalErrorMessage.value = 'Noe gikk galt. Vennligst prøv igjen.' + }) + + isSendingEmail.value = false +} + +const closeModal = () => { + isModalOpen.value = false + isSendingEmail.value = false + modalErrorMessage.value = null + resetEmail.value = '' + successMessage.value = null +} + +watch( + () => userStore.errorMessage, + (newValue: string) => { + errorMessage.value = newValue + } +) +</script> + +<template> + <div class="flex flex-col justify-center gap-5 w-full"> + <div class="flex flex-col"> + <p class="mx-4">Brukernavn</p> + <input + v-model="username" + name="username" + placeholder="Skriv inn brukernavn" + type="text" + /> + </div> + <div class="flex flex-col"> + <p class="mx-4">Passord</p> + <div class="relative"> + <input + name="password" + v-model="password" + :type="showPassword ? 'text' : 'password'" + class="w-full" + placeholder="Skriv inn passord" + /> + <button + class="absolute right-0 top-1 bg-transparent hover:bg-transparent mr-4 mt-1" + @click="toggleShowPassword" + > + {{ showPassword ? '🔓' : '🔒' }} + </button> + <a + @click="isModalOpen = true" + class="transition-none absolute right-3 top-10 hover:underline hover:bg-transparent text-[#ef9691] hover:transition-none hover:p-0 cursor-pointer" + >Glemt passord?</a + > + </div> + </div> + <div class="flex flex-row gap-5"> + <button + name="submit" + :disabled="'' == username.valueOf() || '' == password.valueOf()" + class="primary grow-0" + @click="submitForm" + > + Logg inn + </button> + <p>{{ errorMessage }}</p> + </div> + </div> + <modal-component + v-if="isModalOpen" + :title="'Glemt passord'" + :message="'Vennligst skriv inn e-posten din for Ã¥ endre passordet.'" + > + <div v-if="isSendingEmail" class="flex justify-center items-center"> + <div + class="p-3 animate-spin bg-gradient-to-r from-lime-500 from-30% to-green-600 to-90% md:w-18 md:h-20 h-20 w-20 aspect-square rounded-full" + > + <div class="rounded-full h-full w-full bg-slate-100"></div> + </div> + </div> + <div v-else-if="successMessage"> + <p class="text-green-500 text-center">{{ successMessage }}</p> + <button + class="active-button font-bold py-2 px-4 w-1/2 mt-4 border-2 disabled:border-transparent" + @click="closeModal" + > + Lukk + </button> + </div> + <div v-else-if="modalErrorMessage"> + <p class="text-red-500 text-center">{{ modalErrorMessage }}</p> + <button + class="active-button font-bold py-2 px-4 w-1/2 mt-4 border-2 disabled:border-transparent" + @click="closeModal" + > + Lukk + </button> + </div> + <div v-else> + <input + v-model="resetEmail" + class="border border-gray-300 p-2 w-full mb-7" + placeholder="Skriv e-postadressen din her" + type="email" + /> + <div class="flex gap-5 mt-4"> + <button + :disabled="!isEmailValid" + class="active-button font-bold py-2 px-4 w-1/2 border-2 disabled:border-transparent" + @click="submitReset" + > + Send mail + </button> + <button + class="active-button font-bold py-2 px-4 w-1/2 border-2 disabled:border-transparent" + @click="closeModal" + > + Lukk + </button> + </div> + </div> + </modal-component> +</template> diff --git a/src/components/FormRegister.vue b/src/components/FormRegister.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ec933ae7a7969273c9f06545ddc7de1ff5b4c83 --- /dev/null +++ b/src/components/FormRegister.vue @@ -0,0 +1,200 @@ +<script lang="ts" setup> +import { computed, ref, watch } from 'vue' +import { useUserStore } from '@/stores/userStore' +import ToolTip from '@/components/ToolTip.vue' + +const firstName = ref<string>('') +const lastName = ref<string>('') +const email = ref<string>('') +const username = ref<string>('') +const password = ref<string>('') +const confirm = ref<string>('') + +const showPassword = ref<boolean>(false) +const errorMessage = ref<string>('') +const passwordValidations = ref<string[]>([]); + + +const userStore = useUserStore() + +const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{0,29}$/ +const emailRegex = + /^[æÆøØåÅa-zA-Z0-9_+&*-]+(?:\.[æÆøØåÅa-zA-Z0-9_+&*-]+)*@(?:[æÆøØåÅa-zA-Z0-9-]+\.)+[æÆøØåÅa-zA-Z]{2,7}$/ +const usernameRegex = /^[ÆØÅæøåA-Za-z][æÆøØåÅA-Za-z0-9_]{2,29}$/ +const passwordRegex = /^(?=.*[0-9])(?=.*[a-zæøå])(?=.*[ÆØÅA-Z])(?=.*[@#$%^&+=!])(?=\S+$).{8,30}$/ + +const isFirstNameValid = computed(() => nameRegex.test(firstName.value) && firstName.value) +const isLastNameValid = computed(() => nameRegex.test(lastName.value) && lastName.value) +const isEmailValid = computed(() => emailRegex.test(email.value)) +const isUsernameValid = computed(() => usernameRegex.test(username.value)) +const isPasswordValid = computed(() => passwordRegex.test(password.value)) + +const isFormInvalid = computed( + () => + [isFirstNameValid, isLastNameValid, isEmailValid, isUsernameValid, isPasswordValid].some( + (v) => !v.value + ) || password.value !== confirm.value +) + +const submitForm = () => { + userStore.register(firstName.value, lastName.value, email.value, username.value, password.value) +} + +const toggleShowPassword = () => { + showPassword.value = !showPassword.value +} + +const validatePassword = () => { + const messages = []; + const lengthValid = password.value.length >= 8 && password.value.length <= 30; + const numberValid = /[0-9]/.test(password.value); + const lowercaseValid = /[a-zæøå]/.test(password.value); + const uppercaseValid = /[ÆØÅA-Z]/.test(password.value); + const specialCharacterValid = /[@#$%^&+=!]/.test(password.value); + const noSpacesValid = !/\s/.test(password.value); + + if (!lengthValid) { + messages.push('MÃ¥ være mellom 8 og 30 karakterer. '); + } + if (!numberValid) { + messages.push('MÃ¥ inneholde minst ett tall. '); + } + if (!lowercaseValid) { + messages.push('MÃ¥ inneholde minst én liten bokstav. '); + } + if (!uppercaseValid) { + messages.push('MÃ¥ inneholde minst én stor bokstav. '); + } + if (!specialCharacterValid) { + messages.push('MÃ¥ inneholde minst ett spesialtegn (@#$%^&+=!). '); + } + if (!noSpacesValid) { + messages.push('MÃ¥ ikke inneholde mellomrom. '); + } + + passwordValidations.value = messages; +}; + +watch(password, validatePassword); + +watch( + () => userStore.errorMessage, + (newValue: string) => { + errorMessage.value = newValue + } +) +</script> + +<template> + <div class="flex flex-col justify-center gap-5 w-full"> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Fornavn*</p> + <ToolTip + :message="'MÃ¥ kun inneholde bokstaver, mellomrom, komma, apostrof, punktum, og bindestrek. 1-30 karakterer langt'" + /> + </div> + <input + v-model="firstName" + name="firstName" + :class="{ 'border-2 border-lime-400': isFirstNameValid }" + placeholder="Skriv inn fornavn" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Etternavn*</p> + <ToolTip + :message="'MÃ¥ kun inneholde bokstaver, mellomrom, komma, apostrof, punktum, og bindestrek. 1-30 karakterer langt'" + /> + </div> + <input + v-model="lastName" + name="lastName" + :class="{ 'border-2 border-lime-400': isLastNameValid }" + placeholder="Skriv inn etternavn" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>E-post*</p> + <ToolTip + :message="'Gyldig email: MÃ¥ starte med norske bokstaver, tall, eller spesielle karakterer. Inkluderer \@\ fulgt av et domene. Ender med 2-7 bokstaver.'" + /> + </div> + <input + v-model="email" + name="email" + :class="{ 'border-2 border-lime-400': isEmailValid }" + placeholder="Skriv inn e-post" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Brukernavn*</p> + <ToolTip + :message="'MÃ¥ starte med en bokstav og kan inneholde tall og understrek. 3-30 karakterer langt.'" + /> + </div> + <input + v-model="username" + name="username" + placeholder="Skriv inn brukernavn" + type="text" + :class="{ 'border-2 border-lime-400': isUsernameValid }" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Passord*</p> + <ToolTip + :message="'MÃ¥ være minst 8 karakterer, inkludert et tall, en liten bokstav, en stor bokstav, et spesialtegn (@#$%^&+=!), og ingen mellomrom.'" + /> + </div> + <div class="relative"> + <input + name="password" + v-model="password" + :type="showPassword ? 'text' : 'password'" + placeholder="Skriv inn passord" + class="w-full" + :class="{ 'border-2 border-lime-400': isPasswordValid }" + /> + <button + class="absolute right-0 top-1 bg-transparent hover:bg-transparent mr-4 mt-1" + @click="toggleShowPassword" + > + {{ showPassword ? '🔓' : '🔒' }} + </button> + </div> + <input + v-model="confirm" + :class="{ + 'border-2 border-lime-400': password == confirm && '' !== confirm.valueOf() + }" + class="mt-2" + name="confirm" + placeholder="Bekreft passord" + type="password" + /> + <div class="ml-4"> + <p class="text-sm"> + <span v-for="message in passwordValidations" :key="message">{{ message }}</span> + </p> + </div> + </div> + <div class="flex flex-row gap-5"> + <button + :disabled="isFormInvalid" + class="grow-0 primary" + name="submit" + @click="submitForm" + > + Registrer deg + </button> + </div> + </div> +</template> diff --git a/src/components/GeneratedChallengesModal.vue b/src/components/GeneratedChallengesModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..d1bfdd52c66a8dccc8c73bd2bcd2d917f480216c --- /dev/null +++ b/src/components/GeneratedChallengesModal.vue @@ -0,0 +1,152 @@ +<template> + <div + v-if="showModal" + class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50" + > + <div class="relative bg-white pt-10 p-4 rounded-lg shadow-xl" style="width: 40rem"> + <button @click="closeModal" class="absolute top-0 right-0 m-2 primary"> + <svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M6 18L18 6M6 6l12 12" + /> + </svg> + </button> + <div v-if="generatedChallenges.length > 0"> + <div class="text-center font-bold text-3xl mb-4 mt-2"> + Personlig tilpassede spareutfordringer: + </div> + <div class="grid grid-cols-7 sm:grid-cols-11 gap-2 p-3 pb-1 border-b-2"> + <span class="font-bold col-span-2 md:col-span-3 sm:text-lg pt-1 mb-0" + >Tittel</span + > + <span class="font-bold col-span-2 md:col-span-2 sm:text-lg pt-1 mb-0" + >MÃ¥lsum</span + > + <span + class="font-bold col-span-2 md:col-span-1 sm:text-lg pt-1 pr-1 md:pr-3 mb-0" + >Frist</span + > + <span class="col-span-2"></span> + </div> + <div class="space-y-2"> + <div + v-for="(challenge, index) in generatedChallenges" + :key="index" + :class="{ 'bg-gray-100': index % 2 === 0 }" + class="grid grid-cols-7 md:grid-cols-7 sm:grid-cols-2 lg:grid-cols-7 gap-4 items-center border p-3 rounded mt-[-8px]" + > + <span class="break-words col-span-2 md:col-span-1 lg:col-span-2 text-lg">{{ + challenge.title + }}</span> + <span class="col-span-2 md:col-span-2 lg:col-span-1 text-lg">{{ + challenge.target + }}</span> + <span class="col-span-2 md:col-span-1 lg:col-span-2 text-lg">{{ + challenge.due + }}</span> + <div + class="col-span-7 sm:col-start-3 sm:col-span-2 md:col-span-2 lg:col-span-2 flex items-center justify-end space-x-2" + > + <span v-if="challenge.isAccepted" class="font-bold text-lg" + >Godtatt!</span + > + <button + @click="acceptChallenge(challenge)" + class="font-bold py-1 px-4 mt-[-14px] sm:mt-0 primary" + > + Godta + </button> + </div> + </div> + </div> + </div> + <div v-else class="text-center text-2xl font-bold mt-1"> + Ingen nye spareutfordringer enda ... sjekk igjen senere! + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted, reactive, ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' +import type { AxiosResponse } from 'axios' + +interface Challenge { + title: string + target: number + due: string + dueFull: string + isAccepted: boolean + perPurchase?: number + description?: string + type?: string +} + +const showModal = ref(true) +const generatedChallenges = reactive<Challenge[]>([]) + +async function fetchGeneratedChallenges() { + try { + const response: AxiosResponse = await authInterceptor.get('/challenges/generate') + if (response.status === 200) { + generatedChallenges.splice( + 0, + generatedChallenges.length, + ...response.data.map((ch: any) => ({ + ...ch, + due: new Date(ch.due).toISOString().split('T')[0], + dueFull: ch.due, + isAccepted: false + })) + ) + } else { + generatedChallenges.splice(0, generatedChallenges.length) + } + } catch (error) { + console.error('Error fetching challenges:', error) + } +} + +onMounted(() => { + fetchGeneratedChallenges() + localStorage.setItem('lastModalShow', Date.now().toString()) +}) + +function acceptChallenge(challenge: Challenge) { + if (!challenge) { + console.error('No challenge data provided to acceptChallenge function.') + return + } + const postData = { + title: challenge.title, + saved: 0, + target: challenge.target, + perPurchase: challenge.perPurchase, + description: challenge.description, + due: challenge.dueFull, + type: challenge.type + } + authInterceptor + .post('/challenges', postData) + .then((response: AxiosResponse) => { + challenge.isAccepted = true + }) + .catch((error) => { + console.error('Failed to save challenge:', error) + }) +} + +const closeModal = () => { + showModal.value = false +} +</script> diff --git a/src/components/HelpComponent.vue b/src/components/HelpComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..f1ebac400adf9ea7be83fd0a43c3c3c20b41e588 --- /dev/null +++ b/src/components/HelpComponent.vue @@ -0,0 +1,37 @@ +<template> + <div class="fixed bottom-10 right-10 hover:cursor-pointer z-50" @click="isModalOpen = true"> + <img + alt="Hjelp" + class="h-12 transition-transform duration-300 ease-in-out hover:scale-110" + src="@/assets/hjelp.png" + /> + </div> + <ModalComponent v-if="isModalOpen" @close="isModalOpen = false"> + <InteractiveSpare + :speech="speech" + :png-size="12" + direction="right" + @emit:close="isModalOpen = false" + /> + + <div class="-mb-5 mt-8 text-xs text-gray-500"> + <p class="justify-center items-center">Trykk for Ã¥ se hva Spare har Ã¥ si!</p> + <a + @click="isModalOpen = false" + class="underline hover:bg-transparent font-normal text-gray-500 cursor-pointer transition-none hover:transition-none hover:p-0" + > + Skip + </a> + </div> + </ModalComponent> +</template> + +<script setup lang="ts"> +import InteractiveSpare from '@/components/InteractiveSpare.vue' +import { ref } from 'vue' +import ModalComponent from '@/components/ModalComponent.vue' + +const isModalOpen = ref(false) + +defineProps(['speech']) +</script> diff --git a/src/components/ImgGifTemplate.vue b/src/components/ImgGifTemplate.vue new file mode 100644 index 0000000000000000000000000000000000000000..23cf21c7bc264d98f124834fded6dcdc8d31ca3a --- /dev/null +++ b/src/components/ImgGifTemplate.vue @@ -0,0 +1,20 @@ +<template> + <div class="hover:scale-110 flex justify-center items-center"> + <img + v-if="index % 6 === modValue" + :src="url" + alt="could not load" + class="min-w-24 w-full h-auto min-h-24 max-w-32 max-h-32 md:min-h-32 md:max-h-44 md:min-w-32 md:max-w-44 rounded-lg border-stale-400 shadow-md shadow-gray-500" + /> + </div> +</template> + +<script setup lang="ts"> +interface Props { + url: string + index: number + modValue: number +} + +defineProps<Props>() +</script> diff --git a/src/components/InteractiveSpare.vue b/src/components/InteractiveSpare.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee727c5b0bc753c478f2b60cdc730476af8aa729 --- /dev/null +++ b/src/components/InteractiveSpare.vue @@ -0,0 +1,128 @@ +<template> + <div + class="spareDiv flex items-center mr-10 max-w-[60vh] cursor-pointer" + :class="{ + 'flex-row': direction === 'right', + 'flex-row-reverse': direction === 'left' + }" + @click="nextSpeech" + > + <!-- Image --> + <img + :src="spareImageSrc" + :style="{ width: pngSize + 'rem', height: pngSize + 'rem' }" + :class="['object-contain', ...imageClass]" + alt="Spare" + class="w-dynamic h-dynamic object-contain" + /> + + <!-- Speech Bubble --> + <div + :class="`mb-40 inline-block relative w-64 bg-white p-4 rounded-3xl border border-gray-600 tri-right round ${bubbleDirection}`" + > + <div class="text-left leading-6"> + <p class="speech m-0">{{ currentSpeech }}</p> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, defineProps, ref } from 'vue' +import spareImageSrc from '@/assets/spare.png' + +interface Props { + speech?: Array<string> + direction: 'left' | 'right' + pngSize: number +} + +const props = defineProps<Props>() +const speech = ref<string[]>(props.speech || []) +const currentSpeechIndex = ref(0) +const currentSpeech = computed(() => speech.value[currentSpeechIndex.value]) + +const emit = defineEmits(['emit:close']) + +const nextSpeech = () => { + if (currentSpeechIndex.value < speech.value.length - 1) { + currentSpeechIndex.value++ + } else { + emit('emit:close') + } +} + +const imageClass = computed(() => { + return [ + 'transform', + props.direction === 'right' ? 'scale-x-[-1]' : '' // Flip image if right + ] +}) + +const bubbleDirection = computed(() => { + return props.direction === 'right' ? 'btm-left-in' : 'btm-right-in' +}) +</script> +<style scoped> +/* CSS talk bubble */ + +.border { + border: 0.1rem solid black; +} +.round { + border-radius: 1.875rem; + -webkit-border-radius: 1.875rem; + -moz-border-radius: 1.875rem; +} + +/*Right triangle, placed bottom left side slightly in*/ +.tri-right.border.btm-left-in:before { + content: ' '; + position: absolute; + width: 0; + height: 0; + left: 2.3rem; + right: auto; + top: auto; + bottom: -1.5rem; + border: 0.7rem solid; + border-color: black transparent transparent black; +} +.tri-right.btm-left-in:after { + content: ' '; + position: absolute; + width: 0; + height: 0; + left: 2.375rem; + right: auto; + top: auto; + bottom: -1.25rem; + border: 0.75rem solid; + border-color: white transparent transparent white; +} + +/*Right triangle, placed bottom right side slightly in*/ +.tri-right.border.btm-right-in:before { + content: ' '; + position: absolute; + width: 0; + height: 0; + left: auto; + right: 2.3rem; + top: auto; + bottom: -1.5rem; + border: 0.7rem solid; + border-color: black black transparent transparent; +} +.tri-right.btm-right-in:after { + content: ' '; + position: absolute; + width: 0; + height: 0; + left: auto; + right: 2.375rem; + bottom: -1.25rem; + border: 0.75rem solid; + border-color: white white transparent transparent; +} +</style> diff --git a/src/components/ModalComponent.vue b/src/components/ModalComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..c8d84c67286bbafb9cbeb39832c0e087d516c323 --- /dev/null +++ b/src/components/ModalComponent.vue @@ -0,0 +1,43 @@ +<template> + <div + v-if="isModalOpen" + class="fixed inset-0 bg-black bg-opacity-30 flex justify-center items-center z-50" + > + <div class="bg-white p-6 rounded-lg shadow-lg max-w-lg w-full text-center"> + <h2 class="title font-bold mb-4">{{ title }}</h2> + <p class="message mb-4" v-html="message"></p> + + <slot /> + + <div class="buttons flex flex-row justify-center items-center gap-3 mt-3 w-full"> + <slot name="buttons"></slot> + <button v-if="closeButton" class="button primary" @click="$emit('close')"> + Lukk + </button> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted } from 'vue' + +defineProps({ + title: String, + message: String, + isModalOpen: { + type: Boolean, + default: true, + required: false + }, + closeButton: { + type: Boolean, + default: false, + required: false + } +}) + +onMounted(() => { + console.log('ModalComponent mounted') +}) +</script> diff --git a/src/components/ModalEditAvatar.vue b/src/components/ModalEditAvatar.vue new file mode 100644 index 0000000000000000000000000000000000000000..459190f7713707ab23d9ee66ecd9df46b45c016f --- /dev/null +++ b/src/components/ModalEditAvatar.vue @@ -0,0 +1,161 @@ +<template> + <button @click="openModal" class="primary text-nowrap">Endre avatar</button> + <div + v-if="isModalOpen" + class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50" + > + <div class="bg-white p-6 rounded-lg shadow-lg max-w-[80vh] h-auto w-full text-center"> + <div class="flex flex-row justify-end"> + <button @click="closeModal" class="primary">X</button> + </div> + <h2 class="title">Endre avatar</h2> + <div class="avatar-container flex flex-row justify-between gap-2 items-center my-8"> + <button @click="cycleArray('prev')">â—€</button> + <div class="flex flex-row items-center justify-around"> + <img :src="previousAvatar" alt="avatar" class="avatar h-16 w-16" /> + <img + :src="currentAvatar" + alt="avatar" + class="avatar block mx-auto h-32 w-32 rounded-full border-green-600 border-2 sm:mx-0 sm:shrink-0" + /> + <img :src="nextAvatar" alt="avatar" class="avatar h-16 w-16" /> + </div> + <button @click="cycleArray('next')">â–¶</button> + </div> + <div class="flex flex-row items-center gap-4 mx-auto"> + <button @click="saveAvatar" class="primary save-button basis-1/2">Lagre</button> + <button @click="openFileExplorer" class="primary basis-1/2"> + Upload New Avatar + </button> + </div> + <input type="file" ref="fileInput" @change="handleFileUpload" hidden /> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref, reactive, computed } from 'vue' +import { useUserStore } from '@/stores/userStore' + +const userStore = useUserStore() + +const state = reactive({ + avatars: [ + '/avatar1.png', + '/avatar2.png', + '/avatar3.png', + '/avatar4.png', + '/avatar5.png', + '/avatar6.png', + '/avatar7.png', + '/avatar8.png', + '/avatar9.png' + ], + currentAvatarIndex: 0, + newFile: null, // To hold the new file object + selectedPublicImg: '' // Track blob URLs created for uploaded files +}) + +const isModalOpen = ref(false) +const fileInput = ref<HTMLElement | null>(null) + +const emit = defineEmits(['update-profile-picture']) + +const openModal = () => { + state.avatars = [ + '/avatar1.png', + '/avatar2.png', + '/avatar3.png', + '/avatar4.png', + '/avatar5.png', + '/avatar6.png', + '/avatar7.png', + '/avatar8.png', + '/avatar9.png' + ] + userStore.getProfilePicture() + const urlProfilePicture = userStore.profilePicture + // Check if a profile picture URL exists and append it to the avatars list + const img = localStorage.getItem('profilePicture') as string + console.log(state.avatars) + console.log(img) + if (state.avatars.includes(state.selectedPublicImg) || state.avatars.includes(img)) { + // Remove the public asset from the list if it's already selected + state.avatars = state.avatars.filter((avatar) => avatar !== state.selectedPublicImg) + console.log(state.avatars, 'state.avatars') + } + // Clear + console.log(state.avatars) + localStorage.removeItem('profilePicture') + state.selectedPublicImg = '' + + if (urlProfilePicture) { + state.avatars.push(urlProfilePicture) + state.currentAvatarIndex = state.avatars.length - 1 // Set the current avatar to the profile picture + } + isModalOpen.value = true +} + +const closeModal = () => { + isModalOpen.value = false + //Remove the uploaded file if there is one. + state.avatars = [] + + state.newFile = null // Clear the new file reference +} + +const cycleArray = (direction: string) => { + if (direction === 'prev') { + state.currentAvatarIndex = + (state.currentAvatarIndex - 1 + state.avatars.length) % state.avatars.length + } else { + state.currentAvatarIndex = (state.currentAvatarIndex + 1) % state.avatars.length + } +} + +const handleFileUpload = async (event: any) => { + const input = event.target + if (input.files && input.files[0]) { + const file = input.files[0] + // Clear any existing temporary blob URLs + state.avatars = state.avatars.filter((avatar) => !avatar.startsWith('blob:')) + state.newFile = file // Save the new file object for later upload + state.avatars.push(URL.createObjectURL(file)) // Add the blob URL for preview + state.currentAvatarIndex = state.avatars.length - 1 // Set this new avatar as current + } +} + +const saveAvatar = async () => { + if (state.newFile && currentAvatar.value.startsWith('blob:')) { + // If there's a new file selected, upload it + const formData = new FormData() + formData.append('file', state.newFile) + await userStore.uploadProfilePicture(formData) + } else if (currentAvatar.value.startsWith('/')) { + // If it's a public asset, fetch it as a blob and upload + state.selectedPublicImg = currentAvatar.value + const response = await fetch(currentAvatar.value) + const blob = await response.blob() + const file = new File([blob], 'public-avatar.png', { type: blob.type }) + const formData = new FormData() + formData.append('file', file) + await userStore.uploadProfilePicture(formData) + localStorage.setItem('profilePicture', currentAvatar.value) + } + closeModal() + emit('update-profile-picture', currentAvatar.value) +} + +const openFileExplorer = () => { + fileInput.value?.click() +} + +const currentAvatar = computed(() => state.avatars[state.currentAvatarIndex]) +const nextAvatar = computed( + () => state.avatars[(state.currentAvatarIndex + 1) % state.avatars.length] +) +const previousAvatar = computed( + () => + state.avatars[(state.currentAvatarIndex - 1 + state.avatars.length) % state.avatars.length] +) +</script> diff --git a/src/components/NavBarComponent.vue b/src/components/NavBarComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..951c4981cc34faaed0e6586fd69b4ed08c69aba5 --- /dev/null +++ b/src/components/NavBarComponent.vue @@ -0,0 +1,105 @@ +<template> + <nav class="flex justify-between items-center min-h-32 text-xl w-full px-3 my-0"> + <div class="order-first basis-1/5"> + <router-link to="/hjem" @click="hamburgerOpen = false"> + <img + alt="logo" + class="w-40 cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-90" + src="@/assets/spareSti.png" + /> + </router-link> + </div> + <div v-if="!isHamburger" class="flex flex-row justify-center gap-10 mx-auto basis-3/5"> + <router-link active-class="border-b-2" to="/hjem">ðŸ Hjem</router-link> + <router-link active-class="border-b-2" to="/sparemaal">🎯SparemÃ¥l</router-link> + <router-link active-class="border-b-2" to="/spareutfordringer" + >💰Spareutfordringer</router-link + > + <router-link active-class="border-b-2" to="/profil">ðŸ¤Profil</router-link> + </div> + + <div v-if="!isHamburger" class="flex-row flex gap-2 justify-end w-auto h-14 basis-1/5"> + <ButtonDisplayStreak /> + <button + class="primary basis-1/2 bg-[#95e35d] logout focus:ring focus:ring-black-300 text-nowrap" + @click="openModal" + > + Logg ut + </button> + </div> + <div class="flex flex-row gap-2"> + <ButtonDisplayStreak v-if="isHamburger" /> + <button class="primary logout" v-if="isHamburger" @click="toggleMenu">☰</button> + </div> + </nav> + + <div v-if="hamburgerOpen" class="flex flex-col bg-white border border-slate-300 z-50"> + <router-link to="/hjem" @click="hamburgerOpen = false">ðŸ Hjem</router-link> + <router-link to="/sparemaal" @click="hamburgerOpen = false">🎯SparemÃ¥l</router-link> + <router-link to="/spareutfordringer" @click="hamburgerOpen = false" + >💰Spareutfordringer</router-link + > + <router-link to="/profil" @click="hamburgerOpen = false">ðŸ¤Profil</router-link> + <button class="focus:ring focus:ring-black-300" @click="openModal">Logg ut</button> + </div> + <ModalComponent + :title="'Vil du logge ut?'" + :message="'Er du sikker pÃ¥ at du vil logge ut av SpareSti? Du kan alltid logge inn igjen senere 🕺'" + :is-modal-open="isModalOpen" + @close="isModalOpen = false" + > + <template v-slot:buttons> + <button @click="logout" class="primary">Logg ut</button> + <button @click="closeModal" class="primary danger">Avbryt</button> + </template> + </ModalComponent> +</template> + +<script setup lang="ts"> +import { RouterLink } from 'vue-router' +import { onMounted, ref } from 'vue' +import { useUserStore } from '@/stores/userStore' +import ModalComponent from '@/components/ModalComponent.vue' +import ButtonDisplayStreak from '@/components/ButtonDisplayStreak.vue' + +const userStore = useUserStore() + +const windowWidth = ref(window.innerWidth) +const hamburgerOpen = ref(false) +const isHamburger = ref(false) +const isModalOpen = ref<boolean>(false) + +const logout = () => { + userStore.logout() +} + +const toggleMenu = () => { + hamburgerOpen.value = !hamburgerOpen.value +} + +const updateWindowWidth = () => { + windowWidth.value = window.innerWidth + if (windowWidth.value < 1150) { + isHamburger.value = true + } else { + isHamburger.value = false + hamburgerOpen.value = false + } +} + +onMounted(() => { + if (typeof window !== 'undefined') { + window.addEventListener('resize', updateWindowWidth) + } + updateWindowWidth() +}) + +const openModal = (event: MouseEvent) => { + event.preventDefault() + isModalOpen.value = true +} + +const closeModal = () => { + isModalOpen.value = false +} +</script> diff --git a/src/components/PageControl.vue b/src/components/PageControl.vue new file mode 100644 index 0000000000000000000000000000000000000000..921f2c9f9d9651bbccebef8116c4b19a18a99ab8 --- /dev/null +++ b/src/components/PageControl.vue @@ -0,0 +1,38 @@ +<script lang="ts" setup> +defineProps({ + currentPage: { + type: Number, + required: true + }, + totalPages: { + type: Number, + required: true + }, + onPageChange: { + type: Function, + default: () => {} + } +}) +</script> + +<template> + <div v-if="totalPages > 0" class="flex justify-center gap-4"> + <button + class="primary" + :disabled="currentPage === 0" + @click="onPageChange(currentPage - 1)" + > + Forrige + </button> + <p>{{ currentPage + 1 }} / {{ totalPages }}</p> + <button + class="primary" + :disabled="currentPage === totalPages - 1" + @click="onPageChange(currentPage + 1)" + > + Neste + </button> + </div> +</template> + +<style scoped></style> diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..3a29f2fa6eec6da409198d57d50b13e84cd2fefc --- /dev/null +++ b/src/components/ProgressBar.vue @@ -0,0 +1,13 @@ +<script lang="ts" setup> +defineProps({ + completion: Number +}) +</script> + +<template> + <div class="w-full bg-gray-200 rounded-full overflow-hidden"> + <div :style="{ width: completion + '%' }" class="bg-lime-400 h-2 rounded-full"></div> + </div> +</template> + +<style scoped></style> diff --git a/src/components/SavingsPath.vue b/src/components/SavingsPath.vue new file mode 100644 index 0000000000000000000000000000000000000000..be7ba589faa90f71c35c286a6fc2e3c9fa4dcb83 --- /dev/null +++ b/src/components/SavingsPath.vue @@ -0,0 +1,721 @@ +<template> + <div + v-if="isMounted" + class="flex flex-col basis-2/3 max-h-full mx-auto md:ml-20 md:mr-2 max-w-5/6 md:basis-3/4 md:max-pr-20 md:pr-10 md:max-mr-20" + > + <div class="flex justify-center align-center"> + <span + class="w-full max-w-60 max-h-12 text-black text-2xl font-bold py-2 rounded mt-8 text-center space-x-2 drop-shadow-lg" + > + Din Sparesti + </span> + </div> + <button + v-if="!allChallengesCompleted()" + class="h-auto w-auto absolute flex text-center self-end mr-10 md:mr-20 text-wrap border-2 border-gray-200 rounded-xl shadow-black sm:top-50 sm:text-xs sm:mr-20 lg:mr-32 top-60 z-50 p-2 text-xs md:text-sm hover:scale-105" + @click="scrollToFirstUncompleted" + v-show="!isAtFirstUncompleted" + > + Ufullførte utfordringer<br />↓ + </button> + <div class="h-1 w-4/6 mx-auto my-2 opacity-10"></div> + <div + v-if="challengesLocal" + ref="containerRef" + class="container relative pt-6 w-4/5 bg-cover bg-[center] md:[background-position: center;] mx-auto md:w-4/5 no-scrollbar h-full max-h-[60vh] md:max-h-[60vh] md:min-w-2/5 overflow-y-auto border-transparent rounded-lg bg-white shadow-md shadow-slate-400" + style="background-image: url('src/assets/bakgrunn.png')" + > + <div> + <img src="@/assets/start-sign.png" alt="Spare" class="md:w-1/6 md:h-auto h-20" /> + </div> + + <div + v-for="(challenge, index) in challengesLocal" + :key="challenge.id" + class="flex flex-col items-center" + :ref="(el) => assignRef(el, challenge, index)" + > + <!-- Challenge Row --> + <div + :class="{ + 'justify-center mx-auto md:justify-between': index % 2 === 1, + 'justify-center md:justify-between mx-auto': index % 2 === 0 + }" + class="flex flex-row w-full md:w-4/5 justify-start gap-4 md:gap-8 h-auto" + > + <div class="flex"> + <img-gif-template + :index="index" + :mod-value="1" + url="src/assets/golfSpare.gif" + ></img-gif-template> + <img-gif-template + :index="index" + :mod-value="3" + url="src/assets/sleepingSpare.gif" + ></img-gif-template> + <img-gif-template + :index="index" + :mod-value="5" + url="src/assets/archerSpare.gif" + ></img-gif-template> + </div> + <card-challenge-savings-path + :goal="goalLocal!" + :challenge="challenge" + @update-challenge="handleChallengeUpdate" + ></card-challenge-savings-path> + <div class="flex"> + <img-gif-template + :index="index" + :mod-value="0" + url="src/assets/cowboySpare.gif" + ></img-gif-template> + <img-gif-template + :index="index" + :mod-value="2" + url="src/assets/hotAirBalloonSpare.gif" + ></img-gif-template> + <img-gif-template + :index="index" + :mod-value="4" + url="src/assets/farmerSpare.gif" + ></img-gif-template> + </div> + </div> + <!-- Piggy Steps, centered --> + <div v-if="index !== challengesLocal.length" class="flex justify-center w-full"> + <img + :src="getPigStepsIcon()" + :class="{ 'transform scale-x-[-1]': index % 2 === 0 }" + class="w-20 md:w-24 lg:w-32 h-20 md:h-24 lg:h-32" + alt="Pig Steps" + /> + </div> + + <div + v-if="index === challengesLocal.length - 1 && index % 2 === 0" + class="flex flex-row mt-2" + > + <button class="text-2xl ml-48 mr-2 primary" @click="addSpareUtfordring"> + + + </button> + <p class="">Legg til <br />Spareutfordring</p> + </div> + <div + v-else-if="index === challengesLocal.length - 1 && index % 2 !== 0" + class="mr-20 flex flex-row" + > + <button class="text-2xl ml-10 rounded-full primary" @click="addSpareUtfordring"> + + + </button> + <p class="pl-2">Legg til <br />Spareutfordring</p> + </div> + <!-- Finish line --> + </div> + <img + src="@/assets/finishline2.png" + class="w-full max-h-auto mx-auto mt-4" + alt="Finish Line" + /> + </div> + <!-- Goal --> + <div v-if="goalLocal" class="flex flex-row justify-around m-t-2 pt-6 w-full mx-auto"> + <div class="grid grid-rows-2 grid-flow-col gap 4"> + <div class="row-span-3 cursor-pointer" @click="editGoal(goalLocal)"> + <img + :src="goalImageUrl" + class="w-12 h-12 mx-auto rounded-sm" + :alt="goalLocal.title" + /> + <div class="text-lg font-bold" data-cy="goal-title">{{ goalLocal.title }}</div> + </div> + </div> + <div class="flex flex-col items-end"> + <div @click="goToEditGoal" class="cursor-pointer"> + <h3 class="text-blue-500 text-base">Endre mÃ¥l</h3> + </div> + <div + :key="componentKey" + ref="targetRef" + class="bg-yellow-400 px-4 py-1 rounded-full text-black font-bold" + > + {{ goalLocal.saved }}kr / {{ goalLocal.target }}kr + </div> + </div> + </div> + </div> + <!-- Animation icon --> + <img + src="@/assets/penger.png" + alt="Penger" + ref="iconRef" + class="max-w-20 max-h-20 absolute opacity-0" + /> + <img + v-if="goalLocal" + :src="goalImageUrl" + alt="could not load" + ref="goalIconRef" + class="shadow-sm shadow-amber-300 max-w-20 max-h-20 absolute opacity-0 rounded-sm" + /> +</template> + +<script setup lang="ts"> +import { + type ComponentPublicInstance, + nextTick, + onMounted, + onUnmounted, + reactive, + type Ref, + ref +} from 'vue' +import anime from 'animejs' +import type { Challenge } from '@/types/challenge' +import type { Goal } from '@/types/goal' +import confetti from 'canvas-confetti' +import { useRouter } from 'vue-router' +import { useGoalStore } from '@/stores/goalStore' +import ImgGifTemplate from '@/components/ImgGifTemplate.vue' +import CardChallengeSavingsPath from '@/components/CardChallengeSavingsPath.vue' +import authInterceptor from '@/services/authInterceptor' + +const router = useRouter() +const goalStore = useGoalStore() + +interface Props { + challenges: Challenge[] + goal: Goal | null | undefined +} +const props = defineProps<Props>() + +const challengesLocal = ref<Challenge[]>() +let goalLocal: Goal | null | undefined = reactive({ + title: '', // Default empty string to prevent undefined errors + saved: 0, + target: 0 +} as Goal) +const isMounted = ref<boolean>(false) +const componentKey = ref<number>(0) + +//Initialisation: + +onMounted(async () => { + window.addEventListener('resize', handleWindowSizeChange) + handleWindowSizeChange() + challengesLocal.value = props.challenges + goalLocal = props.goal + sortChallenges() + allChallengesCompleted() + // Delay the execution of the following logic by 300ms + setTimeout(() => { + const container = containerRef.value + if (container) { + container.addEventListener('scroll', () => { + if (!firstUncompletedRef.value) return + const containerRect = container.getBoundingClientRect() + const firstUncompletedRect = firstUncompletedRef.value.getBoundingClientRect() + isAtFirstUncompleted.value = !( + firstUncompletedRect.top > containerRect.bottom || + firstUncompletedRect.bottom < containerRect.top + ) + }) + } + scrollToFirstUncompleted() + }, 300) // Timeout set to 300 milliseconds + // Load existing animated states first + loadAnimatedStates() + + // Get completed challenge IDs, ensuring that only defined IDs are considered + const completedChallenges = challengesLocal.value + .filter((challenge) => challenge.completion! >= 100 && challenge.id !== undefined) + .map((challenge) => challenge.id as number) // Use 'as number' to assert that ids are numbers after the check + + // Update only new completions that are not already in the animatedChallenges + const newAnimations = completedChallenges.filter((id) => !animatedChallenges.value.includes(id)) + animatedChallenges.value = [...animatedChallenges.value, ...newAnimations] + + // Save the updated list back to localStorage + localStorage.setItem('animatedChallenges', JSON.stringify(animatedChallenges.value)) + isMounted.value = true +}) + +onUnmounted(() => { + window.removeEventListener('resize', handleWindowSizeChange) + const container = containerRef.value + if (container) { + container.removeEventListener('scroll', () => { + // Clean up the scroll listener + }) + } +}) + +const handleChallengeUpdate = (updatedChallenge: Challenge) => { + if (challengesLocal.value) { + const index = challengesLocal.value.findIndex((c) => c.id === updatedChallenge.id) + if (index !== -1) { + challengesLocal.value[index] = { ...updatedChallenge } + } + + if ( + updatedChallenge.completion! >= 100 && + !animatedChallenges.value.includes(updatedChallenge.id as number) + ) { + animateChallenge(updatedChallenge) + saveAnimatedStateChallenge(updatedChallenge) + } + + if (goalLocal) { + incrementGoalSaved(updatedChallenge) + // Force component update right here might be more appropriate + componentKey.value++ + } + } +} + +const incrementGoalSaved = async (challenge: Challenge) => { + if (goalLocal) { + // Correct the addition mistake and remove setTimeout + goalLocal.saved = goalLocal.saved + challenge.perPurchase + await nextTick() // Only add the perPurchase amount + + const completion = (goalLocal.saved / goalLocal.target) * 100 + if (completion >= 100 && !animatedGoals.value.includes(goalLocal.id as number)) { + animateGoal(goalLocal) + setTimeout(() => { + goalStore.getUserGoals() + goalLocal = goalStore.priorityGoal + }, 4000) // Keep this delay only for the store update and goal switch + } else { + await goalStore.getUserGoals() + goalLocal = goalStore.priorityGoal + } + } +} + +/** + * Navigates to the spareutfordringer page + */ +const addSpareUtfordring = () => { + router.push('/spareutfordringer').catch((error) => { + console.error('Routing error:', error) + }) +} + +/** + * Checks if all challenges are completed + */ +const allChallengesCompleted = () => { + // Assuming challenges.value is an array of challenge objects + if (challengesLocal.value) { + for (const challenge of challengesLocal.value) { + if (challenge.completion !== 100) { + return false // If any challenge is not completed, return false + } + } + return true + } // If all challenges are completed, return true +} + +//-----------Animation for goal and challenge completion-----------------// + +// Reactive references for DOM elements +const iconRef = ref<HTMLElement | null>(null) +const goalIconRef = ref<HTMLElement | null>(null) +const containerRef = ref<HTMLElement | null>(null) +const targetRef = ref<HTMLElement | null>(null) + +// Declare the ref with a type annotation for an array of strings +const animatedChallenges: Ref<number[]> = ref([]) +const animatedGoals: Ref<number[]> = ref([]) + +/** + * Loads the states for animated goals and challenges + */ +const loadAnimatedStates = () => { + const animated = localStorage.getItem('animatedChallenges') + const animatedG = localStorage.getItem('animatedGoals') + animatedChallenges.value = animated ? JSON.parse(animated) : [] + animatedGoals.value = animatedG ? JSON.parse(animatedG) : [] +} + +/** + * Saves the animated state for challenge + * triggers the confetti method + * triggers the recalculation of dom positioning + * @param challenge + */ +const animateChallenge = (challenge: Challenge) => { + if ( + challenge.completion! >= 100 && + !animatedChallenges.value.includes(challenge.id as number) + ) { + if (challenge.id != null) { + animatedChallenges.value.push(challenge.id) + } // Ensure no duplication + saveAnimatedStateChallenge(challenge) // Refactor this to update localStorage correctly + triggerConfetti() + recalculateAndAnimate(false) + } +} + +/** + * Saves the animated state for goal + * triggers the confetti method + * triggers the recalculation of dom positioning + * @param goal + */ +const animateGoal = (goal: Goal) => { + console.log('im in animated goal') + + if (goal.id != null) { + animatedGoals.value.push(goal.id) + } // Ensure no duplication + saveAnimatedStateGoal(goal) // Refactor this to update localStorage correctly + triggerConfetti() + recalculateAndAnimate(true) +} + +/** + * Recalculates the position of the dom elements + * @param isGoal + */ +const recalculateAndAnimate = (isGoal: boolean) => { + console.log('im in recalculate and animate') + + if (!isGoal && iconRef.value && containerRef.value && targetRef.value) { + animateIcon(isGoal) + } else if (isGoal && containerRef.value && goalIconRef.value) { + animateIcon(isGoal) + } else if (!isGoal && !targetRef.value) { + animateIcon(isGoal) + } else { + console.error('Element references are not ready.') + } +} + +/** + * Saves the animated state for challenge + * @param challenge + */ +const saveAnimatedStateChallenge = (challenge: Challenge) => { + if (challenge.id != null) { + animatedChallenges.value.push(challenge.id) + } + localStorage.setItem('animatedChallenges', JSON.stringify(animatedChallenges.value)) +} + +/** + * Saves the animated state for goal + * @param goal + */ +const saveAnimatedStateGoal = (goal: Goal) => { + console.log('Saving animated state for:', goal.id) + if (goal.id != null) { + animatedGoals.value.push(goal.id) + } + localStorage.setItem('animatedGoals', JSON.stringify(animatedGoals.value)) +} + +/** + * animates the icon images + * @param isGoal + */ +const animateIcon = (isGoal: boolean) => { + console.log('im in animate icon') + const icon = iconRef.value + const container = containerRef.value + const target = targetRef.value + + if (!icon || !container) { + console.error('Required animation elements are not available.') + return + } + // Obtain bounding rectangles safely + const containerRect = container.getBoundingClientRect() + const targetRect = target?.getBoundingClientRect() + const iconRect = icon.getBoundingClientRect() + + // Initialize translation coordinates + let translateX1 = 0, + translateY1 = 0, + translateX2 = 0, + translateY2 = 0 + + if (isGoal) { + const goal = goalIconRef.value + if (goal) { + const goalRect = goal.getBoundingClientRect() + if (goalRect) { + // Calculate the translation coordinates for the goal + translateX1 = + containerRect.left + + containerRect.width / 2 - + goalRect.width / 2 - + goalRect.left + translateY1 = + containerRect.top + + containerRect.height / 2 - + goalRect.height / 2 - + goalRect.top + + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: goal, + translateX: translateX1, + translateY: translateY1, + opacity: [0, 1], // Fix: start from 0 opacity and animate to 1 + duration: 1000 + }) + .add({ + targets: goal, + opacity: [1, 0], // Fade out after moving + duration: 3000, + scale: 3, + begin: function (anim) { + if (icon) icon.classList.add('glow') // Ensure icon exists before applying class + }, + complete: function (anim) { + if (icon) icon.classList.remove('glow') // Clean up: remove class after animation + } + }) + } else { + console.error('Goal rectangle is not available.') + } + } else { + console.error('Goal element is not available.') + } + } else if (!isGoal && target && targetRect) { + // Calculate the translation coordinates for the icon + translateX1 = + containerRect.left + containerRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY1 = + containerRect.top + containerRect.height / 2 - iconRect.height / 2 - iconRect.top + translateX2 = targetRect.left + targetRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY2 = targetRect.top + targetRect.height / 2 - iconRect.height / 2 - iconRect.top + + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: icon, + translateX: translateX1, + translateY: translateY1, + opacity: 0, // Start invisible + duration: 1000 + }) + .add({ + targets: icon, + opacity: 1, // Reveal the icon once it starts moving to the container + duration: 1000, // Make the opacity change almost instantaneously + scale: 3 + }) + .add({ + targets: icon, + translateX: translateX2, + translateY: translateY2, + scale: 0.5, + opacity: 1, // Keep the icon visible while moving to the target + duration: 1500 + }) + .add({ + targets: icon, + opacity: 0, // Fade out once it reaches the target + scale: 1, + duration: 500 + }) + .add({ + targets: icon, + translateX: 0, // Reset translation to original + translateY: 0, // Reset translation to original + duration: 500 + }) + } else if (!isGoal && !target) { + // Calculate the translation coordinates for the icon + translateX1 = + containerRect.left + containerRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY1 = + containerRect.top + containerRect.height / 2 - iconRect.height / 2 - iconRect.top + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: icon, + translateX: translateX1, + translateY: translateY1, + opacity: 0, // Start invisible + duration: 1000 + }) + .add({ + targets: icon, + opacity: 1, // Reveal the icon once it starts moving to the container + duration: 1000, // Make the opacity change almost instantaneously + scale: 3 + }) + .add({ + targets: icon, + opacity: 0, // Fade out once it reaches the target + scale: 1, + duration: 500 + }) + .add({ + targets: icon, + translateX: 0, // Reset translation to original + translateY: 0, // Reset translation to original + duration: 500 + }) + } +} +/** + * Triggers confeti animation + */ +const triggerConfetti = () => { + confetti({ + particleCount: 400, + spread: 80, + origin: { x: 0.8, y: 0.8 } + }) +} + +const goalImageUrl = ref('src/assets/pengesekkStreak.png') + +const getGoalIcon = async (goalId: number) => { + try { + const imageResponse = await authInterceptor.get(`/goals/picture?id=${goalId}`, { + responseType: 'blob' + }) + goalImageUrl.value = URL.createObjectURL(imageResponse.data) + } catch (error) { + console.error('Failed to load challenge icon:', error) + goalImageUrl.value = 'src/assets/pengesekkStreak.png' + } +} + +onMounted(() => { + if (props.goal?.id) { + getGoalIcon(props.goal.id) + } else { + console.error('Goal id is undefined') + } +}) + +const getPigStepsIcon = () => { + return 'src/assets/pigSteps.png' +} + +const goToEditGoal = () => { + router.push({ name: 'edit-goal', params: { id: goalLocal?.id } }) +} + +const editGoal = (goal: Goal) => { + router.push(`/sparemaal/rediger/${goal.id}`) +} + +/** + * Sorts the challenges by completion status and due date + + */ +const sortChallenges = () => { + if (challengesLocal.value) { + challengesLocal.value.sort((a, b) => { + // First, sort by completion status: non-completed (less than 100) before completed (100) + if (a.completion !== 100 && b.completion === 100) { + return 1 // 'a' is not completed and 'b' is completed, 'a' should come first + } else if (a.completion === 100 && b.completion !== 100) { + return -1 // 'a' is completed and 'b' is not, 'b' should come first + } else { + // Explicitly convert dates to numbers for subtraction + const dateA = new Date(a.due).getTime() + const dateB = new Date(b.due).getTime() + return dateA - dateB + } + }) + } +} + +// Interface for element references +interface ElementRefs { + [key: string]: HTMLElement | undefined +} + +const elementRefs = reactive<ElementRefs>({}) +const isAtFirstUncompleted = ref(false) +const firstUncompletedRef: Ref<HTMLElement | undefined> = ref() +const screenSize = ref<number>(window.innerWidth) + +/** + * Handles the window size change event + */ +const handleWindowSizeChange = () => { + screenSize.value = window.innerWidth +} + +/** + * Scrolls to the first uncompleted challenge + + */ +const scrollToFirstUncompleted = () => { + if (challengesLocal.value) { + let found = false + for (let i = 0; i < challengesLocal.value.length; i++) { + if (challengesLocal.value[i].completion! < 100) { + const refKey = `uncompleted-${i}` + if (elementRefs[refKey]) { + elementRefs[refKey]!.scrollIntoView({ behavior: 'smooth', block: 'start' }) + firstUncompletedRef.value = elementRefs[refKey] // Store the reference + found = true + isAtFirstUncompleted.value = true + break + } + } + } + if (!found) { + isAtFirstUncompleted.value = false + } + } +} + +/** + * Assigns the reference to the element + * @param el + * @param challenge + * @param index + */ +const assignRef = ( + el: Element | ComponentPublicInstance | null, + challenge: Challenge, + index: number +) => { + const refKey = `uncompleted-${index}` + if (el instanceof HTMLElement) { + // Ensure that el is an HTMLElement + if (challenge.completion! < 100) { + elementRefs[refKey] = el + } + } else { + // Cleanup if the element is unmounted or not an HTMLElement + if (elementRefs[refKey]) { + delete elementRefs[refKey] + } + } +} +</script> + +<style scoped> +/* Tailwind CSS - Custom CSS for hiding scrollbars */ +.no-scrollbar::-webkit-scrollbar { + display: none; /* for Chrome, Safari, and Opera */ +} +.no-scrollbar { + -ms-overflow-style: none; /* for Internet Explorer and Edge */ +} +</style> diff --git a/src/components/SpareComponent.vue b/src/components/SpareComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..d5e7a829bb27560bcd87a992c9bc2b17a6c41047 --- /dev/null +++ b/src/components/SpareComponent.vue @@ -0,0 +1,66 @@ +<template> + <div> + <!-- This is the clickable image that will trigger the modal to open --> + <div + class="flex items-center" + :class="{ + 'flex-row scale-x-[-1]': imageDirection === 'right', + 'flex-row-reverse': imageDirection === 'left' + }" + > + <a + @click="isModalOpen = true" + class="hover:bg-transparent hover:p-0 hover:scale-105 z-20" + > + <img + alt="Spare" + class="md:h-5/6 md:w-5/6 w-2/3 h-2/3 cursor-pointer ml-14 md:ml-10" + src="@/assets/spare.png" + /> + </a> + </div> + + <!-- InteractiveSpare modal component --> + <ModalComponent v-if="isModalOpen" @close="isModalOpen = false"> + <InteractiveSpare + :speech="speech" + :png-size="pngSize!" + direction="left" + @emit:close="isModalOpen = false" + /> + + <div class="-mb-5 mt-8 text-xs text-gray-500"> + <p class="justify-center items-center">Trykk for Ã¥ se hva Spare har Ã¥ si!</p> + <a + @click="isModalOpen = false" + class="underline hover:bg-transparent font-normal text-gray-500 cursor-pointer transition-none hover:transition-none hover:p-0" + > + Skip + </a> + </div> + </ModalComponent> + </div> +</template> + +<script setup lang="ts"> +import InteractiveSpare from '@/components/InteractiveSpare.vue' +import { defineProps, ref, watchEffect } from 'vue' +import ModalComponent from '@/components/ModalComponent.vue' + +const isModalOpen = ref(false) + +const props = defineProps({ + speech: Array<string>, + pngSize: Number, + direction: String, + imageDirection: String, + show: { + type: Boolean, + default: false, + required: false + } +}) +watchEffect(() => { + isModalOpen.value = props.show +}) +</script> diff --git a/src/components/ToolTip.vue b/src/components/ToolTip.vue new file mode 100644 index 0000000000000000000000000000000000000000..ab2e58b9cbd31702490c7612e121f2252c82446f --- /dev/null +++ b/src/components/ToolTip.vue @@ -0,0 +1,36 @@ +<script lang="ts" setup> +import { ref } from 'vue' + +defineProps({ + message: String +}) + +const show = ref(false) + +const toggleShow = () => { + show.value = !show.value +} +</script> + +<template> + <div class="relative"> + <div + class="cursor-pointer" + tabindex="0" + @mouseleave="show = false" + @mouseover="show = true" + @keydown.space.prevent="toggleShow" + @keydown.enter.prevent="toggleShow" + > + (?) + </div> + <div + v-if="show" + class="absolute -inset-x-36 z-10 p-2 mt-2 w-40 text-sm bg-gray-100 rounded shadow-lg" + > + {{ message }} + </div> + </div> +</template> + +<style scoped></style> diff --git a/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts b/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..796c198f8b0dc97d32c41dcab9ee86b2f5fd3027 --- /dev/null +++ b/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import ButtonComponent from '@/components/ButtonAddGoalOrChallenge.vue' + +describe('ButtonComponent', () => { + it('renders correctly', () => { + const wrapper = mount(ButtonComponent, { + props: { + buttonText: 'Click me', + type: 'goal' + } + }) + expect(wrapper.exists()).toBe(true) + }) + + it('displays the correct button text', () => { + const wrapper = mount(ButtonComponent, { + props: { + buttonText: 'Submit', + type: 'goal' + } + }) + const buttonText = wrapper.find('span.truncate') + expect(buttonText.text()).toBe('Submit') + }) +}) diff --git a/src/components/__tests__/CardChallengeTest.spec.ts b/src/components/__tests__/CardChallengeTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..726d7e59708b7add55d35ef777e7e90e01248259 --- /dev/null +++ b/src/components/__tests__/CardChallengeTest.spec.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import CardChallenge from '../CardChallenge.vue' +import type { Challenge } from '../../types/challenge' + +describe('CardChallenge', () => { + let incompleteWrapper: any + let completeWrapper: any + + const incompleteChallenge: Challenge = { + id: 1, + title: 'Test title', + perPurchase: 10, + saved: 100, + target: 1000, + description: 'Test description', + due: '2022-01-01T00:00:00Z', + createdOn: '2021-01-01T00:00:00Z', + type: 'Challenge type', + completion: 10 + } + + const completeChallenge: Challenge = { + id: 1, + title: 'Test title', + perPurchase: 10, + saved: 100, + target: 1000, + description: 'Test description', + due: '2022-01-01T00:00:00Z', + createdOn: '2021-01-01T00:00:00Z', + type: 'Challenge type', + completion: 10, + completedOn: '2022-01-01T00:00:00Z' + } + + const mountIncompletedWrapper = async () => { + incompleteWrapper = mount(CardChallenge, { + propsData: { + challengeInstance: incompleteChallenge + } + }) + await incompleteWrapper.vm.$nextTick() + } + + const mountCompleteWrapper = async () => { + completeWrapper = mount(CardChallenge, { + propsData: { + challengeInstance: completeChallenge + } + }) + await completeWrapper.vm.$nextTick() + } + + it('renders correctly', () => { + mountIncompletedWrapper() + expect(incompleteWrapper.text()).toContain('Test title') + expect(incompleteWrapper.text()).toContain('100kr / 1000kr') + expect(incompleteWrapper.text()).toContain('2022-01-01 00:00') + }) + + it('sets isCompleted to false', () => { + mountIncompletedWrapper() + expect(incompleteWrapper.vm.isCompleted).toBe(false) + }) + + it('sets isCompleted to true', () => { + mountCompleteWrapper() + expect(completeWrapper.vm.isCompleted).toBe(true) + }) +}) diff --git a/src/components/__tests__/CardGoalTest.spec.ts b/src/components/__tests__/CardGoalTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ada562db10c85a49537a0a0833891479848aa42 --- /dev/null +++ b/src/components/__tests__/CardGoalTest.spec.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import CardGoal from '../CardGoal.vue' +import type { Goal } from '../../types/goal' + +describe('CardGoal', () => { + let incompleteWrapper: any + let completeWrapper: any + + const incompleteGoal: Goal = { + id: 1, + title: 'Test title', + saved: 100, + target: 1000, + description: 'Test description', + due: '2022-01-01T00:00:00Z', + createdOn: '2021-01-01T00:00:00Z', + completion: 10 + } + + const completeGoal: Goal = { + id: 1, + title: 'Test title', + saved: 100, + target: 1000, + description: 'Test description', + due: '2022-01-01T00:00:00Z', + createdOn: '2021-01-01T00:00:00Z', + completion: 10, + completedOn: '2022-01-01T00:00:00Z' + } + + const mountIncompletedWrapper = async () => { + incompleteWrapper = mount(CardGoal, { + propsData: { + goalInstance: incompleteGoal + } + }) + await incompleteWrapper.vm.$nextTick() + } + + const mountCompleteWrapper = async () => { + completeWrapper = mount(CardGoal, { + propsData: { + goalInstance: completeGoal + } + }) + await completeWrapper.vm.$nextTick() + } + + it('renders correctly', () => { + mountIncompletedWrapper() + expect(incompleteWrapper.text()).toContain('Test title') + expect(incompleteWrapper.text()).toContain('100kr / 1000kr') + expect(incompleteWrapper.text()).toContain('2022-01-01 00:00') + }) + + it('sets isCompleted to false', () => { + mountIncompletedWrapper() + expect(incompleteWrapper.vm.isCompleted).toBe(false) + }) + + it('sets isCompleted to true', () => { + mountCompleteWrapper() + expect(completeWrapper.vm.isCompleted).toBe(true) + }) +}) diff --git a/src/components/__tests__/ContinueButtonTest.spec.ts b/src/components/__tests__/ContinueButtonTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc9285c7abb1bfbd9fec3b5b308865068fec964c --- /dev/null +++ b/src/components/__tests__/ContinueButtonTest.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' + +describe('ContinueButtonComponent', () => { + it('renders correctly', () => { + const wrapper = mount(ContinueButtonComponent) + expect(wrapper.text()).toContain('Fortsett') + }) + + it('is disabled when the `disabled` prop is true', () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: true } + }) + const button = wrapper.find('button') + expect(button.attributes('disabled')).toBeDefined() + expect(button.classes()).toContain('opacity-60') + expect(button.classes()).toContain('cursor-not-allowed') + }) + + it('does not emit click event when disabled', async () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: true } + }) + await wrapper.trigger('click') + expect(wrapper.emitted()).not.toHaveProperty('click') + }) + + it('emits click event when not disabled', async () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: false } + }) + await wrapper.trigger('click') + expect(wrapper.emitted()).toHaveProperty('click') + }) +}) diff --git a/src/components/__tests__/FormLoginTest.spec.ts b/src/components/__tests__/FormLoginTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff05225207f4cb7de16668f3d79caf6afe022bb8 --- /dev/null +++ b/src/components/__tests__/FormLoginTest.spec.ts @@ -0,0 +1,71 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import FormLogin from '../FormLogin.vue' +import { createPinia } from 'pinia' +import MockAdapter from 'axios-mock-adapter' +import axios from 'axios' + +describe('FormLogin', () => { + let wrapper: any + let mock: MockAdapter + + beforeEach(() => { + wrapper = mount(FormLogin, { + global: { + plugins: [createPinia()] + } + }) + + mock = new MockAdapter(axios) + }) + + afterEach(() => { + mock.restore() + }) + + it('renders properly', () => { + expect(wrapper.text()).toContain('Brukernavn') + expect(wrapper.text()).toContain('Passord') + expect(wrapper.text()).toContain('Logg inn') + + expect(wrapper.find('input[type="text"]').exists()).toBe(true) + expect(wrapper.find('input[type="password"]').exists()).toBe(true) + expect(wrapper.find('button').exists()).toBe(true) + + expect((wrapper.find('input[type="text"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[type="password"]').element as HTMLInputElement).value).toBe('') + }) + + it('disables button when none inputs are filled', () => { + const button = wrapper.findAll('button').find((b: any) => b.text() === 'Logg inn') + expect(button.attributes('disabled')).toBeDefined() + }) + + it('disables button when only username is filled', () => { + const button = wrapper.findAll('button').find((b: any) => b.text() === 'Logg inn') + + const inputUsername = wrapper.find('input[type="text"]') + inputUsername.setValue('username') + expect(button.attributes('disabled')).toBeDefined() + }) + + it('disables button when only password is filled', () => { + const button = wrapper.findAll('button').find((b: any) => b.text() === 'Logg inn') + + const inputPassword = wrapper.find('input[type="password"]') + inputPassword.setValue('password') + expect(button.attributes('disabled')).toBeDefined() + }) + + it('enables button when input', async () => { + const button = wrapper.findAll('button').find((b: any) => b.text() === 'Logg inn') + const inputUsername = wrapper.find('input[type="text"]') + const inputPassword = wrapper.find('input[type="password"]') + + inputUsername.setValue('username') + inputPassword.setValue('password') + await wrapper.vm.$nextTick() + + expect(button.attributes('disabled')).toBeUndefined() + }) +}) diff --git a/src/components/__tests__/FormRegisterTest.spec.ts b/src/components/__tests__/FormRegisterTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3604b5c4a4351e9cc2f8cf781285983b102a679 --- /dev/null +++ b/src/components/__tests__/FormRegisterTest.spec.ts @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import FormRegister from '../FormRegister.vue' // Change this to your registration component +import { createPinia } from 'pinia' +import MockAdapter from 'axios-mock-adapter' +import axios from 'axios' + +describe('FormRegister', () => { + let wrapper: any + let mock: MockAdapter + + beforeEach(() => { + wrapper = mount(FormRegister, { + global: { + plugins: [createPinia()] + } + }) + + mock = new MockAdapter(axios) + }) + + afterEach(() => { + mock.restore() + }) + + function successfulFormFill() { + wrapper.find('input[name="firstName"]').setValue('firstName') + wrapper.find('input[name="lastName"]').setValue('lastName') + wrapper.find('input[name="email"]').setValue('email@test.work') + wrapper.find('input[name="username"]').setValue('username') + wrapper.find('input[name="password"]').setValue('Password123!') + wrapper.find('input[name="confirm"]').setValue('Password123!') + } + + it('renders properly', () => { + expect(wrapper.text()).toContain('Brukernavn') + expect(wrapper.text()).toContain('Passord') + expect(wrapper.text()).toContain('Registrer deg') + + expect(wrapper.find('input[name="firstName"]').exists()).toBe(true) + expect(wrapper.find('input[name="lastName"]').exists()).toBe(true) + expect(wrapper.find('input[name="email"]').exists()).toBe(true) + expect(wrapper.find('input[name="username"]').exists()).toBe(true) + expect(wrapper.find('input[name="password"]').exists()).toBe(true) + expect(wrapper.find('input[name="confirm"]').exists()).toBe(true) + + expect((wrapper.find('input[name="firstName"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[name="lastName"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[name="email"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[name="username"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[name="password"]').element as HTMLInputElement).value).toBe('') + expect((wrapper.find('input[name="confirm"]').element as HTMLInputElement).value).toBe('') + + expect(wrapper.find('button[name="submit"]').exists()).toBe(true) + }) + + it('disables button when none inputs are filled', () => { + const button = wrapper.find('button[name="submit"]') + expect(button.attributes('disabled')).toBeDefined() + }) + + it('enables button when all inputs are filled', async () => { + const button = wrapper.find('button[name="submit"]') + + successfulFormFill() + + await wrapper.vm.$nextTick() + + expect(button.attributes('disabled')).toBeUndefined() + }) + + it('disables button when password and confirm password do not match', async () => { + const button = wrapper.find('button[name="submit"]') + + successfulFormFill() + wrapper.find('input[name="confirm"]').setValue('Password123') + + await wrapper.vm.$nextTick() + + expect(button.attributes('disabled')).toBeDefined() + }) + + it('disable button when email is invalid', async () => { + const button = wrapper.find('button[name="submit"]') + + successfulFormFill() + wrapper.find('input[name="email"]').setValue('email') + + await wrapper.vm.$nextTick() + + expect(button.attributes('disabled')).toBeDefined() + }) +}) diff --git a/src/components/__tests__/InteractiveSpareTest.spec.ts b/src/components/__tests__/InteractiveSpareTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1beebffebdc09ea8e8b69004789f25044c37272f --- /dev/null +++ b/src/components/__tests__/InteractiveSpareTest.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import SpeechBubbleComponent from '@/components/InteractiveSpare.vue' // Adjust the import path as needed. + +describe('SpeechBubbleComponent', () => { + it('renders correctly with default props', () => { + const wrapper: any = mount(SpeechBubbleComponent, { + props: { + direction: 'left', + speech: ['Hello', 'World'], + pngSize: 100, + isModalOpen: true + } + }) + expect(wrapper.exists()).toBeTruthy() + }) + + /* + it('applies dynamic classes based on direction prop', () => { + const wrapper = mount(SpeechBubbleComponent, { + props: { + direction: 'right', + speech: ['Hello', 'World'], + pngSize: 100, + isModalOpen: true + } + }) + expect(wrapper.find('.spareDiv').classes()).toContain('flex-row') + const wrapperReverse = mount(SpeechBubbleComponent, { + props: { + direction: 'left', + speech: ['Hello', 'World'], + pngSize: 100, + isModalOpen: true + } + }) + expect(wrapperReverse.find('.spareDiv').classes()).toContain('flex-row-reverse') + }) + + it('image class is computed based on direction', () => { + const wrapper = mount(SpeechBubbleComponent, { + props: { + direction: 'right', + speech: ['Hello', 'World'], + pngSize: 100, + isModalOpen: true + } + }) + expect(wrapper.find('img').classes()).toContain('scale-x-[-1]') + }) + + it('updates speech text on div click', async () => { + const wrapper = mount(SpeechBubbleComponent, { + props: { + direction: 'left', + speech: ['First speech', 'Second speech'], + pngSize: 100, + isModalOpen: true + } + }) + expect(wrapper.find('.speech').text()).toBe('First speech') + await wrapper.find('.spareDiv').trigger('click') + expect(wrapper.find('.speech').text()).toBe('Second speech') + }) + */ +}) diff --git a/src/components/__tests__/ModalTest.spec.ts b/src/components/__tests__/ModalTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7fd91adddcbbba3bcd92c01df9193b64a8eb9c4 --- /dev/null +++ b/src/components/__tests__/ModalTest.spec.ts @@ -0,0 +1,37 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import ModalComponent from '@/components/ModalComponent.vue' + +describe('ModalComponent', () => { + let wrapper: any + + beforeEach(() => { + wrapper = shallowMount(ModalComponent, { + props: { + title: 'Test Title', + message: 'Test Message', + isModalOpen: true + } + }) + }) + + it('opens modal when button is clicked', async () => { + expect(wrapper.props().isModalOpen).toBe(true) + }) + + it('title should be: Test Title', () => { + expect(wrapper.find('.title').text()).toBe('Test Title') + }) + + it('title should not be: Wrong Title', () => { + expect(wrapper.find('.title').text()).not.toBe('Wrong Title') + }) + + it('message should be: Test Message', () => { + expect(wrapper.find('.message').text()).toBe('Test Message') + }) + + it('message should not be: Wrong Message', () => { + expect(wrapper.find('.message').text()).not.toBe('Wrong Message') + }) +}) diff --git a/src/components/__tests__/PageControlTest.spec.ts b/src/components/__tests__/PageControlTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..71739dfebc3e4cd9efc819de55d160d253e09e17 --- /dev/null +++ b/src/components/__tests__/PageControlTest.spec.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import PageControl from '../PageControl.vue' + +describe('PageControl', () => { + let wrapper: any + + beforeEach(() => { + wrapper = mount(PageControl, { + props: { + currentPage: 0, + totalPages: 10 + } + }) + }) + + it('renders correctly', () => { + expect(wrapper.text()).toContain('1 / 10') + expect(wrapper.find('button', { text: 'Forrige' }).exists()).toBe(true) + expect(wrapper.find('button', { text: 'Neste' }).exists()).toBe(true) + }) + + it('disables the "Forrige" button and enables the "Neste" button when currentPage is 0', () => { + expect(wrapper.find('button', { text: 'Forrige' }).attributes('disabled')).toBe('') + expect(wrapper.find('button', { text: 'Neste' }).attributes('disabled')).toBe('') + }) + + it('enables both buttons when currentPage is more than 0 and less than totalPages', async () => { + await wrapper.setProps({ currentPage: 1 }) + expect(wrapper.find('button', { text: 'Forrige' }).attributes('disabled')).toBeUndefined() + expect(wrapper.find('button', { text: 'Neste' }).attributes('disabled')).toBeUndefined() + }) + + it('enables the "Forrige" button and disables the "Neste" button when currentPage is equal to totalPages - 1', async () => { + await wrapper.setProps({ currentPage: 9 }) + expect(wrapper.find('button', { text: 'Forrige' }).attributes('disabled')).toBeUndefined() + expect(wrapper.find('button', { text: 'Neste' }).attributes('disabled')).toBeUndefined() + }) +}) diff --git a/src/components/icons/IconCommunity.vue b/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000000000000000000000000000000000000..36348e2ee2939606a832d4fd1b6353646af45f5f --- /dev/null +++ b/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> + <path + d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z" + /> + </svg> +</template> diff --git a/src/components/icons/IconDocumentation.vue b/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c0fa7610b34b2f5853f7d2c3143d2ff6c9ee1b4 --- /dev/null +++ b/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor"> + <path + d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z" + /> + </svg> +</template> diff --git a/src/components/icons/IconEcosystem.vue b/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000000000000000000000000000000000000..0702bbbe16bbbe88654ec119dc307cd678ea6d63 --- /dev/null +++ b/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor"> + <path + d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z" + /> + </svg> +</template> diff --git a/src/components/icons/IconSupport.vue b/src/components/icons/IconSupport.vue new file mode 100644 index 0000000000000000000000000000000000000000..908b94c4f8b389565dd8511113b74b7a34d276b8 --- /dev/null +++ b/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> + <path + d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z" + /> + </svg> +</template> diff --git a/src/components/icons/IconTooling.vue b/src/components/icons/IconTooling.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2a971e08d007f25fe63be672f86b6ce16806f8b --- /dev/null +++ b/src/components/icons/IconTooling.vue @@ -0,0 +1,18 @@ +<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license--> +<template> + <svg + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + role="img" + class="iconify iconify--mdi" + width="24" + height="24" + preserveAspectRatio="xMidYMid meet" + viewBox="0 0 24 24" + > + <path + d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z" + fill="currentColor" + ></path> + </svg> +</template> diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dcad83c30800a564e96bad81c93d6be2ffaceaa --- /dev/null +++ b/src/main.ts @@ -0,0 +1,14 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b50a37b654c4d822fb8226a29a1da8e0f4eef800 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,197 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useUserStore } from '@/stores/userStore' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'start', + component: () => import('@/views/StartView.vue') + }, + { + path: '/hjem', + name: 'home', + component: () => import('@/views/HomeView.vue') + }, + { + path: '/logginn', + name: 'login', + component: () => import('@/views/RegisterLoginView.vue') + }, + { + path: '/logginn/:username', + name: 'login-bio', + component: () => import('@/views/BiometricLoginView.vue') + }, + { + path: '/registrer', + name: 'register', + component: () => import('@/views/RegisterLoginView.vue') + }, + { + path: '/forgotPassword', + name: 'resetPassword', + component: () => import('@/views/ResetPasswordView.vue') + }, + { + path: '/profil', + name: 'profile', + component: () => import('@/views/ViewProfileView.vue') + }, + { + path: '/profil/rediger', + name: 'edit-profile', + component: () => import('@/views/ManageProfileView.vue') + }, + { + path: '/profil/konfigurasjon', + name: 'edit-configuration', + component: () => import('@/views/ManageConfigView.vue') + }, + { + path: '/sparemaal', + name: 'goals', + component: () => import('@/views/UserGoalsView.vue') + }, + { + path: '/sparemaal/rediger/ny', + name: 'new-goal', + component: () => import('@/views/ManageGoalView.vue') + }, + { + path: '/sparemaal/rediger/:id', + name: 'edit-goal', + component: () => import('@/views/ManageGoalView.vue') + }, + { + path: '/sparemaal/oversikt/:id', + name: 'view-goal', + component: () => import('@/views/ViewGoalView.vue') + }, + { + path: '/spareutfordringer', + name: 'challenges', + component: () => import('@/views/UserChallengesView.vue') + }, + { + path: '/spareutfordringer/ny', + name: 'new-challenge', + component: () => import('@/views/ManageChallengeView.vue') + }, + { + path: '/spareutfordringer/rediger/:id', + name: 'edit-challenge', + component: () => import('@/views/ManageChallengeView.vue') + }, + { + path: '/spareutfordringer/oversikt/:id', + name: 'view-challenge', + component: () => import('@/views/ViewChallengeView.vue') + }, + { + path: '/konfigurasjonSteg1', + name: 'configurations1', + component: () => import('@/views/ConfigHabitChangeView.vue') + }, + { + path: '/konfigurasjonSteg2', + name: 'configurations2', + component: () => import('@/views/ConfigFamiliarWithSavingsView.vue') + }, + { + path: '/konfigurasjonSteg3', + name: 'configurations3', + component: () => import('@/views/ConfigSpendingItemsView.vue') + }, + { + path: '/konfigurasjonSteg4', + name: 'configurations4', + component: () => import('@/views/ConfigSpendingItemsAmountView.vue') + }, + { + path: '/konfigurasjonSteg5', + name: 'configurations5', + component: () => import('@/views/ConfigSpendingItemsTotalAmountView.vue') + }, + { + path: '/konfigurasjonSteg6', + name: 'configurations6', + component: () => import('@/views/ConfigAccountNumberView.vue') + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: () => import('@/views/NotFoundView.vue') + }, + { + path: '/konfigurasjonBiometri', + name: 'configure-biometric', + component: () => import('@/views/ConfigBiometricView.vue') + } + ], + scrollBehavior() { + return { top: 0 } + } +}) + +router.beforeEach(async (to, from, next) => { + const publicPages = [ + { name: 'login' }, + { name: 'login-bio' }, + { name: 'register' }, + { name: 'resetPassword' }, + { name: 'start' } + ] + + const configPages = [ + { name: 'configure-biometric' }, + { name: 'configurations1' }, + { name: 'configurations2' }, + { name: 'configurations3' }, + { name: 'configurations4' }, + { name: 'configurations5' }, + { name: 'configurations6' } + ] + + const authRequired = !publicPages.some((page) => page.name === to.name) + const loginCredentials = sessionStorage.getItem('accessToken') + const bioCredentials = localStorage.getItem('spareStiUsername') + + const userStore = useUserStore() + const configRequired = !configPages.some((page) => page.name === to.name) + + if (!loginCredentials) { + if (bioCredentials && to.name !== 'login-bio') { + await router.replace({ name: 'login-bio', params: { username: bioCredentials } }) + return next({ name: 'login-bio', params: { username: bioCredentials } }) + } else if (authRequired && !bioCredentials && to.name !== 'login') { + await router.replace({ name: 'login' }) + return next({ name: 'login' }) + } else if (!authRequired) { + next() + } + } else { + if (!userStore.user.isConfigured) { + await userStore.checkIfUserConfigured() + } + + const isConfigured = userStore.user.isConfigured + if (configRequired && !isConfigured) { + await router.replace({ name: 'configure-biometric' }) + return next({ name: 'configure-biometric' }) + } else if (!configRequired && isConfigured) { + await router.replace({ name: 'home' }) + return next({ name: 'home' }) + } + + if (!authRequired) { + await router.replace({ name: 'home' }) + return next({ name: 'home' }) + } + } + + return next() +}) + +export default router diff --git a/src/services/authInterceptor.ts b/src/services/authInterceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9c604619f788cec3a46cf142af7a1a983cf9693 --- /dev/null +++ b/src/services/authInterceptor.ts @@ -0,0 +1,51 @@ +import type { AxiosResponse } from 'axios' +import axios, { AxiosError } from 'axios' +import router from '@/router' +import { useUserStore } from '@/stores/userStore' + +const authInterceptor = axios.create({ + baseURL: 'http://localhost:8080', + headers: { + 'Content-Type': 'application/json' + } +}) + +authInterceptor.interceptors.request.use( + (config) => { + const accessToken = sessionStorage.getItem('accessToken') + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}` + } + return config + }, + (error: AxiosError) => { + return Promise.reject(error) + } +) + +authInterceptor.interceptors.response.use( + (response: AxiosResponse) => { + return response + }, + async (error) => { + const originalRequest = error.config + if ( + (error.response.status === 401 || error.response.status === 403) && + !originalRequest._retry + ) { + originalRequest._retry = true + + sessionStorage.removeItem('accessToken') + const username = localStorage.getItem('spareStiUsername') + + if (!username) { + useUserStore().logout() + } else { + await router.push({ name: 'login-bio', params: { username: username } }) + } + } + return Promise.reject(error) + } +) + +export default authInterceptor diff --git a/src/stores/challengeStore.ts b/src/stores/challengeStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..7eb27b71f09821a97a22ee79e76055909b34a8ff --- /dev/null +++ b/src/stores/challengeStore.ts @@ -0,0 +1,77 @@ +// store/challengeStore.js +import { defineStore } from 'pinia' +import { ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' +import type { Challenge } from '@/types/challenge' + +export const useChallengeStore = defineStore('challenge', () => { + const challenges = ref<Challenge[]>([]) + + const getUserChallenges = async () => { + try { + const response = await authInterceptor('/challenges') + if (response.data && response.data.content) { + challenges.value = response.data.content + } else { + challenges.value = [] + console.error('No challenge content found:', response.data) + } + } catch (error) { + console.error('Error fetching challenges:', error) + challenges.value = [] // Ensure challenges is always an array + } + } + + // Assuming 'challenges' is a reactive state in your store that holds the list of challenges + const editUserChallenge = async (challenge: Challenge) => { + try { + const response = await authInterceptor.put(`/challenges/${challenge.id}`, challenge) + if (response.data) { + // Update local challenge state to reflect changes + const index = challenges.value.findIndex((c) => c.id === challenge.id) + if (index !== -1) { + challenges.value[index] = { ...challenges.value[index], ...response.data } + console.log('Updated Challenge:', response.data) + return challenges.value[index] + } + } else { + console.error('No challenge content found in response data') + return null + } + } catch (error) { + console.error('Error updating challenge:', error) + return null + } + } + const completeUserChallenge = async (challenge: Challenge) => { + try { + const response = await authInterceptor.put( + `/challenges/${challenge.id}/complete`, + challenge + ) + if (response.data) { + // Update local challenge state to reflect changes + const index = challenges.value.findIndex((c) => c.id === challenge.id) + if (index !== -1) { + challenges.value[index] = { ...challenges.value[index], ...response.data } + console.log('Updated Challenge:', response.data) + console.log('Challenge Completed store:', challenges.value[index]) + return challenges.value[index] + } + } else { + console.error('No challenge content found in response data') + return null + } + } catch (error) { + console.error('Error updating challenge:', error) + return null + } + } + + return { + challenges, + getUserChallenges, + editUserChallenge, + completeUserChallenge + } +}) diff --git a/src/stores/goalStore.ts b/src/stores/goalStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce033826cace512c30dacc7d9d297d6fe9e9e037 --- /dev/null +++ b/src/stores/goalStore.ts @@ -0,0 +1,60 @@ +import { defineStore } from 'pinia' +import type { Goal } from '@/types/goal' +import { ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' + +export const useGoalStore = defineStore('goal', () => { + const goals = ref<Goal[]>([]) + const priorityGoal = ref<Goal | null>(null) + const getUserGoals = async () => { + try { + const response = await authInterceptor('/goals') + if (response.data && response.data.content) { + goals.value = response.data.content + for (const goal of goals.value) { + if (goal.priority === 1) { + priorityGoal.value = goal + break + } else { + priorityGoal.value = null + } + } + console.log(response.data.content) + } else { + goals.value = [] + console.error('No goal content found:', response.data) + } + } catch (error) { + console.error('Error fetching challenges:', error) + goals.value = [] // Ensure challenges is always an array + } + } + + // Assuming 'challenges' is a reactive state in your store that holds the list of challenges + const editUserGoal = async (goal: Goal) => { + if (!goal || goal.id === null) { + console.error('Invalid goal or goal ID.') + return + } + + try { + const response = await authInterceptor.put(`/goals/${goal.id}`, goal) + if (response.data) { + const index = goals.value.findIndex((g) => g.id === goal.id) + if (index !== -1) { + goals.value[index] = { ...goals.value[index], ...response.data } + } + } else { + console.error('No goal content found in response data') + } + } catch (error) { + console.error('Error updating goal:', error) + } + } + return { + goals, + priorityGoal, + getUserGoals, + editUserGoal + } +}) diff --git a/src/stores/userConfigStore.ts b/src/stores/userConfigStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b5edbf17c9edc47d1e74e2d16d64a9bc3801305 --- /dev/null +++ b/src/stores/userConfigStore.ts @@ -0,0 +1,100 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import authInterceptor from '@/services/authInterceptor' +import { AxiosError } from 'axios' +import type { ChallengeConfig } from '@/types/challengeConfig' +import router from '@/router' + +export const useUserConfigStore = defineStore('userConfig', () => { + const challengeConfig = ref<ChallengeConfig>({ + experience: '', + motivation: '', + challengeTypeConfigs: [] + }) + + const savingAccount = ref({ + accountType: 'SAVING', + accNumber: 0, + balance: 0 + }) + + const spendingAccount = ref({ + accountType: 'SPENDING', + accNumber: 0, + balance: 0 + }) + + const errorMessage = ref<string>('') + + const setExperience = (value: string) => { + challengeConfig.value.experience = value + } + + const setMotivation = (value: string) => { + challengeConfig.value.motivation = value + } + + const addChallengeTypeConfig = ( + type: string, + specificAmount: number, + generalAmount: number + ) => { + challengeConfig.value.challengeTypeConfigs.push({ + type: type, + specificAmount: specificAmount, + generalAmount: generalAmount + }) + } + + const setAccount = (type: 'SAVING' | 'SPENDING', accNumber: number) => { + if (type === 'SAVING') { + savingAccount.value.accNumber = accNumber + } else { + spendingAccount.value.accNumber = accNumber + } + } + + const createConfig = () => { + authInterceptor + .post('/accounts', savingAccount.value) + .then(() => authInterceptor.post('/accounts', spendingAccount.value)) + .then(() => authInterceptor.post('/config/challenge', challengeConfig.value)) + .then(() => { + resetConfig() + return router.push({ name: 'home', query: { firstLogin: 'true' } }) + }) + .catch((error: AxiosError) => { + console.error(error) + resetConfig() + return router.push({ name: 'configurations1' }) + }) + } + + const resetConfig = () => { + challengeConfig.value = { + experience: '', + motivation: '', + challengeTypeConfigs: [] + } + savingAccount.value = { + accountType: 'SAVING', + accNumber: 0, + balance: 0 + } + spendingAccount.value = { + accountType: 'SPENDING', + accNumber: 0, + balance: 0 + } + } + + return { + challengeConfig, + errorMessage, + setExperience, + setMotivation, + setAccount, + addChallengeTypeConfig, + createConfig + } +}) diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f640e3e4fe2d21397328d0fa6dda1fab9890d23 --- /dev/null +++ b/src/stores/userStore.ts @@ -0,0 +1,286 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import type { User } from '@/types/user' +import router from '@/router' +import type { AxiosError } from 'axios' +import axios from 'axios' +import authInterceptor from '@/services/authInterceptor' +import type { Streak } from '@/types/streak' +import type { CredentialRequestOptions } from '@/types/CredentialRequestOptions' +import { base64urlToUint8array, initialCheckStatus, uint8arrayToBase64url } from '@/util' +import type { CredentialCreationOptions } from '@/types/CredentialCreationOptions' + +export const useUserStore = defineStore('user', () => { + const user = ref<User>({ + firstName: '', + lastName: '', + username: '', + isConfigured: false + }) + const errorMessage = ref<string>('') + const streak = ref<Streak>() + const profilePicture = ref<string>('') + + const register = async ( + firstName: string, + lastName: string, + email: string, + username: string, + password: string + ) => { + await axios + .post(`http://localhost:8080/auth/register`, { + firstName: firstName, + lastName: lastName, + email: email, + username: username, + password: password + }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + + user.value.firstName = firstName + user.value.lastName = lastName + user.value.username = username + + router.push({ name: 'configure-biometric' }) + }) + .catch((error) => { + const axiosError = error as AxiosError + errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' + }) + } + + const login = (username: string, password: string) => { + axios + .post(`http://localhost:8080/auth/login`, { + username: username, + password: password + }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + + user.value.firstName = response.data.firstName + user.value.lastName = response.data.lastName + user.value.username = response.data.username + + return authInterceptor('/profile') + }) + .then((profileResponse) => { + if (profileResponse.data.hasPasskey === true) { + localStorage.setItem('spareStiUsername', username) + } else { + localStorage.removeItem('spareStiUsername') + } + return checkIfUserConfigured() + }) + .then(() => { + user.value.isConfigured + ? router.push({ name: 'home' }) + : router.push({ name: 'configure-biometric' }) + }) + .catch((error) => { + const axiosError = error as AxiosError + errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' + }) + } + + const logout = () => { + sessionStorage.removeItem('accessToken') + localStorage.removeItem('spareStiUsername') + + user.value.firstName = '' + user.value.lastName = '' + user.value.username = '' + user.value.isConfigured = false + + router.push({ name: 'login' }) + } + + const getUserStreak = () => { + authInterceptor('/profile/streak') + .then((response) => { + streak.value = response.data + }) + .catch((error) => { + console.error('Error fetching challenges:', error) + streak.value = undefined + }) + } + + const bioRegister = async () => { + authInterceptor + .post('/auth/bioRegistration') + .then((response) => { + initialCheckStatus(response) + + const credentialCreateJson: CredentialCreationOptions = response.data + + const credentialCreateOptions: CredentialCreationOptions = { + publicKey: { + ...credentialCreateJson.publicKey, + challenge: base64urlToUint8array( + credentialCreateJson.publicKey.challenge as unknown as string + ), + user: { + ...credentialCreateJson.publicKey.user, + id: base64urlToUint8array( + credentialCreateJson.publicKey.user.id as unknown as string + ) + }, + excludeCredentials: credentialCreateJson.publicKey.excludeCredentials?.map( + (credential) => ({ + ...credential, + id: base64urlToUint8array(credential.id as unknown as string) + }) + ), + extensions: credentialCreateJson.publicKey.extensions + } + } + + return navigator.credentials.create( + credentialCreateOptions + ) as Promise<PublicKeyCredential> + }) + .then((publicKeyCredential) => { + const publicKeyResponse = + publicKeyCredential.response as AuthenticatorAttestationResponse + const encodedResult = { + type: publicKeyCredential.type, + id: publicKeyCredential.id, + response: { + attestationObject: uint8arrayToBase64url( + publicKeyResponse.attestationObject + ), + clientDataJSON: uint8arrayToBase64url(publicKeyResponse.clientDataJSON), + transports: publicKeyResponse.getTransports?.() || [] + }, + clientExtensionResults: publicKeyCredential.getClientExtensionResults() + } + + return authInterceptor.post('/auth/finishBioRegistration', { + credential: JSON.stringify(encodedResult) + }) + }) + .then(() => { + localStorage.setItem('spareStiUsername', user.value.username) + }) + .catch((error) => { + console.error(error) + }) + } + + const bioLogin = (username: string) => { + axios + .post(`http://localhost:8080/auth/bioLogin/${username}`) + .then((request) => { + initialCheckStatus(request) + + const credentialGetJson: CredentialRequestOptions = request.data + + const credentialGetOptions: CredentialRequestOptions = { + publicKey: { + ...credentialGetJson.publicKey, + allowCredentials: credentialGetJson.publicKey.allowCredentials?.map( + (credential) => ({ + ...credential, + id: base64urlToUint8array(credential.id as unknown as string) + }) + ), + challenge: base64urlToUint8array( + credentialGetJson.publicKey.challenge as unknown as string + ), + extensions: credentialGetJson.publicKey.extensions + } + } + + return navigator.credentials.get( + credentialGetOptions + ) as Promise<PublicKeyCredential> + }) + .then((publicKeyCredential) => { + const response = publicKeyCredential.response as AuthenticatorAssertionResponse + + const encodedResult = { + type: publicKeyCredential.type, + id: publicKeyCredential.id, + response: { + authenticatorData: + response.authenticatorData && + uint8arrayToBase64url(response.authenticatorData), + clientDataJSON: + response.clientDataJSON && + uint8arrayToBase64url(response.clientDataJSON), + signature: response.signature && uint8arrayToBase64url(response.signature), + userHandle: + response.userHandle && uint8arrayToBase64url(response.userHandle) + }, + clientExtensionResults: publicKeyCredential.getClientExtensionResults() + } + + return axios.post(`http://localhost:8080/auth/finishBioLogin/${username}`, { + credential: JSON.stringify(encodedResult) + }) + }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + + user.value.firstName = response.data.firstName + user.value.lastName = response.data.lastName + user.value.username = response.data.username + + router.push({ name: 'home' }) + }) + .catch((error) => { + console.error(error) + }) + } + + const checkIfUserConfigured = async () => { + await authInterceptor('/config') + .then((response) => { + user.value.isConfigured = response.data.challengeConfig != null + }) + .catch(() => { + user.value.isConfigured = false + }) + } + // Inside your store or component methods + const uploadProfilePicture = async (formData: FormData) => { + try { + const response = await authInterceptor.post('/profile/picture', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + console.log('Upload successful:', response.data) + } catch (error: any) { + console.error('Failed to upload profile picture:', error.response.data) + } + } + + const getProfilePicture = async () => { + try { + const imageResponse = await authInterceptor.get('/profile/picture', { + responseType: 'blob' + }) + profilePicture.value = URL.createObjectURL(imageResponse.data) + } catch (error: any) { + console.error('Failed to retrieve profile picture:', error.response.data) + } + } + + return { + user, + checkIfUserConfigured, + register, + login, + logout, + bioLogin, + bioRegister, + errorMessage, + getUserStreak, + streak, + uploadProfilePicture, + getProfilePicture, + profilePicture + } +}) diff --git a/src/types/CredentialCreationOptions.ts b/src/types/CredentialCreationOptions.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6fdd7a76510ca4033a1c763a2ca9545797ab1b5 --- /dev/null +++ b/src/types/CredentialCreationOptions.ts @@ -0,0 +1,3 @@ +export interface CredentialCreationOptions { + publicKey: PublicKeyCredentialCreationOptions +} diff --git a/src/types/CredentialRequestOptions.ts b/src/types/CredentialRequestOptions.ts new file mode 100644 index 0000000000000000000000000000000000000000..4349a83cdfde9e60fe294adeef87dd4f1792087c --- /dev/null +++ b/src/types/CredentialRequestOptions.ts @@ -0,0 +1,3 @@ +export interface CredentialRequestOptions { + publicKey: PublicKeyCredentialRequestOptions +} diff --git a/src/types/challenge.ts b/src/types/challenge.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b7b5ab571d851b09415ec23d61de868e9283eab --- /dev/null +++ b/src/types/challenge.ts @@ -0,0 +1,15 @@ +// Assuming the use of classes from 'class-transformer' for date handling or plain TypeScript + +export interface Challenge { + id?: number + title: string + perPurchase: number + saved: number // BigDecimal in Java, but TypeScript uses number for floating points + target: number + description: string + due: string // Mapping ZonedDateTime to Date, optional since Temporal annotation not always implies required + createdOn?: string // Mapping ZonedDateTime to Date + type?: string // Not specified as @NotNull, so it's optional + completion?: number // Assuming BigDecimal maps to number, optional due to @Transient + completedOn?: string // Adding the new variable as optional +} diff --git a/src/types/challengeConfig.ts b/src/types/challengeConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..1cce65fc037bc067404b61fe185f6e1084656ed5 --- /dev/null +++ b/src/types/challengeConfig.ts @@ -0,0 +1,9 @@ +export interface ChallengeConfig { + experience: string + motivation: string + challengeTypeConfigs: { + type: string + generalAmount: number | null + specificAmount: number | null + }[] +} diff --git a/src/types/goal.ts b/src/types/goal.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c0962f9f7b59e4e5f439e449478d1ed01ae1dbe --- /dev/null +++ b/src/types/goal.ts @@ -0,0 +1,12 @@ +export interface Goal { + id?: number + title: string + saved: number + target: number + completion?: number + description: string + priority?: number + createdOn?: string + due: string + completedOn?: string +} diff --git a/src/types/profile.ts b/src/types/profile.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d0ee92ff43589dc64d6d19507a92428d349307d --- /dev/null +++ b/src/types/profile.ts @@ -0,0 +1,21 @@ +export interface Profile { + id: number + firstName: string + lastName: string + email: string + username: string + password?: string + savedAmount?: number + spendingAccount: { + accNumber?: number + accountType?: string + balance?: number + } + savingAccount: { + accNumber?: number + accountType?: string + balance?: number + } + badges?: object[] + hasPasskey?: boolean +} diff --git a/src/types/streak.ts b/src/types/streak.ts new file mode 100644 index 0000000000000000000000000000000000000000..f49346322939e0b0517ebca400e5fcaeccddedc3 --- /dev/null +++ b/src/types/streak.ts @@ -0,0 +1,5 @@ +export interface Streak { + streakStart?: string + streak?: number + firstDue?: string +} diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fb1fe33d0d2e97d742cd7be056c18869668c44d --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,7 @@ +export interface User { + firstName: string + lastName: string + username: string + isConfigured: boolean + isBiometric?: boolean +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f6cbfc818ab356135d6acf232024019ea4fafe0 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,33 @@ +import base64js from 'base64-js' +import { type AxiosResponse } from 'axios' + +export function base64urlToUint8array(base64Bytes: string): Uint8Array { + const padding = '===='.substring(0, (4 - (base64Bytes.length % 4)) % 4) + return base64js.toByteArray((base64Bytes + padding).replace(/\//g, '_').replace(/\+/g, '-')) +} + +export function uint8arrayToBase64url(bytes: Uint8Array | ArrayBuffer | number[]): string { + if (bytes instanceof Uint8Array) { + return base64js + .fromByteArray(bytes) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + } else { + return uint8arrayToBase64url(new Uint8Array(bytes)) + } +} + +export function checkStatus(response: AxiosResponse<any>): AxiosResponse<any> | undefined { + if (response.status !== 200) { + console.log('an error occurred: ', response.statusText) + return undefined + } else { + return response + } +} + +export function initialCheckStatus(response: AxiosResponse<any>): any { + checkStatus(response) + return response.data +} diff --git a/src/utilo.js b/src/utilo.js new file mode 100644 index 0000000000000000000000000000000000000000..4125c85af520a7d429032a90f80c9a4611f07740 --- /dev/null +++ b/src/utilo.js @@ -0,0 +1,28 @@ +import base64js from 'base64-js' + +export function base64urlToUint8array(base64Bytes) { + const padding = '===='.substring(0, (4 - (base64Bytes.length % 4)) % 4) + return base64js.toByteArray((base64Bytes + padding).replace(/\//g, '_').replace(/\+/g, '-')) +} +export function uint8arrayToBase64url(bytes) { + if (bytes instanceof Uint8Array) { + return base64js + .fromByteArray(bytes) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + } else { + return uint8arrayToBase64url(new Uint8Array(bytes)) + } +} +export function checkStatus(response) { + if (response.status !== 200) { + console.log('an error occurred: ', response.body) + } else { + return response + } +} +export function initialCheckStatus(response) { + checkStatus(response) + return response.data +} diff --git a/src/views/BiometricLoginView.vue b/src/views/BiometricLoginView.vue new file mode 100644 index 0000000000000000000000000000000000000000..15d669023dc049728d7ceff61eea7e6c2c00ad20 --- /dev/null +++ b/src/views/BiometricLoginView.vue @@ -0,0 +1,30 @@ +<script lang="ts" setup> +import { useRoute } from 'vue-router' +import router from '@/router' +import { useUserStore } from '@/stores/userStore' + +const route = useRoute() +const username = route.params.username as string + +const removeBioCredential = () => { + localStorage.removeItem('spareStiUsername') + router.push({ name: 'login' }) +} + +const bioLogin = () => { + useUserStore().bioLogin(username) +} +</script> + +<template> + <div class="flex flex-col items-center h-screen gap-5 my-10"> + <h1>Hei {{ username }}, velkommen tilbake!💥</h1> + <button class="primary" @click="bioLogin">Biometrisk login</button> + <p>Ikke deg? Eller funker ikke biometrisk innlogging?</p> + <button class="primary" @click="removeBioCredential"> + Logg inn med brukernavn og passord + </button> + </div> +</template> + +<style scoped></style> diff --git a/src/views/ConfigAccountNumberView.vue b/src/views/ConfigAccountNumberView.vue new file mode 100644 index 0000000000000000000000000000000000000000..3102f5112df4b323097bb8ce7029abc5fbbe1994 --- /dev/null +++ b/src/views/ConfigAccountNumberView.vue @@ -0,0 +1,128 @@ +<template> + <div + class="flex flex-col items-center justify-center min-h-screen md:pt-10 pt-4 pb-24 text-center" + > + <h1 class="mb-8 lg:mb-12 text-4xl font-bold"> + Legg til kontonummer for sparekonto og brukskonto + </h1> + <div class="absolute bottom-0 md:bottom-40 left-0 w-40 h-40 md:w-52 md:h-52 ml-4"> + <p class="text-sm font-bold mb-3 animate-bounce">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her skriver du inn kontonummer for sparekonto og brukskonto. 🪩', + 'Sparekonto er kontoen du vil legge alle dine oppsparte penger pÃ¥!', + 'Brukskonto er kontoen du ønsker at pangene skal gÃ¥ ut fra', + 'Du kan endre dette senere hvis du ønsker det!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div + class="flex flex-col items-center justify-center bg-white rounded-lg p-8 shadow-lg w-full md:w-[45%]" + > + <div class="w-full mb-4"> + <label for="savingsAccount" class="block text-lg font-bold mb-2">Sparekonto</label> + <input + id="savingsAccount" + v-model="savingsAccount" + @input="restrictToNumbers($event as InputEvent, 'savings')" + @focus="removeFormatting('savings')" + @blur="applyFormatting('savings')" + class="w-full h-11 px-3 rounded-md text-xl focus:outline-none transition-colors border-2 border-gray-300" + type="text" + placeholder="Skriv inn ditt kontonummer..." + /> + </div> + <div class="w-full mb-4"> + <label for="spendingAccount" class="block text-lg font-bold mb-2">Brukskonto</label> + <input + id="spendingAccount" + v-model="spendingAccount" + @input="restrictToNumbers($event as InputEvent, 'spending')" + @focus="removeFormatting('spending')" + @blur="applyFormatting('spending')" + class="w-full h-11 px-3 rounded-md text-xl focus:outline-none transition-colors border-2 border-gray-300" + type="text" + placeholder="Skriv inn ditt kontonummer..." + /> + </div> + </div> + <p class="mt-10">Husk at du kan endre dette senere!</p> + <div class="absolute bottom-36 right-2"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="!isFormValid" + class="px-10 py-3 text-2xl font-bold mb-4 mr-2" + ></ContinueButtonComponent> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import { useUserConfigStore } from '@/stores/userConfigStore' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import SpareComponent from '@/components/SpareComponent.vue' + +const MAX_DIGITS = 11 +const userConfigStore = useUserConfigStore() + +const spendingAccount = ref('') +const savingsAccount = ref('') + +const isFormValid = computed(() => { + return ( + spendingAccount.value.replace(/\./g, '').length === MAX_DIGITS && + savingsAccount.value.replace(/\./g, '').length === MAX_DIGITS + ) +}) + +async function onButtonClick() { + const savingAccountNumber = parseInt(savingsAccount.value.replace(/\./g, '')) + const spendingAccountNumber = parseInt(spendingAccount.value.replace(/\./g, '')) + + userConfigStore.setAccount('SAVING', savingAccountNumber) + userConfigStore.setAccount('SPENDING', spendingAccountNumber) + + await userConfigStore.createConfig() +} + +function restrictToNumbers(event: InputEvent, type: string) { + const inputValue = (event.target as HTMLInputElement)?.value + if (inputValue !== undefined) { + const sanitizedValue = inputValue.replace(/\D/g, '') + const truncatedValue = sanitizedValue.slice(0, MAX_DIGITS) + if (type === 'spending') { + spendingAccount.value = truncatedValue + } else { + savingsAccount.value = truncatedValue + } + } +} + +function applyFormatting(type: string) { + if (type === 'spending') { + spendingAccount.value = formatAccount(spendingAccount.value) + } else { + savingsAccount.value = formatAccount(savingsAccount.value) + } +} + +function removeFormatting(type: string) { + if (type === 'spending') { + spendingAccount.value = removeFormat(spendingAccount.value) + } else { + savingsAccount.value = removeFormat(savingsAccount.value) + } +} + +function formatAccount(value: string): string { + return value.replace(/\D/g, '').replace(/^(.{4})(.{2})(.*)$/, '$1.$2.$3') +} + +function removeFormat(value: string): string { + return value.replace(/\./g, '') +} +</script> diff --git a/src/views/ConfigBiometricView.vue b/src/views/ConfigBiometricView.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a6647a0a8117882470a08a5725d7d49b0151f6b --- /dev/null +++ b/src/views/ConfigBiometricView.vue @@ -0,0 +1,27 @@ +<template> + <div class="flex flex-col justify-center items-center w-full gap-5 m-5"> + <h1>Alternativ innlogging</h1> + <h3>Vil du legge til alternativ innlogging som biometrisk autentisering?</h3> + <div class="flex flex-row justify-center gap-10"> + <img alt="bioAuthTouch" class="w-40 h-40" src="@/assets/bioAuthTouch.png" /> + <img alt="bioAuthFace" class="w-40 h-40" src="@/assets/bioAuthFace.png" /> + </div> + <div class="flex flex-col gap-5"> + <button @click="bioRegister">Legg til nÃ¥!</button> + <button @click="router.push({ name: 'configurations1' })">Jeg gjør det senere</button> + </div> + </div> +</template> +<script setup lang="ts"> +import { useUserStore } from '@/stores/userStore' +import router from '@/router' + +const userStore = useUserStore() + +const bioRegister = async () => { + await userStore.bioRegister() + await router.push({ name: 'configurations1' }) +} +</script> + +<style scoped></style> diff --git a/src/views/ConfigFamiliarWithSavingsView.vue b/src/views/ConfigFamiliarWithSavingsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..817e51b2996f978bd950e7bed8f022cca96b33df --- /dev/null +++ b/src/views/ConfigFamiliarWithSavingsView.vue @@ -0,0 +1,98 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> + <h1 class="mb-8 md:mb-16 mt-2 text-2xl font-bold sm:text-4xl"> + Hvor kjent er du med sparing fra før? + </h1> + <div class="absolute bottom-4 md:bottom-40 left-2 w-28 h-28 md:w-40 md:h-40 lg:w-52 lg:h-52 ml-4"> + <p class="md:text-sm text-xs font-bold mb-3 animate-bounce invisible sm:visible">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her kan du fylle inn hvor kjent du er med sparing fra før, slik at vi kan hjelpe deg pÃ¥ best mulig mÃ¥te! 💡', + 'Hvis du er usikker, velg det alternativet som passer best. Du kan endre dette senere!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div class="grid grid-cols-1 gap-8 mb-2 sm:gap-10 sm:mb-12 md:grid-cols-3"> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'litt', + 'border-gray-300 border-2': selectedOption !== 'litt' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('litt')" + > + <img src="@/assets/nose.png" alt="Pig nose" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-lg font-bold">Litt kjent</p> + </div> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'noe', + 'border-gray-300 border-2': selectedOption !== 'noe' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('noe')" + > + <img src="@/assets/head.png" alt="Pig face" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-lg font-bold">Noe kjent</p> + </div> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'godt', + 'border-gray-300 border-2': selectedOption !== 'godt' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('godt')" + > + <img src="@/assets/pig.png" alt="Whole pig" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-lg font-bold">Godt kjent</p> + </div> + </div> + <p class="mb-4 md:mb-10">Husk at du kan endre dette senere!</p> + <ContinueButtonComponent + :disabled="selectedOption === null" + @click="onButtonClick" + class="md:px-10 px-8 md:py-3 py-2 text-2xl self-end" + ></ContinueButtonComponent> + </div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { useUserConfigStore } from '@/stores/userConfigStore' +import SpareComponent from '@/components/SpareComponent.vue' + +const selectedOption = ref<string | null>(null) +const userConfigStore = useUserConfigStore() + +const selectOption = (option: string) => { + selectedOption.value = option + let experienceValue = '' + + switch (option) { + case 'litt': + experienceValue = 'VERY_LOW' + break + case 'noe': + experienceValue = 'MEDIUM' + break + case 'godt': + experienceValue = 'VERY_HIGH' + break + } + + userConfigStore.setExperience(experienceValue) +} + +const onButtonClick = () => { + if (selectedOption.value) { + router.push({ name: 'configurations3' }) + } else { + console.error('No option selected') + } +} +</script> diff --git a/src/views/ConfigHabitChangeView.vue b/src/views/ConfigHabitChangeView.vue new file mode 100644 index 0000000000000000000000000000000000000000..d0a5bb6a36a13c6d41b5b5ad71f11b0968331082 --- /dev/null +++ b/src/views/ConfigHabitChangeView.vue @@ -0,0 +1,98 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> + <h1 class="mb-8 md:mb-16 mt-2 text-2xl font-bold sm:text-4xl"> + Hvor store vaneedringer er du villig til Ã¥ gjøre? + </h1> + <div class="absolute bottom-4 md:bottom-40 left-2 w-28 h-28 md:w-40 md:h-40 lg:w-52 lg:h-52 ml-4"> + <p class="md:text-sm text-xs font-bold mb-3 animate-bounce invisible sm:visible">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her kan du velge hvor mye innsats du er villig til Ã¥ legge inn for Ã¥ endre vanene dine! 📚', + 'Hvis du er usikker, velg det alternativet som passer best. Du kan endre dette senere!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div class="grid grid-cols-1 gap-8 mb-2 sm:gap-10 sm:mb-12 md:grid-cols-3"> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'litt', + 'border-gray-300 border-2': selectedOption !== 'litt' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('litt')" + > + <img src="@/assets/litt.png" alt="Thumbs down emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Litt</p> + </div> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'passe', + 'border-gray-300 border-2': selectedOption !== 'passe' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('passe')" + > + <img src="@/assets/passe.png" alt="A little bit emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Passe</p> + </div> + <div + :class="{ + 'border-[var(--green)] border-4': selectedOption === 'store', + 'border-gray-300 border-2': selectedOption !== 'store' + }" + class="flex flex-col items-center justify-center w-32 h-32 p-2 sm:w-60 sm:h-60 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" + @click="selectOption('store')" + > + <img src="@/assets/store.png" alt="Thumbs up emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Store</p> + </div> + </div> + <p class="mb-4 md:mb-10">Husk at du kan endre dette senere!</p> + <ContinueButtonComponent + :disabled="selectedOption === null" + @click="onButtonClick" + class="md:px-10 px-8 md:py-3 py-2 text-2xl self-end" + ></ContinueButtonComponent> + </div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { useUserConfigStore } from '@/stores/userConfigStore' +import SpareComponent from '@/components/SpareComponent.vue' + +const selectedOption = ref<string | null>(null) +const userConfigStore = useUserConfigStore() + +const selectOption = (option: string) => { + selectedOption.value = option + let motivationValue = '' + + switch (option) { + case 'litt': + motivationValue = 'VERY_LOW' + break + case 'passe': + motivationValue = 'MEDIUM' + break + case 'store': + motivationValue = 'VERY_HIGH' + break + } + + userConfigStore.setMotivation(motivationValue) +} + +const onButtonClick = () => { + if (selectedOption.value) { + router.push({ name: 'configurations2' }) + } else { + console.error('No option selected') + } +} +</script> diff --git a/src/views/ConfigSpendingItemsAmountView.vue b/src/views/ConfigSpendingItemsAmountView.vue new file mode 100644 index 0000000000000000000000000000000000000000..4f63e2d6e2d0bc7ea450b1fa2dc959b5c5c6b041 --- /dev/null +++ b/src/views/ConfigSpendingItemsAmountView.vue @@ -0,0 +1,126 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center relative"> + <h1 class="mb-2 text-2xl font-bold sm:text-4xl"> + Hvor mye bruker du per kjøp pÃ¥ ... + </h1> + <p class="text-sm mb-8 md:mb-10">Her kan du skrive inn hvor mye du bruker per kjøp pÃ¥ ulike kategorier</p> + <div class="absolute bottom-0 md:bottom-40 left-0 w-40 h-40 md:w-52 md:h-52 ml-4"> + <p class="text-sm font-bold mb-3 animate-bounce">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her kan du skrive inn hvor mye penger du bruker per kjøp pÃ¥ ulike ting. ðŸ”', + 'For eksempel koster en kopp kaffe â˜•ï¸ kanskje 30 kr, mens en kinobillett ðŸŽŸï¸ koster 100 kr.', + 'Du kan redigere dette senere!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div class="w-full flex justify-center"> + <div :class="[showSecondBox ? 'md:grid md:grid-cols-2 md:gap-4 sm:gap-8 mb-6' : '']"> + <div + v-if="showFirstBox" + class="flex flex-col items-center bg-white rounded-lg p-4 sm:p-8 shadow-lg" + :class="showSecondBox ? 'w-full' : 'w-full md:w-1/2 mx-auto'" + :style="{ minWidth: '400px', maxWidth: '400px' }" + > + <div + v-for="(option, index) in firstBoxOptions" + :key="`first-option-${index}`" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.type }}</p> + <div class="flex items-center w-2/3"> + <input + v-model="amounts[index]" + @input="filterAmount(index, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': !amounts[index], + 'border-[var(--green)]': amounts[index] + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + <div + v-if="showSecondBox" + class="flex flex-col items-center bg-white rounded-lg p-4 sm:p-8 shadow-lg" + :class="showSecondBox ? 'w-full' : 'w-full md:w-1/2 mx-auto'" + :style="{ minWidth: '400px', maxWidth: '400px' }" + > + <div + v-for="(option, index) in secondBoxOptions" + :key="`second-option-${index}`" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.type }}</p> + <div class="flex items-center w-2/3"> + <input + v-model="amounts[index + firstBoxOptions.length]" + @input="filterAmount(index + firstBoxOptions.length, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': !amounts[index + firstBoxOptions.length], + 'border-[var(--green)]': + amounts[index + firstBoxOptions.length] + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + </div> + </div> + <p class="mt-10">Husk at du kan endre dette senere!</p> + <div class="w-full text-right"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="!isAllAmountsFilled" + class="px-10 py-3 text-2xl font-bold mb-20 mt-10 sm:mb-12 sm:mt-10" + ></ContinueButtonComponent> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { useUserConfigStore } from '@/stores/userConfigStore' +import SpareComponent from '@/components/SpareComponent.vue' + +const userConfigStore = useUserConfigStore() + +const options = ref(userConfigStore.challengeConfig.challengeTypeConfigs) +const amounts = ref(options.value.map(() => '')) + +const isAllAmountsFilled = computed(() => amounts.value.every((amount) => amount.trim() !== '')) + +const onButtonClick = () => { + options.value.forEach((option, index) => { + userConfigStore.challengeConfig.challengeTypeConfigs[index].specificAmount = + parseFloat(amounts.value[index]) || 0 + }) + router.push({ name: 'configurations5' }) +} + +const filterAmount = (index: number, event: Event) => { + const input = event.target as HTMLInputElement + let filteredValue = input.value.replace(/[^\d,]/g, '') + filteredValue = filteredValue.replace(/(,.*?),/g, '$1').replace(/,+/g, ',') + amounts.value[index] = filteredValue +} + +const firstBoxOptions = computed(() => options.value.slice(0, 6)) +const secondBoxOptions = computed(() => (options.value.length > 6 ? options.value.slice(6) : [])) + +const showFirstBox = computed(() => options.value.length > 0) +const showSecondBox = computed(() => options.value.length > 6) +</script> diff --git a/src/views/ConfigSpendingItemsTotalAmountView.vue b/src/views/ConfigSpendingItemsTotalAmountView.vue new file mode 100644 index 0000000000000000000000000000000000000000..16171fae13b9c1c4cce3aa459590a89fd4d1a3b2 --- /dev/null +++ b/src/views/ConfigSpendingItemsTotalAmountView.vue @@ -0,0 +1,127 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center relative"> + <h1 class="mb-2 text-2xl font-bold sm:text-4xl"> + Hvor mye bruker du per uke pÃ¥ ... + </h1> + <p class="text-sm mb-8 md:mb-10">Her kan du skrive inn hvor mye du bruker per uke pÃ¥ ulike kategorier</p> + <div class="absolute bottom-0 md:bottom-40 left-0 w-40 h-40 md:w-52 md:h-52 ml-4"> + <p class="text-sm font-bold mb-3 animate-bounce">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her skal du skrive inn hvor mye du bruker per uke pÃ¥ ulike kategorier. 🗓ï¸', + 'Hvis du kjøper kaffe hver dag, kan du skrive inn hvor mye du bruker pÃ¥ kaffe per uke.', + 'Du kan redigere dette senere!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div class="w-full flex justify-center"> + <div :class="[showSecondBox ? 'md:grid md:grid-cols-2 md:gap-4 sm:gap-8 mb-6' : '']"> + <div + v-if="showFirstBox" + class="flex flex-col items-center bg-white rounded-lg p-4 sm:p-8 shadow-lg" + :class="showSecondBox ? 'w-full' : 'w-full md:w-1/2 mx-auto'" + :style="{ minWidth: '400px', maxWidth: '400px' }" + > + <div + v-for="(option, index) in firstBoxOptions" + :key="`first-option-${index}`" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.type }}</p> + <div class="flex items-center w-2/3"> + <input + v-model="amounts[index]" + @input="filterAmount(index, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': !amounts[index], + 'border-[var(--green)]': amounts[index] + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + <div + v-if="showSecondBox" + class="flex flex-col items-center bg-white rounded-lg p-4 sm:p-8 shadow-lg" + :class="showSecondBox ? 'w-full' : 'w-full md:w-1/2 mx-auto'" + :style="{ minWidth: '400px', maxWidth: '400px' }" + > + <div + v-for="(option, index) in secondBoxOptions" + :key="`second-option-${index}`" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.type }}</p> + <div class="flex items-center w-2/3"> + <input + v-model="amounts[index + firstBoxOptions.length]" + @input="filterAmount(index + firstBoxOptions.length, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': !amounts[index + firstBoxOptions.length], + 'border-[var(--green)]': + amounts[index + firstBoxOptions.length] + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + </div> + </div> + <p class="mt-10">Husk at du kan endre dette senere!</p> + <div class="w-full text-right"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="!isAllAmountsFilled" + class="px-10 py-3 text-2xl font-bold mb-20 mt-10 sm:mb-12 sm:mt-10" + ></ContinueButtonComponent> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { useUserConfigStore } from '@/stores/userConfigStore' +import SpareComponent from '@/components/SpareComponent.vue' + +const userConfigStore = useUserConfigStore() + +const options = ref(userConfigStore.challengeConfig.challengeTypeConfigs) +const amounts = ref(options.value.map(() => '')) + +const isAllAmountsFilled = computed(() => amounts.value.every((amount) => amount.trim() !== '')) + +const onButtonClick = async () => { + options.value.forEach((option, index) => { + userConfigStore.challengeConfig.challengeTypeConfigs[index].generalAmount = + parseFloat(amounts.value[index]) || 0 + }) + + await router.push({ name: 'configurations6' }) +} + +const filterAmount = (index: number, event: Event) => { + const input = event.target as HTMLInputElement + let filteredValue = input.value.replace(/[^\d,]/g, '') + filteredValue = filteredValue.replace(/(,.*?),/g, '$1').replace(/,+/g, ',') + amounts.value[index] = filteredValue +} + +const firstBoxOptions = computed(() => options.value.slice(0, 6)) +const secondBoxOptions = computed(() => (options.value.length > 6 ? options.value.slice(6) : [])) + +const showFirstBox = computed(() => options.value.length > 0) +const showSecondBox = computed(() => options.value.length > 6) +</script> diff --git a/src/views/ConfigSpendingItemsView.vue b/src/views/ConfigSpendingItemsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..c983f3e100f9036265fae0c93a746762a7f23f0d --- /dev/null +++ b/src/views/ConfigSpendingItemsView.vue @@ -0,0 +1,138 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen text-center"> + <h1 class="mb-3 text-2xl font-bold sm:text-4xl mt-0 md:mt-7">Hva bruker du mye penger pÃ¥?</h1> + <p class="text-sm mb-8 md:mb-10">Hvis du ikke finner noe som passer, kan du skrive inn egne kategorier i "Annet ..." feltet</p> + <div class="absolute bottom-0 md:bottom-40 left-0 w-40 h-40 md:w-52 md:h-52 ml-4"> + <p class="text-sm font-bold mb-3 animate-bounce">Trykk pÃ¥ meg for hjelp â—ï¸</p> + <SpareComponent + :speech="[ + 'Her kan du velge hva du bruker mye penger pÃ¥, slik at vi kan hjelpe deg med Ã¥ spare penger! 💸', + 'Hvis du ikke finner noe som passer, kan du skrive inn egne kategorier i \'Annet ...\' feltet', + 'Du mÃ¥ minst velge en kategori!', + 'Du kan redigere dette senere!' + ]" + :png-size="10" + :direction="'right'" + :imageDirection="'right'" + ></SpareComponent> + </div> + <div class="flex flex-wrap justify-center gap-8 mb-8"> + <div + class="flex flex-col items-center justify-center bg-white rounded-lg sm:p-8 shadow-lg sm:w-full md:w-[45%]" + > + <div + v-for="buttonText in [ + 'Kaffe', + 'Snus', + 'Kantina', + 'Sigaretter', + 'Transport', + 'Klær' + ]" + :key="buttonText" + class="w-full my-4" + > + <button + :class="[ + 'w-full md:w-64 h-11 rounded-md text-xl font-bold', + selectedOptions.includes(buttonText) + ? 'border-2 border-[var(--green)]' + : 'border-2 border-gray-300' + ]" + @click="toggleOption(buttonText)" + style="background: transparent" + > + {{ buttonText }} + </button> + </div> + </div> + <div + class="flex flex-col items-center justify-center bg-white rounded-lg sm:p-8 shadow-lg sm:w-full md:w-[45%]" + > + <div + v-for="(option, index) in customOptions" + :key="`custom-${index}`" + class="w-full my-4" + > + <input + v-model="customOptions[index]" + :class="[ + 'w-full md:w-64 h-11 px-3 rounded-md text-xl focus:outline-none transition-colors border-2', + customOptions[index].trim() !== '' + ? 'border-[var(--green)]' + : 'border-gray-300' + ]" + type="text" + :placeholder="'Annet ' + ' ...'" + /> + </div> + </div> + </div> + <p class="mb-1">Husk at du kan endre dette senere!</p> + <div class="w-full text-right"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="!isFormValid" + class="px-10 py-3 text-2xl font-bold mt-36 mr-4 sm:mb-12 sm:mt-10" + ></ContinueButtonComponent> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { useUserConfigStore } from '@/stores/userConfigStore' +import SpareComponent from '@/components/SpareComponent.vue' + +const userConfigStore = useUserConfigStore() +const selectedOptions = ref<string[]>([]) +const customOptions = ref(['', '', '', '', '', '']) + +const toggleOption = (option: string) => { + const index = selectedOptions.value.indexOf(option) + if (index === -1) { + selectedOptions.value.push(option) + } else { + selectedOptions.value.splice(index, 1) + } +} + +const isFormValid = computed(() => { + const predefinedSelected = selectedOptions.value.length > 0 + const customFilled = customOptions.value.some((option) => option.trim() !== '') + return predefinedSelected || customFilled +}) + +const onButtonClick = () => { + if (!isFormValid.value) { + console.error('Form is not valid') + return + } + + const predefinedChallengeTypes = selectedOptions.value.map((option) => ({ + type: option, + specificAmount: 0, + generalAmount: 0 + })) + + const customChallengeTypes = customOptions.value + .filter((option) => option.trim() !== '') + .map((option) => ({ + type: option, + specificAmount: 0, + generalAmount: 0 + })) + + for (const challengeType of predefinedChallengeTypes.concat(customChallengeTypes)) { + userConfigStore.addChallengeTypeConfig( + challengeType.type, + challengeType.specificAmount, + challengeType.generalAmount + ) + } + + router.push({ name: 'configurations4' }) +} +</script> diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ed9f5db1efad7289ddd4a610a19d787e631be53 --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,90 @@ +<template> + <div class="flex flex-col items-center max-h-[60vh] md:flex-row md:max-h-[80vh] mx-auto"> + <div class="flex flex-col basis-1/3 order-last md:order-first md:basis-1/4 md:pl-1 mt-10"> + <SpareComponent + :speech="speech" + :show="showWelcome" + :png-size="12" + :direction="'right'" + :imageDirection="'right'" + class="my-10 md:ml-5" + ></SpareComponent> + <div class="flex flex-col gap-2 items-center mx-auto mt-4 mb-20 md:gap-4 md:m-0 md:ml-4 w-full"> + <ButtonAddGoalOrChallenge :buttonText="'Legg til sparemÃ¥l'" :type="'goal'" /> + <ButtonAddGoalOrChallenge + :buttonText="'Legg til spareutfordring'" + :type="'challenge'" + /> + <ButtonAddGoalOrChallenge + :buttonText="'Generer spareutfordring'" + :type="'generatedChallenge'" + :showModal="showModal" + @click="showModal = true" + @update:showModal="showModal = $event" + /> + </div> + </div> + <savings-path v-if="isMounted" :challenges="challenges" :goal="goal"></savings-path> + </div> + <GeneratedChallengesModal v-show="showModal" @update:showModal="showModal = $event" /> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import ButtonAddGoalOrChallenge from '@/components/ButtonAddGoalOrChallenge.vue' +import type { Challenge } from '@/types/challenge' +import type { Goal } from '@/types/goal' +import { useGoalStore } from '@/stores/goalStore' +import { useChallengeStore } from '@/stores/challengeStore' +import SavingsPath from '@/components/SavingsPath.vue' +import router from '@/router' +import GeneratedChallengesModal from '@/components/GeneratedChallengesModal.vue' +import SpareComponent from '@/components/SpareComponent.vue' + +const showModal = ref(false) + +const goalStore = useGoalStore() +const challengeStore = useChallengeStore() +const speech = ref<string[]>([]) + +const challenges = ref<Challenge[]>([]) +const showWelcome = ref<boolean>(false) + +const goal = ref<Goal | null | undefined>(null) +const isMounted = ref(false) + +onMounted(async () => { + await goalStore.getUserGoals() + await challengeStore.getUserChallenges() + challenges.value = challengeStore.challenges + goal.value = goalStore.priorityGoal + const lastModalShow = localStorage.getItem('lastModalShow') + if (!lastModalShow || Date.now() - Number(lastModalShow) >= 24 * 60 * 60 * 1000) { + showModal.value = true + } + firstLoggedInSpeech() + SpareSpeech() + isMounted.value = true +}) + +const firstLoggedInSpeech = () => { + const isFirstLogin = router.currentRoute.value.query.firstLogin === 'true' + if (isFirstLogin) { + showWelcome.value = true + speech.value.push('Hei, jeg er Spare!') + speech.value.push('Jeg skal hjelpe deg med Ã¥ spare penger.') + speech.value.push('Trykk pÃ¥ meg for Ã¥ høre hva jeg har Ã¥ si ðŸ·') + speech.value.push('Trenger du hjelp? Trykk pÃ¥ â“ nede i høyre hjørne') + router.replace({ name: 'home', query: { firstLogin: 'false' } }) + } +} + +const SpareSpeech = () => { + speech.value = [ + 'Hei! Jeg er sparegrisen, Spare!', + 'Valkommen til SpareSti 👑', + 'Du kan trykke pÃ¥ meg for Ã¥ høre hva jeg har Ã¥ si ðŸ·', + 'Trenger du hjelp? Trykk pÃ¥ â“ nede i høyre hjørne' + ] +} +</script> diff --git a/src/views/ManageChallengeView.vue b/src/views/ManageChallengeView.vue new file mode 100644 index 0000000000000000000000000000000000000000..543cbd8fa03fc8cc5c382774626bce40a2e88012 --- /dev/null +++ b/src/views/ManageChallengeView.vue @@ -0,0 +1,331 @@ +<script lang="ts" setup> +import { useRouter } from 'vue-router' +import { computed, onMounted, ref, watch } from 'vue' +import ProgressBar from '@/components/ProgressBar.vue' +import authInterceptor from '@/services/authInterceptor' +import type { Challenge } from '@/types/challenge' +import ModalComponent from '@/components/ModalComponent.vue' +import InteractiveSpare from '@/components/InteractiveSpare.vue' + +const router = useRouter() +const uploadedFile = ref<File | null>(null) + +const modalTitle = ref('') +const modalMessage = ref('') +const confirmModalOpen = ref(false) +const errorModalOpen = ref(false) + +const oneWeekFromNow = new Date() +oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7) +const minDate = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().slice(0, 10) +const selectedDate = ref<string>(minDate) + +const thirtyDaysFromNow = new Date() +thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30) +const maxDate = thirtyDaysFromNow.toISOString().slice(0, 10) + +const challengeInstance = ref<Challenge>({ + title: '', + perPurchase: 0, + saved: 0, + target: 0, + description: '', + due: '' +}) + +watch(selectedDate, (newDate) => { + challengeInstance.value.due = newDate +}) + +const isEdit = computed(() => router.currentRoute.value.name === 'edit-challenge') +const pageTitle = computed(() => (isEdit.value ? 'Rediger utfordring🎨' : 'Ny utfordring🎨')) +const submitButton = computed(() => (isEdit.value ? 'Oppdater' : 'Opprett')) +const completion = computed( + () => (challengeInstance.value.saved / challengeInstance.value.target) * 100 +) + +function validateInputs() { + const errors = [] + + challengeInstance.value.due = selectedDate.value + 'T23:59:59.999Z' + + if (!challengeInstance.value.title || challengeInstance.value.title.length > 20) { + errors.push('Tittelen mÃ¥ være mellom 1 og 20 tegn.') + } + if (challengeInstance.value.description.length > 280) { + errors.push('Beskrivelsen mÃ¥ være under 280 tegn.') + } + if (challengeInstance.value.target <= 0) { + errors.push('MÃ¥lbeløpet mÃ¥ være større enn 0.') + } + if (new Date(challengeInstance.value.due) < new Date(minDate)) { + errors.push('Forfallsdatoen mÃ¥ være minst en uke frem i tid.') + } + if (challengeInstance.value.perPurchase <= 0) { + errors.push('Pris per sparing mÃ¥ være større enn 0.') + } + return errors +} + +const handleFileChange = (event: Event) => { + const target = event.target as HTMLInputElement + if (target.files && target.files.length > 0) { + uploadedFile.value = target.files[0] + } else { + uploadedFile.value = null + } +} + +const submitAction = async () => { + const errors = validateInputs() + if (errors.length > 0) { + const formatErrors = errors.join('\n') + modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨' + modalMessage.value = formatErrors + errorModalOpen.value = true + return + } + try { + let response + if (isEdit.value) { + response = await updateChallenge() + } else { + response = await createChallenge() + } + + const challengeId = isEdit.value ? challengeInstance.value.id : response.id + + if (uploadedFile.value && challengeId) { + const formData = new FormData() + formData.append('file', uploadedFile.value) + formData.append('id', challengeId.toString()) + + await authInterceptor.post('/challenges/picture', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + } + + await router.push({ name: 'challenges' }) + } catch (error) { + console.error('Error during challenge submission:', error) + modalTitle.value = 'Systemfeil' + modalMessage.value = 'En feil oppstod under lagring av utfordringen.' + errorModalOpen.value = true + } +} + +onMounted(async () => { + if (isEdit.value) { + const challengeId = router.currentRoute.value.params.id + if (!challengeId) return router.push({ name: 'challenges' }) + + await authInterceptor(`/challenges/${challengeId}`) + .then((response) => { + if (response.data.completedOn) { + router.push({ name: 'challenges' }) + } + + challengeInstance.value = response.data + selectedDate.value = response.data.due.slice(0, 16) + }) + .catch((error) => { + console.error(error) + router.push({ name: 'challenges' }) + }) + } +}) + +const createChallenge = async () => { + const response = await authInterceptor.post('/challenges', challengeInstance.value) + return response.data +} + +const updateChallenge = async () => { + const response = await authInterceptor.put( + `/challenges/${challengeInstance.value.id}`, + challengeInstance.value + ) + return response.data +} + +const cancelCreation = () => { + if ( + challengeInstance.value.title !== '' || + challengeInstance.value.description !== '' || + challengeInstance.value.perPurchase !== 0 || + challengeInstance.value.saved !== 0 || + challengeInstance.value.target !== 0 + ) { + modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨' + modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?' + confirmModalOpen.value = true + } else { + router.push({ name: 'challenges' }) + } +} + +const confirmCancel = () => { + router.push({ name: 'challenges' }) + confirmModalOpen.value = false +} + +const removeUploadedFile = () => { + uploadedFile.value = null +} +</script> + +<template> + <div class="relative flex-1"> + <h1 class="font-bold flex justify-center items-center" v-text="pageTitle" /> + <div class="flex md:flex-row flex-col justify-center md:items-start items-center"> + <div class="flex flex-col gap-5 items-center justify-center"> + <div class="flex flex-col"> + <p class="mx-4">Tittel*</p> + <input + v-model="challengeInstance.title" + placeholder="Skriv en tittel" + type="text" + /> + </div> + + <div class="flex flex-col"> + <p class="mx-4">Beskrivelse (valgfri)</p> + <textarea + v-model="challengeInstance.description" + class="w-80 h-20 no-rezise" + placeholder="Beskriv sparemÃ¥let" + /> + </div> + + <div class="flex flex-col sm:flex-row gap-3"> + <div class="flex flex-col"> + <p class="mx-4">Spare per gang</p> + <input + v-model="challengeInstance.perPurchase" + class="w-40 text-right" + placeholder="Kr spart per sparing" + type="number" + /> + </div> + + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Kroner spart💸</p> + </div> + <input + v-model="challengeInstance.saved" + class="w-40 text-right" + placeholder="Sparebeløp" + type="number" + /> + </div> + + <div class="flex flex-col"> + <p class="mx-4">Av mÃ¥lbeløp💯*</p> + <input + v-model="challengeInstance.target" + class="w-40 text-right" + placeholder="MÃ¥lbeløp" + type="number" + /> + </div> + </div> + <ProgressBar :completion="completion" /> + + <div class="flex flex-row gap-4"> + <div class="flex flex-col"> + <p class="mx-4">Forfallsdato*</p> + <input + :min="minDate" + v-model="selectedDate" + placeholder="Forfallsdato" + type="date" + /> + </div> + <div class="flex flex-col items-center"> + <p>Last opp ikon for utfordringen📸</p> + <label + for="fileUpload" + class="bg-white text-black text-lg cursor-pointer leading-none rounded-full border p-3 border-black" + > + Legg til 💾 + </label> + <input + id="fileUpload" + type="file" + accept=".jpg, .png" + hidden + @change="handleFileChange" + /> + <div v-if="uploadedFile" class="flex justify-center items-center mt-2"> + <p class="text-sm">{{ uploadedFile.name }}</p> + <button + @click="removeUploadedFile" + class="ml-2 text-xs font-bold border-2 p-1 rounded text-red-500" + > + Fjern fil + </button> + </div> + </div> + </div> + + <div class="flex flex-row justify-between w-full"> + <button class="primary danger" @click="cancelCreation" v-text="'Avbryt'" /> + + <button class="primary" @click="submitAction" v-text="submitButton" /> + </div> + <ModalComponent + :title="modalTitle" + :message="modalMessage" + :isModalOpen="errorModalOpen" + @close="errorModalOpen = false" + > + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="errorModalOpen = false"> + Lukk + </button> + </div> + </div> + </template> + </ModalComponent> + + <ModalComponent + :title="modalTitle" + :message="modalMessage" + :isModalOpen="confirmModalOpen" + @close="confirmModalOpen = false" + > + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="confirmCancel">Bekreft</button> + <button class="primary danger" @click="confirmModalOpen = false"> + Avbryt + </button> + </div> + </div> + </template> + </ModalComponent> + </div> + </div> + <div + class="lg:absolute right-5 lg:top-1/3 max-lg:bottom-0 max-lg:mt-44 transform -translate-y-1/2 lg:w-1/4 lg:max-w-xs" + > + <InteractiveSpare + :png-size="10" + :speech="[ + `Trenger du hjelp? Trykk pÃ¥ â“ nede i høyre hjørne!` + ]" + direction="left" + /> + </div> + </div> +</template> + +<style scoped> +.no-rezise { + resize: none; +} +</style> diff --git a/src/views/ManageConfigView.vue b/src/views/ManageConfigView.vue new file mode 100644 index 0000000000000000000000000000000000000000..2a5ad08eaf1f68ff0d5e299965567082fb8c635e --- /dev/null +++ b/src/views/ManageConfigView.vue @@ -0,0 +1,194 @@ +<script lang="ts" setup> +import authInterceptor from '@/services/authInterceptor' +import CardTemplate from '@/components/CardTemplate.vue' +import type { ChallengeConfig } from '@/types/challengeConfig' +import { onMounted, ref } from 'vue' +import ModalComponent from '@/components/ModalComponent.vue' +import router from '@/router' + +const configuration = ref<ChallengeConfig>({ + motivation: '', + experience: '', + challengeTypeConfigs: [ + { + type: 'Kaffe', + generalAmount: 100, + specificAmount: 10 + } + ] +}) + +const error = ref<string | null>(null) + +const deleteChallengeType = (type: string) => { + if (configuration.value.challengeTypeConfigs) { + configuration.value.challengeTypeConfigs = configuration.value.challengeTypeConfigs.filter( + (item) => item.type !== type + ) + } +} + +const createChallengeType = () => { + configuration.value.challengeTypeConfigs?.push({ + type: '', + specificAmount: null, + generalAmount: null + }) +} + +const validateAndSave = () => { + if (!configuration.value.motivation) { + return (error.value = 'Du mÃ¥ velge hvor store vaneendringer du er villig til Ã¥ gjøre') + } + + if (!configuration.value.experience) { + return (error.value = 'Du mÃ¥ velge hvor kjent du er med sparing fra før av') + } + + if (configuration.value.challengeTypeConfigs.length == 0) { + return (error.value = 'Du mÃ¥ legge til minst én ting du bruker mye penger pÃ¥') + } + + if ( + configuration.value.challengeTypeConfigs.some( + (item) => !item.type || !item.specificAmount || !item.generalAmount + ) + ) { + return (error.value = 'Du mÃ¥ fylle ut alle feltene for ting du bruker mye penger pÃ¥') + } + + if ( + configuration.value.challengeTypeConfigs.some( + (item) => + (item.specificAmount && item.specificAmount < 0) || + (item.generalAmount && item.generalAmount < 0) + ) + ) { + return (error.value = 'Prisene kan ikke være negative') + } + + saveConfiguration() +} + +const saveConfiguration = () => { + authInterceptor + .put('/config/challenge', configuration.value) + .then(() => { + router.push({ name: 'profile' }) + }) + .catch((error) => { + error.value = error.response.data.message + }) +} + +onMounted(() => { + authInterceptor('/config/challenge') + .then((response) => { + configuration.value = response.data + }) + .catch((error) => { + console.error(error) + }) +}) +</script> + +<template> + <div class="w-full flex px-10 justify-center"> + <div class="flex flex-col justify-center items-center max-w-screen-xl gap-3"> + <h1>Rediger konfigurasjonâœï¸</h1> + + <h3 class="font-bold">Hvor store vaneedringer er du villig til Ã¥ gjøre?</h3> + <div v-if="configuration" class="flex flex-row gap-5"> + <CardTemplate + :class="{ 'border-2 border-lime-400': configuration.motivation === 'VERY_LOW' }" + class="cursor-pointer p-4 border-2" + @click="configuration.motivation = 'VERY_LOW'" + > + <p class="font-bold">Litt</p> + </CardTemplate> + <CardTemplate + :class="{ 'border-2 border-lime-400': configuration.motivation === 'MEDIUM' }" + class="cursor-pointer p-4 border-2" + @click="configuration.motivation = 'MEDIUM'" + > + <p class="font-bold">Passe</p> + </CardTemplate> + <CardTemplate + :class="{ + 'border-2 border-lime-400': configuration.motivation === 'VERY_HIGH' + }" + class="cursor-pointer p-4 border-2" + @click="configuration.motivation = 'VERY_HIGH'" + > + <p class="font-bold">Store</p> + </CardTemplate> + </div> + + <h3 class="font-bold">Hvor kjent er du med sparing fra før av?</h3> + <div v-if="configuration" class="flex flex-row gap-5"> + <CardTemplate + :class="{ 'border-2 border-lime-400': configuration.experience === 'VERY_LOW' }" + class="cursor-pointer p-4 border-2" + @click="configuration.experience = 'VERY_LOW'" + > + <p class="font-bold">Litt kjent</p> + </CardTemplate> + <CardTemplate + :class="{ 'border-2 border-lime-400': configuration.experience === 'MEDIUM' }" + class="cursor-pointer p-4 border-2" + @click="configuration.experience = 'MEDIUM'" + > + <p class="font-bold">Noe kjent</p> + </CardTemplate> + <CardTemplate + :class="{ + 'border-2 border-lime-400': configuration.experience === 'VERY_HIGH' + }" + class="cursor-pointer p-4 border-2" + @click="configuration.experience = 'VERY_HIGH'" + > + <p class="font-bold">Godt kjent</p> + </CardTemplate> + </div> + + <h3 class="font-bold my-0">Hva bruker du mye penger pÃ¥?</h3> + <div class="flex flex-col gap-4 p-4 items-center"> + <CardTemplate + v-for="(item, index) in configuration.challengeTypeConfigs" + :key="index" + class="flex flex-row flex-wrap justify-center gap-5 border-2 p-4" + > + <input v-model="item.type" placeholder="Type" type="text" /> + <input + v-model="item.specificAmount" + placeholder="Generell pris" + type="number" + /> + <input v-model="item.generalAmount" placeholder="Pris per uke" type="number" /> + <button + class="primary danger w-min items-center" + @click="deleteChallengeType(item.type)" + v-text="'Slett'" + /> + </CardTemplate> + <button + class="font-bold text-2xl cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-100 justify-start" + @click="createChallengeType" + v-text="'Legg til flereðŸ“'" + /> + </div> + + <div class="flex flex-row justify-center gap-5"> + <button class="primary danger" @click="router.back()">Avbryt</button> + <button class="primary" @click="validateAndSave">Lagre</button> + </div> + </div> + + <ModalComponent v-if="error"> + <p class="my-4" v-text="error" /> + <button @click="error = null">Lukk</button> + </ModalComponent> + </div> +</template> + +<style scoped></style> diff --git a/src/views/ManageGoalView.vue b/src/views/ManageGoalView.vue new file mode 100644 index 0000000000000000000000000000000000000000..2fbe057119d5d05989ffd3bbe4074edca6fd05bb --- /dev/null +++ b/src/views/ManageGoalView.vue @@ -0,0 +1,354 @@ +<script lang="ts" setup> +import { useRouter } from 'vue-router' +import { computed, onMounted, type Ref, ref, watch } from 'vue' +import type { Goal } from '@/types/goal' +import ProgressBar from '@/components/ProgressBar.vue' +import authInterceptor from '@/services/authInterceptor' +import ModalComponent from '@/components/ModalComponent.vue' +import InteractiveSpare from '@/components/InteractiveSpare.vue' + +const router = useRouter() +const uploadedFile: Ref<File | null> = ref(null) + +const minDate = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().slice(0, 10) +const selectedDate = ref<string>(minDate) + +const modalMessage = ref<string>('') +const modalTitle = ref<string>('') +const errorModalOpen = ref<boolean>(false) +const confirmModalOpen = ref<boolean>(false) + +const goalInstance = ref<Goal>({ + title: '', + saved: 0, + target: 0, + description: '', + due: '' +}) + +watch(selectedDate, (newDate) => { + goalInstance.value.due = newDate +}) + +const isEdit = computed(() => router.currentRoute.value.name === 'edit-goal') +const pageTitle = computed(() => (isEdit.value ? 'Rediger sparemÃ¥l🎨' : 'Nytt sparemÃ¥l🎨')) +const submitButton = computed(() => (isEdit.value ? 'Oppdater' : 'Opprett')) +const completion = computed(() => (goalInstance.value.saved / goalInstance.value.target) * 100) + +function validateInputs() { + const errors = [] + + goalInstance.value.due = selectedDate.value + 'T23:59:59.999Z' + + if (!goalInstance.value.title) { + errors.push('Tittel mÃ¥ fylles ut') + } + if (!goalInstance.value.target) { + errors.push('MÃ¥lbeløp mÃ¥ fylles ut') + } + if (!goalInstance.value.due) { + errors.push('Forfallsdato mÃ¥ fylles ut') + } + + if (goalInstance.value.target < 1) { + errors.push('MÃ¥lbeløp mÃ¥ være større enn 0') + } + + if (goalInstance.value.saved < 0) { + errors.push('Sparebeløp kan ikke være negativt') + } + + if (goalInstance.value.saved > goalInstance.value.target) { + errors.push('Sparebeløp kan ikke være større enn mÃ¥lbeløp') + } + + return errors +} + +const submitAction = async () => { + const errors = validateInputs() + if (errors.length > 0) { + const formatErrors = errors.join('<br>') + modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨' + modalMessage.value = formatErrors + errorModalOpen.value = true + return + } + + try { + let response + + if (isEdit.value) { + response = await updateGoal() + } else { + response = await createGoal() + } + + const goalId = isEdit.value ? goalInstance.value.id : response.id // Adjusted to handle the returned data + + if (uploadedFile.value && goalId) { + const formData = new FormData() + formData.append('file', uploadedFile.value) + formData.append('id', goalId.toString()) + + await authInterceptor.post('/goals/picture', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + } + + await router.push({ name: 'goals' }) + } catch (error) { + console.error('Error during goal submission:', error) + modalTitle.value = 'Systemfeil' + modalMessage.value = 'En feil oppstod under lagring av utfordringen.' + errorModalOpen.value = true + } +} + +watch(selectedDate, (newDate) => { + console.log(newDate) +}) + +onMounted(async () => { + if (isEdit.value) { + const goalId = router.currentRoute.value.params.id + if (!goalId) return router.push({ name: 'goals' }) + + await authInterceptor(`/goals/${goalId}`) + .then((response) => { + goalInstance.value = response.data + selectedDate.value = response.data.due.slice(0, 10) + }) + .catch((error) => { + console.error(error) + router.push({ name: 'goals' }) + }) + } else { + goalInstance.value.due = selectedDate.value + } +}) + +const createGoal = async (): Promise<any> => { + try { + const response = await authInterceptor.post('/goals', goalInstance.value) + return response.data // Ensure the response data is returned + } catch (error) { + console.error('Failed to create goal:', error) + throw error // Rethrow the error to handle it in the submitAction method + } +} + +const updateGoal = async (): Promise<any> => { + try { + const response = await authInterceptor.put( + `/goals/${goalInstance.value.id}`, + goalInstance.value + ) + return response.data // Ensure the response data is returned + } catch (error) { + console.error('Failed to update goal:', error) + throw error // Rethrow the error to handle it in the submitAction method + } +} + +const deleteGoal = () => { + authInterceptor + .delete(`/goals/${goalInstance.value.id}`) + .then(() => { + router.push({ name: 'goals' }) + }) + .catch((error) => { + console.error(error) + }) +} + +function cancelCreation() { + if ( + goalInstance.value.title !== '' || + goalInstance.value.description !== '' || + goalInstance.value.target !== 0 || + selectedDate.value !== '' + ) { + modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨' + modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?' + confirmModalOpen.value = true + } else { + router.push({ name: 'goals' }) + } +} + +const confirmCancel = () => { + router.push({ name: 'goals' }) + confirmModalOpen.value = false +} + +const handleFileChange = (event: Event) => { + const target = event.target as HTMLInputElement + if (target.files && target.files.length > 0) { + uploadedFile.value = target.files[0] // Save the first selected file + } else { + uploadedFile.value = null + } +} + +const removeUploadedFile = () => { + uploadedFile.value = null +} + +onMounted(async () => { + if (isEdit.value) { + const goalId = router.currentRoute.value.params.id + if (!goalId) return router.push({ name: 'goals' }) + + await authInterceptor(`/goals/${goalId}`) + .then((response) => { + goalInstance.value = response.data + selectedDate.value = response.data.due.slice(0, 16) + }) + .catch((error) => { + console.error(error) + router.push({ name: 'goals' }) + }) + } +}) +</script> + +<template> + <div class="relative flex-1 min-h-screen"> + <h1 class="font-bold flex justify-center items-center" v-text="pageTitle" /> + <div class="flex md:flex-row flex-col justify-center md:items-start items-center"> + <div class="flex flex-col gap-5 items-center justify-center"> + <div class="flex flex-col"> + <p class="mx-4">Tittel*</p> + <input v-model="goalInstance.title" placeholder="Skriv en tittel" type="text" /> + </div> + + <div class="flex flex-col"> + <p class="mx-4">Beskrivelse (valgfri)</p> + <textarea + v-model="goalInstance.description" + class="w-80 h-20 no-rezise" + placeholder="Beskriv sparemÃ¥let" + /> + </div> + + <div class="flex flex-col sm:flex-row gap-3"> + <div class="flex flex-col"> + <p class="mx-4">Kroner spart💸</p> + <input + v-model="goalInstance.saved" + class="w-40 text-right" + placeholder="Sparebeløp" + type="number" + /> + </div> + + <div class="flex flex-col"> + <p class="mx-4">Av mÃ¥lbeløp💯*</p> + <input + v-model="goalInstance.target" + class="w-40 text-right" + placeholder="MÃ¥lbeløp" + type="number" + /> + </div> + </div> + <ProgressBar :completion="completion" /> + + <div class="flex flex-row gap-4"> + <div class="flex flex-col"> + <p class="mx-4">Forfallsdato*</p> + <input + :min="minDate" + v-model="selectedDate" + placeholder="Forfallsdato" + type="date" + /> + </div> + <div class="flex flex-col items-center"> + <p>Last opp ikon for utfordringen📸</p> + <label + for="fileUpload" + class="bg-white text-black text-lg cursor-pointer leading-none rounded-full border p-3 border-black" + > + Legg til 💾 + </label> + <input + id="fileUpload" + type="file" + accept=".jpg, .png" + hidden + @change="handleFileChange" + /> + <div v-if="uploadedFile" class="flex justify-center items-center mt-2"> + <p class="text-sm">{{ uploadedFile.name }}</p> + <button + @click="removeUploadedFile" + class="ml-2 text-xs font-bold border-2 p-1 rounded text-red-500" + > + Fjern fil + </button> + </div> + </div> + </div> + + <div class="flex flex-row justify-between w-full"> + <button + v-if="isEdit" + class="ml-2 primary danger" + @click="deleteGoal" + v-text="'Slett'" + /> + <button + v-else + class="ml-2 primary danger" + @click="cancelCreation" + v-text="'Avbryt'" + /> + <button class="primary" @click="submitAction" v-text="submitButton" /> + </div> + <ModalComponent + :title="modalTitle" + :message="modalMessage" + :isModalOpen="errorModalOpen" + @close="errorModalOpen = false" + > + <template v-slot:buttons> + <button class="primary" @click="errorModalOpen = false">Lukk</button> + </template> + </ModalComponent> + + <ModalComponent + :title="modalTitle" + :message="modalMessage" + :isModalOpen="confirmModalOpen" + @close="confirmModalOpen = false" + > + <template v-slot:buttons> + <button class="primary" @click="confirmCancel">Bekreft</button> + <button class="primary danger" @click="confirmModalOpen = false"> + Avbryt + </button> + </template> + </ModalComponent> + </div> + <div + class="lg:absolute right-5 lg:top-1/4 max-lg:bottom-0 max-lg:mt-44 transform -translate-y-1/2 lg:w-1/4 lg:max-w-xs" + > + <InteractiveSpare + :png-size="10" + :speech="[ + `Trenger du hjelp? Trykk pÃ¥ â“ nede i høyre hjørne!` + ]" + direction="left" + /> + </div> + </div> + </div> +</template> + +<style scoped> +.no-rezise { + resize: none; +} +</style> diff --git a/src/views/ManageProfileView.vue b/src/views/ManageProfileView.vue new file mode 100644 index 0000000000000000000000000000000000000000..470edfffaae114078d0ae427ee241ebddb223575 --- /dev/null +++ b/src/views/ManageProfileView.vue @@ -0,0 +1,272 @@ +<script lang="ts" setup> +import authInterceptor from '@/services/authInterceptor' +import { computed, onMounted, ref } from 'vue' +import type { Profile } from '@/types/profile' +import CardTemplate from '@/components/CardTemplate.vue' +import router from '@/router' +import ToolTip from '@/components/ToolTip.vue' +import InteractiveSpare from '@/components/InteractiveSpare.vue' + +const profile = ref<Profile>({ + id: 0, + firstName: '', + lastName: '', + email: '', + username: '', + password: '', + spendingAccount: { + accNumber: undefined, + balance: 0 + }, + savingAccount: { + accNumber: undefined, + balance: 0 + } +}) + +const updatePassword = ref<boolean>(false) +const confirmPassword = ref<string>('') +const errorMessage = ref<string>('') +const isModalOpen = ref(false) +const image = ref<File>() + +const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{0,29}$/ +const emailRegex = + /^[æÆøØåÅa-zA-Z0-9_+&*-]+(?:\.[æÆøØåÅa-zA-Z0-9_+&*-]+)*@(?:[æÆøØåÅa-zA-Z0-9-]+\.)+[æÆøØåÅa-zA-Z]{2,7}$/ +const passwordRegex = /^(?=.*[0-9])(?=.*[a-zæøå])(?=.*[ÆØÅA-Z])(?=.*[@#$%^&+=!])(?=\S+$).{8,30}$/ +const accountNumberRegex = /^\d{11}$/ + +const MAX_DIGITS = 11 + +function restrictToNumbers(event: InputEvent, type: string) { + const inputValue = (event.target as HTMLInputElement)?.value + if (inputValue !== undefined) { + const sanitizedValue = inputValue.replace(/\D/g, '') + const truncatedValue = sanitizedValue.slice(0, MAX_DIGITS) + if (type === 'spending') { + profile.value.spendingAccount.accNumber = parseInt(truncatedValue) + } else { + profile.value.savingAccount.accNumber = parseInt(truncatedValue) + } + } +} + +const isFirstNameValid = computed( + () => nameRegex.test(profile.value.firstName) && profile.value.firstName +) +const isLastNameValid = computed( + () => nameRegex.test(profile.value.lastName) && profile.value.lastName +) +const isEmailValid = computed(() => emailRegex.test(profile.value.email)) +const isPasswordValid = computed(() => passwordRegex.test(profile.value.password || '')) +const isSpendingAccountValid = computed(() => + accountNumberRegex.test(profile.value.spendingAccount.accNumber?.toString() || '') +) +const isSavingAccountValid = computed(() => + accountNumberRegex.test(profile.value.savingAccount.accNumber?.toString() || '') +) + +const isFormInvalid = computed( + () => + [ + isFirstNameValid, + isLastNameValid, + isEmailValid, + isSpendingAccountValid, + isSavingAccountValid + ].some((v) => !v.value) || + (updatePassword.value + ? profile.value.password !== confirmPassword.value || profile.value.password === '' + : false) +) + +onMounted(async () => { + await authInterceptor('/profile') + .then((response) => { + profile.value = response.data + }) + .catch((error) => { + console.error(error) + }) +}) + +const selectImage = async () => { + const fileInput = document.getElementById('fileInput')! as HTMLInputElement + if (!fileInput) { + // Error handling + + console.log('Vi klarte ikke Ã¥ hente bildene dine. Prøv igjen!') + } + if (!fileInput.files) { + return + } + image.value = fileInput.files[0] +} +const uploadImage = async () => { + // bildet mÃ¥ lastes opp som en form. altsÃ¥ en body med form + // const formData = new FormData() + // authInterceptor.post("/profile/picture", formData) +} + +const saveChanges = async () => { + if (isFormInvalid.value) { + errorMessage.value = 'Vennligst fyll ut alle feltene riktig' + return + } + + if (!updatePassword.value) { + delete profile.value.password + } + + await authInterceptor + .put('/profile', profile.value) + .then(() => { + router.back() + }) + .catch((error) => { + errorMessage.value = error.response.data.message + }) +} +</script> + +<template> + <div class="w-full flex px-10 justify-center"> + <div class="flex flex-row flex-wrap justify-center w-full max-w-screen-xl gap-20"> + <div class="flex flex-col max-w-96 w-full gap-5"> + <h1>Rediger profil</h1> + <div class="w-full flex flex-row gap-5 justify-between justify-items-end"> + <div class="flex flex-col justify-center"> + <button class="h-min bg-transparent text-4xl" v-text="'⬅ï¸'" /> + </div> + <div class="w-32 h-32 border-stale-200 border-2 rounded-full shrink-0" /> + <div class="flex flex-col justify-center"> + <button class="h-min bg-transparent text-4xl" v-text="'âž¡ï¸'" /> + </div> + </div> + <div class="flex flex-row justify-center"> + <input + id="fileInput" + type="file" + style="display: none" + accept=".jpg, .jpeg, .png, .gif, .img" + /> + <button v-text="'Velg eget bilde!'" @click="selectImage()" /> + + <button v-text="'Send bilde'" @click="uploadImage()" /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Fornavn*</p> + <ToolTip + :message="'Must include only letters, spaces, commas, apostrophes, periods, and hyphens. 1-30 characters long'" + /> + </div> + <input + v-model="profile.firstName" + name="firstName" + placeholder="Skriv inn fornavn" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>Etternavn*</p> + <ToolTip + :message="'Must include only letters, spaces, commas, apostrophes, periods, and hyphens. 1-30 characters long'" + /> + </div> + <input + v-model="profile.lastName" + name="lastName" + placeholder="Skriv inn etternavn" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <p>E-post*</p> + <ToolTip + :message="'Valid email: Starts with Norwegian letters, numbers, or special characters. Includes \@\ followed by a domain. Ends with 2-7 letters.'" + /> + </div> + <input + v-model="profile.email" + name="email" + placeholder="Skriv inn e-post" + type="text" + /> + </div> + <div class="flex flex-col"> + <div class="flex flex-row justify-between mx-4"> + <div class="flex flex-row gap-2"> + <p>Endre passord</p> + <input v-model="updatePassword" type="checkbox" /> + </div> + <ToolTip + v-if="updatePassword" + :message="'Must be at least 8 characters, including at least one number, one lowercase letter, one uppercase letter, one special character (@#$%^&+=!), and no spaces.'" + /> + </div> + <input + v-if="updatePassword" + v-model="profile.password" + class="w-full" + name="password" + placeholder="Skriv inn passord" + /> + <input + v-if="updatePassword" + v-model="confirmPassword" + class="mt-2" + name="confirm" + placeholder="Bekreft passord" + type="password" + /> + </div> + + <p v-if="errorMessage" class="text-red-500" v-text="errorMessage" /> + </div> + <div class="flex flex-col justify-end max-w-96 w-full gap-5"> + <InteractiveSpare + :png-size="10" + :speech="['Her kan du endre pÃ¥ profilen din!']" + direction="left" + :isModalOpen="isModalOpen" + /> + + <p class="font-bold">Endre kontonummer:</p> + + <CardTemplate> + <div class="bg-red-100"> + <p class="font-bold mx-3" v-text="'Brukskonto'" /> + </div> + <input + @input="restrictToNumbers($event as InputEvent, 'spending')" + v-model="profile.spendingAccount.accNumber" + class="border-transparent rounded-none rounded-b-xl w-full" + placeholder="Kontonummer" + type="number" + /> + </CardTemplate> + + <CardTemplate> + <div class="bg-red-100"> + <p class="font-bold mx-3" v-text="'Sparekonto'" /> + </div> + <input + @input="restrictToNumbers($event as InputEvent, 'saving')" + v-model="profile.savingAccount.accNumber" + class="border-transparent rounded-none rounded-b-xl w-full" + placeholder="Kontonummer" + type="number" + /> + </CardTemplate> + + <div class="flex flex-row justify-between"> + <button class="primary danger" @click="router.back()" v-text="'Avbryt'" /> + <button class="primary" @click="saveChanges" v-text="'Lagre endringer'" /> + </div> + </div> + </div> + </div> +</template> diff --git a/src/views/NotFoundView.vue b/src/views/NotFoundView.vue new file mode 100644 index 0000000000000000000000000000000000000000..89b08e7fe479daa6d480bc832120cbb44b50851b --- /dev/null +++ b/src/views/NotFoundView.vue @@ -0,0 +1,8 @@ +<script lang="ts" setup></script> + +<template> + <div class="flex flex-col justify-center items-center mt-16"> + <h1>404 - Siden finnes ikke</h1> + <p>Denne siden finnes ikke. GÃ¥ tilbake til <RouterLink to="/">hjem</RouterLink>.</p> + </div> +</template> diff --git a/src/views/RegisterLoginView.vue b/src/views/RegisterLoginView.vue new file mode 100644 index 0000000000000000000000000000000000000000..16d78619fdf348fd038c8858499065d46d11c923 --- /dev/null +++ b/src/views/RegisterLoginView.vue @@ -0,0 +1,54 @@ +<script setup lang="ts"> +import FormLogin from '@/components/FormLogin.vue' +import FormRegister from '@/components/FormRegister.vue' +import { onMounted, ref } from 'vue' +import router from '@/router' + +const isLogin = ref<boolean>(true) + +onMounted(() => { + isLogin.value = router.currentRoute.value.path === '/logginn' +}) +</script> + +<template> + <div class="flex flex-col items-center gap-5 justify-center md:flex-row h-screen"> + <div class="flex items-center justify-center md:w-2/3"> + <img + src="@/assets/spare_og_sti.png" + alt="Spare og sti logo" + class="w-5/6 ml-10 md:mb-64" + /> + </div> + <div class="flex flex-col md:mr-10 md:mt-20 md:w-1/3 h-screen justify-start"> + <div class="flex flex-row gap-5 justify-center"> + <h3 + :class="{ selected: isLogin }" + class="cursor-pointer" + tabindex="0" + @click="isLogin = true" + @keydown.enter.prevent="isLogin = true" + > + Logg inn + </h3> + <h3 + :class="{ selected: !isLogin }" + class="cursor-pointer" + tabindex="0" + @click="isLogin = false" + @keydown.enter.prevent="isLogin = false" + > + Registrer deg + </h3> + </div> + <FormLogin v-if="isLogin" /> + <FormRegister v-else /> + </div> + </div> +</template> + +<style scoped> +.selected { + border-bottom: 2px solid black; +} +</style> diff --git a/src/views/ResetPasswordView.vue b/src/views/ResetPasswordView.vue new file mode 100644 index 0000000000000000000000000000000000000000..1558f00d20fd122a9a4dcca02795fff1b41ca803 --- /dev/null +++ b/src/views/ResetPasswordView.vue @@ -0,0 +1,113 @@ +<template> + <div> + <h1 class="flex flex-col items-center mt-8">Oppdater passord</h1> + <p class="flex flex-col items-center mb-16">Skriv inn ditt nye passord ðŸ”</p> + <div class="flex justify-center items-center w-full"> + <div class="flex flex-col md:w-1/4 w-2/3"> + <div class="flex flex-row justify-between mx-4"> + <p>Nytt passord:</p> + <ToolTip + :message="'Must be at least 8 characters, including at least one number, one lowercase letter, one uppercase letter, one special character (@#$%^&+=!), and no spaces.'" + /> + </div> + <div class="relative"> + <input + name="password" + v-model="newPassword" + :type="showPassword ? 'text' : 'password'" + placeholder="Skriv inn passord" + class="w-full" + /> + <button + class="absolute right-0 top-1 bg-transparent hover:bg-transparent" + @click="toggleShowPassword" + > + {{ showPassword ? '🔓' : '🔒' }} + </button> + </div> + <input + v-model="confirm" + :class="{ 'bg-green-200': newPassword == confirm && '' !== confirm.valueOf() }" + class="mt-2 w-full" + name="confirm" + placeholder="Bekreft passord" + type="password" + /> + <div class="flex justify-center mt-10"> + <button + :disabled="!canResetPassword" + @click="resetPassword" + class="p-2 w-2/3 md:w-5/6 disabled:opacity-50" + > + Oppdater passord + </button> + </div> + </div> + </div> + <ModalComponent + :title="'Passordet er oppdatert ✨'" + :message="'Passordet ditt er nÃ¥ oppdatert. Du kan nÃ¥ logge inn med ditt nye passord.'" + :is-modal-open="isModalOpen" + @close="isModalOpen = false" + > + <template v-slot:buttons> + <button + @click="goToLogin" + class="active-button font-bold py-2 px-4 w-1/2 hover:bg-[#f7da7c] border-2 border-[#f7da7c] disabled:border-transparent" + > + Logg inn + </button> + </template> + </ModalComponent> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import axios from 'axios' +import ToolTip from '@/components/ToolTip.vue' +import ModalComponent from '@/components/ModalComponent.vue' + +const route = useRoute() +const router = useRouter() + +const resetID = ref(route.query.resetID) +const userID = ref(route.query.userID) +const newPassword = ref<string>('') +const confirm = ref<string>('') +const showPassword = ref<boolean>(false) +const isModalOpen = ref<boolean>(false) + +const passwordRegex = /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!])(?=\S+$).{8,}$/ + +const isPasswordValid = computed(() => passwordRegex.test(newPassword.value)) + +const canResetPassword = computed(() => { + return isPasswordValid.value && newPassword.value === confirm.value && confirm.value !== '' +}) + +const resetPassword = async () => { + isModalOpen.value = true + + try { + await axios.post('http://localhost:8080/forgotPassword/resetPassword', { + resetID: resetID.value, + userID: userID.value, + newPassword: newPassword.value + }) + } catch (error) { + const err = error as Error + console.error(err.message) + } +} + +const toggleShowPassword = () => { + showPassword.value = !showPassword.value +} + +const goToLogin = () => { + isModalOpen.value = false + router.push('/logginn') +} +</script> diff --git a/src/views/StartView.vue b/src/views/StartView.vue new file mode 100644 index 0000000000000000000000000000000000000000..3f9842c83b91204842f1cf825d8352cdd2ad6f56 --- /dev/null +++ b/src/views/StartView.vue @@ -0,0 +1,83 @@ +<template> + <div class="background-container"> + <div class="overflow-hidden md:relative absolute max-md:-left-2/3 right-0 left-0"> + <img + src="@/assets/start_page/sti.png" + alt="Background" + class="min-w-[1420px] h-auto overflow-hidden" + /> + </div> + <div class="flex flex-col items-center pt-40 absolute top-0 left-0 right-0 z-10"> + <img src="@/assets/spare.png" alt="Spare" class="md:w-1/6 w-1/3 h-auto" /> + <img + src="@/assets/spareSti.png" + alt="Sparesti" + class="md:w-5/12 w-10/12 h-auto md:mt-4 mt-20" + /> + <p class="mt-2 lg:text-2xl text-lg font-bold">GJØR SPARING TIL EN LEK!</p> + </div> + + <div + class="flex flex-col items-end mr-3 justify-center space-y-4 z-20 absolute inset-0 -top-3/4 md:left-3/4 left-1/4 md:w-1/6 md:right-1/3 md:mt-44" + > + <button + class="md:py-3 md:px-0 md:w-3/4 py-2 px-12 w-1/2 border border-[#95E35D] shadow-lg rounded-lg transition-all duration-500 bg-[#95E35D] hover:bg-white text-sm md:text-base" + @click="goToRegister" + > + Start her + </button> + <button + class="md:py-3 md:px-0 md:w-3/4 py-2 px-12 w-1/2 border border-[#95E35D] shadow-lg rounded-lg transition-all duration-500 bg-white hover:bg-[#95E35D] text-sm md:text-base" + @click="goToLogin" + > + Logg inn + </button> + </div> + + <div class="invisible md:visible absolute -bottom-80 right-0 mr-4 pr-10 w-1/4"> + <h2>Kom igang med mÃ¥lene dine! 🚀</h2> + <p>Sett deg et sparemÃ¥l og utfordre deg selv til Ã¥ spare mer. 🎯</p> + </div> + + <div class="invisible md:visible absolute -bottom-[1450px] right-0 mr-4 pr-10 w-1/4"> + <h2>Gi slipp pÃ¥ dÃ¥rlige vaner! ðŸ»</h2> + <p>Sett deg et spareutfordringer, bra for lommeboka og helsa! 🦾</p> + </div> + + <div + class="absolute md:-bottom-[600px] -bottom-[500px] md:left-10 left-10 md:ml-2 md:pl-10 md:w-1/3 w-5/6" + > + <h2>Spar til din neste ferie! ðŸ–ï¸â˜€ï¸ï¸</h2> + <p>Ferie er viktig for Ã¥ lade batteriene 🔋. Spar til din neste ferie og nyt livet!</p> + <div + class="absolute top-50 left-10 right-10 h-24 overflow-hidden bg-[url('@/assets/start_page/skyer.png')] bg-repeat-x bg-center animate-clouds" + ></div> + <div + class="absolute top-80 left-10 right-10 h-24 overflow-hidden bg-[url('@/assets/start_page/strand.png')] bg-repeat-x bg-center animate-beach" + ></div> + <div + class="absolute -bottom-52 left-10 right-10 h-24 bg-[url('@/assets/start_page/fly.png')] bg-contain bg-no-repeat bg-center z-10" + ></div> + <div + class="flame1 absolute -bottom-36 md:left-60 left-44 h-2 w-8 rounded-lg bg-blue-100 animate-flame z-9" + ></div> + <div + class="flame2 absolute -bottom-48 md:left-[258px] left-48 h-2 w-10 rounded-lg bg-blue-100 animate-flame z-9" + ></div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { useRouter } from 'vue-router' + +const router = useRouter() + +const goToLogin = () => { + router.push('/logginn') +} + +const goToRegister = () => { + router.push('/registrer') +} +</script> diff --git a/src/views/UserChallengesView.vue b/src/views/UserChallengesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..2e05bb08119724ddaa01620cda852b335942b490 --- /dev/null +++ b/src/views/UserChallengesView.vue @@ -0,0 +1,90 @@ +<script lang="ts" setup> +import { useRouter } from 'vue-router' +import { onMounted, ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' +import type { Challenge } from '@/types/challenge' +import CardChallenge from '@/components/CardChallenge.vue' +import PageControl from '@/components/PageControl.vue' + +const router = useRouter() + +const currentPageActive = ref(0) +const totalPagesActive = ref(1) +const currentPageCompleted = ref(0) +const totalPagesCompleted = ref(1) + +const activeChallenges = ref<Challenge[]>([]) +const completedChallenges = ref<Challenge[]>([]) + +const getActiveChallenges = async (newPage: number) => { + await authInterceptor(`/challenges/active?page=${newPage}&size=5`) + .then((response) => { + currentPageActive.value = response.data.number + totalPagesActive.value = response.data.totalPages + activeChallenges.value = response.data.content + }) + .catch((error) => { + console.error(error) + }) +} + +const getCompletedChallenges = async (newPage: number) => { + await authInterceptor(`/challenges/completed?page=${newPage}&size=5`) + .then((response) => { + currentPageCompleted.value = response.data.number + totalPagesCompleted.value = response.data.totalPages + completedChallenges.value = response.data.content + }) + .catch((error) => { + console.error(error) + }) +} + +onMounted(async () => { + await getActiveChallenges(currentPageActive.value) + await getCompletedChallenges(currentPageActive.value) +}) +</script> + +<template> + <h1 class="font-bold text-center">Dine utfordringer</h1> + <div class="flex flex-col gap-5 items-center"> + <div class="flex flex-row gap-5"> + <button class="primary" @click="router.push({ name: 'new-challenge' })"> + Opprett en ny utfordring + </button> + </div> + + <h2 class="font-bold">Aktive utfordringer🚀</h2> + <div class="flex flex-row justify-center gap-10 flex-wrap"> + <CardChallenge + v-for="challenge in activeChallenges" + :key="challenge.id" + :challenge-instance="challenge" + /> + <p v-if="!activeChallenges">Du har ingen aktive spareutfordringer😢</p> + </div> + <PageControl + :currentPage="currentPageActive" + :on-page-change="getActiveChallenges" + :totalPages="totalPagesActive" + /> + + <h2 class="font-bold">Fullførte utfordringer💯</h2> + <div class="flex flex-row justify-center gap-10 flex-wrap"> + <CardChallenge + class="border-2 border-slate-200 hover:bg-slate-50" + v-for="challenge in completedChallenges" + :key="challenge.id" + :challenge-instance="challenge" + /> + </div> + <PageControl + :currentPage="currentPageCompleted" + :on-page-change="getCompletedChallenges" + :totalPages="totalPagesCompleted" + /> + </div> +</template> + +<style scoped></style> diff --git a/src/views/UserGoalsView.vue b/src/views/UserGoalsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..734f469364f66c7002b66895c3b7ef0de6b97cc2 --- /dev/null +++ b/src/views/UserGoalsView.vue @@ -0,0 +1,111 @@ +<script lang="ts" setup> +import CardGoal from '@/components/CardGoal.vue' + +import { useRouter } from 'vue-router' +import { onMounted, ref } from 'vue' +import authInterceptor from '@/services/authInterceptor' +import type { Goal } from '@/types/goal' +import draggable from 'vuedraggable' +import PageControl from '@/components/PageControl.vue' + +const router = useRouter() + +const currentPage = ref(0) +const totalPages = ref(1) + +const activeGoals = ref<Goal[]>([]) +const completedGoals = ref<Goal[]>([]) + +const isDraggable = ref(false) + +onMounted(async () => { + await authInterceptor('/goals/active') + .then((response) => { + activeGoals.value = response.data + activeGoals.value.sort((a, b) => (a.priority || 0) - (b.priority || 0)) + }) + .catch((error) => { + console.error(error) + }) + + await updatePage(0) +}) + +const updatePage = async (page: number) => { + await authInterceptor(`/goals/completed?page=${page}&size=5`) + .then((response) => { + currentPage.value = response.data.number + totalPages.value = response.data.totalPages + completedGoals.value = response.data.content + }) + .catch((error) => { + console.error(error) + }) +} + +const changeOrder = async () => { + if (isDraggable.value) { + const priorities = activeGoals.value.map((goal) => goal.id) + await authInterceptor.put('/goals', priorities).catch((error) => { + console.error(error) + }) + isDraggable.value = false + await updatePage(currentPage.value) + } else { + isDraggable.value = true + } +} +</script> + +<template> + <div class="flex flex-col gap-5 items-center"> + <h1 class="font-bold m-0">Dine sparemÃ¥l</h1> + <button class="primary" @click="router.push({ name: 'new-goal' })"> + Opprett et nytt sparemÃ¥l + </button> + <h2 class="font-bold m-0">Aktive sparemÃ¥l🚀</h2> + <p v-if="activeGoals.length === 0">Du har ingen aktive sparemÃ¥l</p> + <draggable + v-else + v-model="activeGoals" + class="flex flex-row flex-wrap justify-center gap-10" + item-key="id" + :disabled="!isDraggable" + > + <template #item="{ element, index }"> + <CardGoal + :key="index" + :class="[ + { 'cursor-move shadow-xl -translate-y-2 duration-300': isDraggable }, + { 'border-2 border-lime-400': index === 0 }, + { 'border-2 border-slate-200 hover:bg-slate-50': index !== 0 } + ]" + :goal-instance="element" + :is-clickable="!isDraggable" + /> + </template> + </draggable> + <button + class="primary secondary" + :disabled="activeGoals.length === 0" + @click="changeOrder()" + > + {{ isDraggable ? 'Lagre rekkefølge' : 'Endre rekkefølge' }} + </button> + <h2 class="font-bold m-0">Fullførte sparemÃ¥l💯</h2> + <p v-if="completedGoals.length === 0">Du har ingen fullførte sparemÃ¥l😢</p> + <div v-else class="flex flex-row flex-wrap justify-center gap-10"> + <CardGoal + class="border-2 border-slate-200 hover:bg-slate-50" + v-for="goal in completedGoals" + :key="goal.id" + :goal-instance="goal" + /> + </div> + <PageControl + :current-page="currentPage" + :on-page-change="updatePage" + :total-pages="totalPages" + /> + </div> +</template> diff --git a/src/views/ViewChallengeView.vue b/src/views/ViewChallengeView.vue new file mode 100644 index 0000000000000000000000000000000000000000..2547802e104ced7cf68dcc7b586c19e165affb2c --- /dev/null +++ b/src/views/ViewChallengeView.vue @@ -0,0 +1,190 @@ +<script lang="ts" setup> +import { useRouter } from 'vue-router' +import { computed, onMounted, ref } from 'vue' +import ProgressBar from '@/components/ProgressBar.vue' +import authInterceptor from '@/services/authInterceptor' +import type { Challenge } from '@/types/challenge' +import SpareComponent from '@/components/SpareComponent.vue' +import starImage from '@/assets/star.png' + +const router = useRouter() +const challengeImageUrl = ref(starImage) +const isImageLoaded = ref(false) + +const challengeInstance = ref<Challenge>({ + title: 'Tittel', + perPurchase: 20, + saved: 0, + target: 100, + description: 'Dette er en lang textbeskrivelse', + due: '2024-01-01T00:00:00.000Z', + type: '☕ï¸', + completedOn: '' +}) + +const timesSaved = computed( + () => challengeInstance.value.saved / challengeInstance.value.perPurchase +) +const completion = computed( + () => (challengeInstance.value.saved / challengeInstance.value.target) * 100 +) +const isCompleted = computed(() => challengeInstance.value.completedOn != null) + +const motivation = ref<string[]>([]) + +const calculateSpeech = () => { + if (completion.value === 0) { + return motivation.value.push( + `Du har ikke spart noe enda. Du har ${challengeInstance.value.target}kr igjen til mÃ¥let.` + ) + } else if (completion.value < 25) { + return motivation.value.push( + `Du har spart ${challengeInstance.value.saved}kr av ${challengeInstance.value.target}kr, som er ${timesSaved.value} ganger.` + ) + } else if (completion.value < 75) { + return motivation.value.push( + `Du er pÃ¥ god vei! Du har spart ${challengeInstance.value.saved}kr av ${challengeInstance.value.target}kr, som er ${timesSaved.value} ganger.` + ) + } else if (completion.value < 100) { + return motivation.value.push( + `Nesten der! Du har spart ${challengeInstance.value.saved}kr av ${challengeInstance.value.target}kr, som er ${timesSaved.value} ganger.` + ) + } else if (completion.value >= 100) { + return motivation.value.push( + `Fantastisk! Du har nÃ¥dd mÃ¥let ditt! Du har spart ${challengeInstance.value.saved}kr av ${challengeInstance.value.target}kr, som er ${timesSaved.value} ganger.` + ) + } +} + +onMounted(async () => { + const challengeId = router.currentRoute.value.params.id + if (!challengeId) return router.push({ name: 'challenges' }) + + try { + const challengeResponse = await authInterceptor.get(`/challenges/${challengeId}`) + challengeInstance.value = challengeResponse.data + calculateSpeech() + + try { + const imageResponse = await authInterceptor.get( + `/challenges/picture?id=${challengeId}`, + { responseType: 'blob' } + ) + challengeImageUrl.value = URL.createObjectURL(imageResponse.data) + } catch (imageError) { + console.error('Failed to load image:', imageError) + } + isImageLoaded.value = true + } catch (error) { + console.error('Failed to load challenge details:', error) + await router.push({ name: 'challenges' }) + } +}) + +const completeChallenge = () => { + authInterceptor + .put(`/challenges/${challengeInstance.value.id}/complete`) + .then(() => { + router.push({ name: 'challenges' }) + }) + .catch((error) => { + console.error(error) + }) +} +</script> + +<template> + <div class="flex flex-row flex-wrap items-center justify-center gap-10"> + <div class="flex flex-col gap-5 max-w-96"> + <div class="flex flex-col items-center"></div> + + <button + class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-100 justify-start" + @click="router.push({ name: 'challenges', params: { id: challengeInstance.id } })" + > + 👈Oversikt + </button> + + <div + class="flex flex-col justify-center border-2 rounded-3xl align-middle p-5 card-shadow overflow-hidden w-full" + > + <h2 class="my-0">Spareutfordring:</h2> + <h2 class="font-light"> + {{ challengeInstance.title }} + </h2> + <div class="flex flex-col gap-4 justify-center"> + <p class="text-wrap break-words">{{ challengeInstance.description }}</p> + <div class="flex justify-center items-center"> + <img + v-if="isImageLoaded" + :src="challengeImageUrl || '@/assets/star.png'" + alt="Goal Image" + class="w-44 h-44 object-cover rounded-lg" + /> + </div> + </div> + <br /> + <p class="text-center"> + Du har spart {{ timesSaved }} ganger som er {{ challengeInstance.saved }}kr av + {{ challengeInstance.target }}kr + </p> + <ProgressBar :completion="completion" /> + <br /> + + <br /> + <p> + Du sparer {{ challengeInstance.perPurchase }}kr hver gang du dropper Ã¥ bruke + penger pÃ¥ {{ challengeInstance.title }} + </p> + <div class="justify-center pl-20"> + <button + class="primary danger mt-2 rounded-2xl p-2 w-40" + @click=" + authInterceptor + .delete(`/challenges/${challengeInstance.id}`) + .then(() => router.push({ name: 'challenges' })) + .catch((error) => console.error(error)) + " + > + Slett + </button> + </div> + </div> + + <div class="flex flex-row justify-between w-full"> + <button + class="primary secondary" + v-if="!isCompleted" + @click=" + router.push({ + name: 'edit-challenge', + params: { id: challengeInstance.id } + }) + " + > + Rediger + </button> + + <button + class="primary" + v-if="!isCompleted" + @click="completeChallenge" + v-text="'Sett utfordring til ferdig'" + /> + </div> + </div> + <SpareComponent + :speech="motivation" + :png-size="15" + :imageDirection="'left'" + :direction="'right'" + class="mb-5" + ></SpareComponent> + </div> +</template> + +<style scoped> +.card-shadow { + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); +} +</style> diff --git a/src/views/ViewGoalView.vue b/src/views/ViewGoalView.vue new file mode 100644 index 0000000000000000000000000000000000000000..446069857f8830a9ad32cea2da97e22e314e495e --- /dev/null +++ b/src/views/ViewGoalView.vue @@ -0,0 +1,165 @@ +<script lang="ts" setup> +import { useRouter } from 'vue-router' +import { computed, onMounted, ref } from 'vue' +import ProgressBar from '@/components/ProgressBar.vue' +import authInterceptor from '@/services/authInterceptor' +import type { Goal } from '@/types/goal' +import SpareComponent from '@/components/SpareComponent.vue' +import starImage from '@/assets/star.png' + +const router = useRouter() +const goalImageUrl = ref(starImage) +const isImageLoaded = ref(false) + +const goalInstance = ref<Goal>({ + title: 'Test tittel', + saved: 0, + target: 100, + description: 'Dette er en lang textbeskrivelse', + due: '' +}) + +const completion = computed(() => (goalInstance.value.saved / goalInstance.value.target) * 100) +const isCompleted = computed(() => goalInstance.value.completedOn != null) + +const motivation = ref<string[]>([]) + +const calculateSpeech = () => { + if (completion.value === 0) { + return motivation.value.push( + `Du har ikke spart noe enda. Du har ${goalInstance.value.target}kr igjen til mÃ¥let.` + ) + } else if (completion.value < 25) { + return motivation.value.push( + `Du har spart ${goalInstance.value.saved}kr av ${goalInstance.value.target}kr.` + ) + } else if (completion.value < 75) { + return motivation.value.push( + `Du er pÃ¥ god vei! Du har spart ${goalInstance.value.saved}kr av ${goalInstance.value.target}kr.` + ) + } else if (completion.value < 100) { + return motivation.value.push( + `Nesten der! Du har spart ${goalInstance.value.saved}kr av ${goalInstance.value.target}kr.` + ) + } else if (completion.value >= 100) { + return motivation.value.push( + `!Fantastisk Du har nÃ¥dd mÃ¥let ditt! Du har spart ${goalInstance.value.saved}kr av ${goalInstance.value.target}kr.` + ) + } +} + +onMounted(async () => { + const goalId = router.currentRoute.value.params.id + if (!goalId) return router.push({ name: 'goals' }) + + try { + const goalResponse = await authInterceptor.get(`/goals/${goalId}`) + goalInstance.value = goalResponse.data + calculateSpeech() + + try { + const imageResponse = await authInterceptor.get(`/goals/picture?id=${goalId}`, { + responseType: 'blob' + }) + goalImageUrl.value = URL.createObjectURL(imageResponse.data) + } catch (imageError) { + console.error('Failed to load image:', imageError) + } + isImageLoaded.value = true + } catch (error) { + console.error('Failed to load goal details:', error) + await router.push({ name: 'goals' }) + } +}) + +const completeGoal = () => { + authInterceptor + .put(`/goals/${goalInstance.value.id}/complete`) + .then(() => { + router.push({ name: 'goals' }) + }) + .catch((error) => { + console.error(error) + }) +} +</script> + +<template> + <div class="flex flex-row flex-wrap items-center justify-center gap-10"> + <div class="flex flex-col gap-5 max-w-96"> + <button + class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-200 ease-in-out hover:scale-110 hover:opacity-100 justify-start" + @click="router.push({ name: 'goals', params: { id: goalInstance.id } })" + > + 👈Oversikt + </button> + + <div + class="flex flex-col justify-center border-2 rounded-3xl align-middle p-5 card-shadow overflow-hidden w-full" + > + <h2 class="my-0">SparemÃ¥l:</h2> + <h2 class="font-light"> + {{ goalInstance.title }} + </h2> + <div class="flex flex-col gap-4 justify-center"> + <p class="text-wrap break-words">{{ goalInstance.description }}</p> + <div class="flex justify-center items-center"> + <img + v-if="isImageLoaded" + :src="goalImageUrl || '@/assets/star.png'" + alt="Goal Image" + class="w-44 h-44 object-cover rounded-lg" + /> + </div> + </div> + <br /> + <p class="text-center"> + Du har spart {{ goalInstance.saved }}kr av {{ goalInstance.target }}kr + </p> + <ProgressBar :completion="completion" /> + <button + class="primary secondary mt-6" + v-if="!isCompleted" + @click=" + router.push({ + name: 'edit-goal', + params: { id: goalInstance.id } + }) + " + > + Rediger + </button> + <button + class="danger mt-2 rounded-2xl p-1" + @click=" + authInterceptor + .delete(`/goals/${goalInstance.id}`) + .then(() => router.push({ name: 'goals' })) + .catch((error) => console.error(error)) + " + > + Slett + </button> + <button + class="primary mt-4" + v-if="!isCompleted" + @click="completeGoal" + v-text="'Marker mÃ¥let som ferdig'" + /> + </div> + </div> + <SpareComponent + :speech="motivation" + :png-size="15" + :imageDirection="'left'" + :direction="'right'" + class="mb-5" + ></SpareComponent> + </div> +</template> + +<style scoped> +.card-shadow { + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); +} +</style> diff --git a/src/views/ViewProfileView.vue b/src/views/ViewProfileView.vue new file mode 100644 index 0000000000000000000000000000000000000000..d166da3316e4e1bdbe270c177ccfe95a12a783df --- /dev/null +++ b/src/views/ViewProfileView.vue @@ -0,0 +1,178 @@ +<script lang="ts" setup> +import authInterceptor from '@/services/authInterceptor' +import { onMounted, ref } from 'vue' +import type { Profile } from '@/types/profile' +import CardTemplate from '@/components/CardTemplate.vue' +import type { Challenge } from '@/types/challenge' +import type { Goal } from '@/types/goal' +import CardGoal from '@/components/CardGoal.vue' +import router from '@/router' +import SpareComponent from '@/components/SpareComponent.vue' +import { useUserStore } from '@/stores/userStore' +import ModalEditAvatar from '@/components/ModalEditAvatar.vue' + +const profile = ref<Profile>() +const completedGoals = ref<Goal[]>([]) +const completedChallenges = ref<Challenge[]>([]) +const speech = ref<string[]>([]) +const profilePicture = ref<string>() + +const userStore = useUserStore() + +const updateUser = async () => { + authInterceptor('/profile') + .then((response) => { + profile.value = response.data + }) + .catch((error) => { + return console.log(error) + }) +} + +onMounted(async () => { + await updateUser() + + await authInterceptor(`/goals/completed?page=0&size=3`) + .then((response) => { + completedGoals.value = response.data.content + }) + .catch((error) => { + return console.log(error) + }) + + await authInterceptor('/challenges/completed?page=0&size=3') + .then((response) => { + completedChallenges.value = response.data.content + }) + .catch((error) => { + return console.log(error) + }) + + await userStore.getProfilePicture() + profilePicture.value = userStore.profilePicture + openSpare() +}) + +const updateBiometrics = async () => { + await useUserStore().bioRegister() + await updateUser() +} + +const updateProfilePicture = async () => { + await updateUser() + await userStore.getProfilePicture() + profilePicture.value = userStore.profilePicture +} + +const openSpare = () => { + speech.value = [ + `Velkommen, ${profile.value?.firstName} ${profile.value?.lastName}! 🤠`, + 'Her kan du finne en oversikt over dine profilinstillinger!', + 'Du kan ogsÃ¥ se dine fullførte sparemÃ¥l og utfordringer!' + ] +} +</script> + +<template> + <div class="w-full flex px-10 justify-center"> + <div class="flex flex-row flex-wrap justify-center w-full max-w-screen-xl gap-20"> + <div class="flex flex-col max-w-96 w-full gap-5"> + <h1>Profil</h1> + <div class="flex flex-row gap-5"> + <div class="flex flex-col gap-1"> + <img + :src="profilePicture" + alt="could not load" + class="block mx-auto h-32 rounded-full border-green-600 border-2 sm:mx-0 sm:shrink-0" + /> + <ModalEditAvatar @update-profile-picture="updateProfilePicture" /> + </div> + <div class="w-full flex flex-col justify-between"> + <h3 class="font-thin my-0 md:text-xl text-lg">{{ profile?.username }}</h3> + <h3 class="font-thin my-0 md:text-xl text-lg"> + {{ profile?.firstName + ' ' + profile?.lastName }} + </h3> + <h3 class="font-thin my-0 md:text-xl text-lg">{{ profile?.email }}</h3> + </div> + </div> + + <h3 + class="font-bold" + v-text="'Du har spart ' + profile?.savedAmount + ' kr totalt'" + /> + + <CardTemplate> + <div class="bg-red-100"> + <p class="font-bold mx-3" v-text="'Brukskonto'" /> + </div> + <p + class="mx-3" + v-text="profile?.spendingAccount.accNumber || 'Ingen brukskonto oppkoblet'" + /> + </CardTemplate> + + <CardTemplate> + <div class="bg-red-100"> + <p class="font-bold mx-3" v-text="'Sparekonto'" /> + </div> + <p + class="mx-3" + v-text="profile?.savingAccount.accNumber || 'Ingen sparekonto oppkoblet'" + /> + </CardTemplate> + <div class="flex flex-col justify-center items-center space-y-2"> + <button + class="primary secondary w-2/3" + @click="router.push({ name: 'edit-profile' })" + v-text="'Rediger bruker'" + /> + <button + class="primary secondary w-2/3" + @click="router.push({ name: 'edit-configuration' })" + v-text="'Rediger konfigurasjon'" + /> + <button class="primary w-2/3" @click="updateBiometrics"> + {{ profile?.hasPasskey ? 'Endre biometri' : 'Legg til biometri' }} + </button> + </div> + </div> + + <div class="flex flex-col"> + <SpareComponent + :speech="speech" + :png-size="15" + :imageDirection="'left'" + :direction="'right'" + class="mb-5" + ></SpareComponent> + <div class="flex flex-row justify-between mx-4"> + <p class="font-bold">Fullførte sparemÃ¥l</p> + <a + @click="router.push({ name: 'goals' })" + class="hover:p-0 cursor-pointer text-blue-500" + v-text="'Se alle'" + /> + </div> + <CardTemplate class="p-4 flex flex-row flex-wrap justify-center gap-2 mb-4 mt-2"> + <CardGoal v-for="goal in completedGoals" :key="goal.id" :goal-instance="goal" /> + </CardTemplate> + + <div class="flex flex-row justify-between mx-4"> + <p class="font-bold">Fullførte utfordringer</p> + <a + @click="router.push({ name: 'challenges' })" + class="hover:p-0 cursor-pointer text-blue-500" + v-text="'Se alle'" + /> + </div> + <CardTemplate class="p-4 flex flex-row flex-wrap justify-center gap-2 mb-4 mt-2"> + <CardGoal + v-for="challenge in completedChallenges" + :key="challenge.id" + :goal-instance="challenge" + /> + </CardTemplate> + </div> + </div> + </div> +</template> diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..602557ab182f079569a219c33e01d5ccca4f462a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,41 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + theme: { + extend: { + animation: { + clouds: 'clouds 20s linear infinite', + beach: 'beach 5s linear infinite', + flame: 'flame 0.3s linear infinite' + }, + keyframes: { + clouds: { + '0%': { backgroundPosition: '0%' }, + '100%': { backgroundPosition: '-100%' } + }, + beach: { + '0%': { backgroundPosition: '0%' }, + '100%': { backgroundPosition: '-100%' } + }, + + flame: { + '0%, 100%': { transform: 'translateX(0%)' }, + '50%': { transform: 'translateX(50%)' } + } + }, + colors: { + 'button-disabled': 'var(--grey)', + 'button-danger': 'var(--red)', + 'button-other': 'var(--accent1)' + }, + backgroundSize: { + 'auto': 'auto', + 'cover': 'cover', + 'contain': 'contain', + 'pc': '20%', + 'phone': '50%', + }, + } + }, + plugins: [] +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000000000000000000000000000000000000..e14c754d3ae5775d2ab13001e251c1371be912de --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..5304731b8d26326f23e647245010b28216f91dac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ], + "compilerOptions": { + "module": "NodeNext" + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000000000000000000000000000000000000..f0940630302f8c4b03a6f601a908d6fa0240ad54 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json new file mode 100644 index 0000000000000000000000000000000000000000..571995d11e6acb21020f2570fb6a034609ee654e --- /dev/null +++ b/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "exclude": [], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..d48b67e5cc93e47429649f84e1e6671fa2680b93 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + host: '0.0.0.0' + } +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b1c897997739635a6e14248a6448b67b2703c44 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', + exclude: [...configDefaults.exclude, 'e2e/**'], + root: fileURLToPath(new URL('./', import.meta.url)) + } + }) +)