diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b564a6..efbe557 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,14 +64,14 @@ jobs: cache: 'maven' - name: Cache SonarQube Cloud packages - uses: actions/cache@v4.2.1 + uses: actions/cache@v4.2.2 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v4.2.1 + uses: actions/cache@v4.2.2 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dff0db3..dfba7fc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -123,14 +123,14 @@ jobs: cache: 'maven' - name: Cache SonarQube Cloud packages - uses: actions/cache@v4.2.1 + uses: actions/cache@v4.2.2 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v4.2.1 + uses: actions/cache@v4.2.2 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} diff --git a/client/package-lock.json b/client/package-lock.json index d1c338e..1d20387 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,7 @@ "license": "GPL-3.0-only", "dependencies": { "@popperjs/core": "^2.11.8", - "@types/node": "^22.13.8", + "@types/node": "^22.13.9", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "bootstrap": "^5.3.3", @@ -21,15 +21,16 @@ "react-bootstrap-icons": "^1.11.5", "react-charts": "^3.0.0-beta.57", "react-dom": "^19.0.0", - "react-i18next": "^15.4.0", + "react-i18next": "^15.4.1", + "react-markdown": "^10.1.0", "react-router": "^7.2.0", "react-router-bootstrap": "^0.26.3", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "vite": "^6.2.0" }, "devDependencies": { "@eslint/compat": "^1.2.7", - "@eslint/eslintrc": "^3.1.0", + "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", "@stylistic/eslint-plugin": "^3.0.1", "@testing-library/jest-dom": "^6.6.3", @@ -49,9 +50,9 @@ "globals": "^15.14.0", "jsdom": "^26.0.0", "prettier": "^3.5.2", - "sass": "^1.83.4", + "sass": "^1.85.1", "source-map-support": "^0.5.21", - "typescript-eslint": "^8.25.0", + "typescript-eslint": "^8.26.0", "vitest": "^3.0.7" } }, @@ -982,10 +983,11 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2158,11 +2160,38 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2248,10 +2277,25 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.13.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", - "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2323,6 +2367,12 @@ "license": "MIT", "optional": true }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -2347,17 +2397,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", - "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", + "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/type-utils": "8.25.0", - "@typescript-eslint/utils": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/type-utils": "8.26.0", + "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2373,20 +2423,20 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", - "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", + "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4" }, "engines": { @@ -2398,18 +2448,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", - "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", + "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0" + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2420,14 +2470,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", - "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", + "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/utils": "8.26.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2440,13 +2490,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", - "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", + "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", "dev": true, "license": "MIT", "engines": { @@ -2458,14 +2508,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", - "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", + "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2481,7 +2531,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { @@ -2498,16 +2548,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", - "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", + "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0" + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2518,17 +2568,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", - "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", + "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/types": "8.26.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2552,6 +2602,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -2998,6 +3054,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3165,6 +3231,16 @@ } ] }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -3199,6 +3275,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -3278,6 +3394,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -3531,6 +3657,19 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3617,6 +3756,19 @@ "node": ">=0.10" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -4514,6 +4666,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4560,6 +4722,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5006,6 +5174,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -5034,6 +5242,16 @@ "void-elements": "3.1.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5154,6 +5372,12 @@ "node": ">=8" } }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5182,6 +5406,30 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5305,6 +5553,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5366,6 +5624,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5403,6 +5671,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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", @@ -6004,6 +6284,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6100,6 +6390,159 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6110,6 +6553,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6434,6 +7319,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-imports": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", @@ -6665,6 +7575,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6807,9 +7727,9 @@ } }, "node_modules/react-i18next": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", - "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -6840,6 +7760,33 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -7013,6 +7960,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -7188,9 +8168,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", "devOptional": true, "license": "MIT", "dependencies": { @@ -7427,6 +8407,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -7627,6 +8617,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -7704,6 +8708,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7877,6 +8899,26 @@ "node": ">=18" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", @@ -8018,9 +9060,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8031,15 +9073,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.25.0.tgz", - "integrity": "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.0.tgz", + "integrity": "sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.25.0", - "@typescript-eslint/parser": "8.25.0", - "@typescript-eslint/utils": "8.25.0" + "@typescript-eslint/eslint-plugin": "8.26.0", + "@typescript-eslint/parser": "8.26.0", + "@typescript-eslint/utils": "8.26.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8050,7 +9092,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/unbox-primitive": { @@ -8090,6 +9132,93 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -8128,6 +9257,34 @@ "punycode": "^2.1.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", @@ -8627,6 +9784,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } }, "dependencies": { @@ -9111,9 +10278,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -9804,11 +10971,35 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" }, + "@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "requires": { + "@types/ms": "*" + } + }, "@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, + "@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "requires": { + "@types/estree": "*" + } + }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -9880,10 +11071,23 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "requires": { + "@types/unist": "*" + } + }, + "@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "@types/node": { - "version": "22.13.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", - "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "requires": { "undici-types": "~6.20.0" } @@ -9946,6 +11150,11 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "optional": true }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, "@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -9967,16 +11176,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", - "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", + "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/type-utils": "8.25.0", - "@typescript-eslint/utils": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/type-utils": "8.26.0", + "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -9984,54 +11193,54 @@ } }, "@typescript-eslint/parser": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", - "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", + "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", - "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", + "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", "dev": true, "requires": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0" + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0" } }, "@typescript-eslint/type-utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", - "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", + "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/utils": "8.26.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" } }, "@typescript-eslint/types": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", - "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", + "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", - "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", + "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -10049,24 +11258,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", - "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", + "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0" + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", - "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", + "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", "dev": true, "requires": { - "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/types": "8.26.0", "eslint-visitor-keys": "^4.2.0" }, "dependencies": { @@ -10078,6 +11287,11 @@ } } }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, "@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -10372,6 +11586,11 @@ "possible-typed-array-names": "^1.0.0" } }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10468,6 +11687,11 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==" }, + "ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" + }, "chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -10491,6 +11715,26 @@ "supports-color": "^7.1.0" } }, + "character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" + }, + "character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" + }, + "character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" + }, + "character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==" + }, "check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -10541,6 +11785,11 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" + }, "comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -10737,6 +11986,14 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "requires": { + "character-entities": "^2.0.0" + } + }, "deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -10793,6 +12050,14 @@ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "optional": true }, + "devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "requires": { + "dequal": "^2.0.0" + } + }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -11456,6 +12721,11 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" + }, "estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -11490,6 +12760,11 @@ "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", "dev": true }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11801,6 +13076,36 @@ "function-bind": "^1.1.2" } }, + "hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "requires": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + } + }, + "hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "requires": { + "@types/hast": "^3.0.0" + } + }, "html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -11824,6 +13129,11 @@ "void-elements": "3.1.0" } }, + "html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==" + }, "http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -11895,6 +13205,11 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, "internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -11919,6 +13234,20 @@ "loose-envify": "^1.0.0" } }, + "is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==" + }, + "is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "requires": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + } + }, "is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -11994,6 +13323,11 @@ "has-tostringtag": "^1.0.2" } }, + "is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -12033,6 +13367,11 @@ "is-extglob": "^2.1.1" } }, + "is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==" + }, "is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -12055,6 +13394,11 @@ "has-tostringtag": "^1.0.2" } }, + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, "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", @@ -12482,6 +13826,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12554,12 +13903,336 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true }, + "mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + } + }, + "mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "requires": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "requires": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + } + }, + "mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "requires": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "requires": { + "@types/mdast": "^4.0.0" + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "requires": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "requires": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "requires": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" + }, + "micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==" + }, + "micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "requires": { + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" + }, + "micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" + }, "micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12780,6 +14453,27 @@ "callsites": "^3.0.0" } }, + "parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "requires": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "dependencies": { + "@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + } + } + }, "parse-imports": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", @@ -12940,6 +14634,11 @@ } } }, + "property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==" + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13033,9 +14732,9 @@ } }, "react-i18next": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", - "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", "requires": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" @@ -13053,6 +14752,24 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + }, "react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -13160,6 +14877,29 @@ "set-function-name": "^2.0.2" } }, + "remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "requires": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + } + }, + "remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + } + }, "resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -13273,9 +15013,9 @@ "dev": true }, "sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", "devOptional": true, "requires": { "@parcel/watcher": "^2.4.1", @@ -13442,6 +15182,11 @@ "source-map": "^0.6.0" } }, + "space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" + }, "spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -13593,6 +15338,15 @@ "es-object-atoms": "^1.0.0" } }, + "stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "requires": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + } + }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -13640,6 +15394,22 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "requires": { + "style-to-object": "1.0.8" + } + }, + "style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "requires": { + "inline-style-parser": "0.2.4" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13760,6 +15530,16 @@ "punycode": "^2.3.1" } }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" + }, + "trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==" + }, "ts-api-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", @@ -13868,19 +15648,19 @@ } }, "typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==" }, "typescript-eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.25.0.tgz", - "integrity": "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.0.tgz", + "integrity": "sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==", "dev": true, "requires": { - "@typescript-eslint/eslint-plugin": "8.25.0", - "@typescript-eslint/parser": "8.25.0", - "@typescript-eslint/utils": "8.25.0" + "@typescript-eslint/eslint-plugin": "8.26.0", + "@typescript-eslint/parser": "8.26.0", + "@typescript-eslint/utils": "8.26.0" } }, "unbox-primitive": { @@ -13911,6 +15691,63 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, + "unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "requires": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + } + }, + "unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + }, "update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -13929,6 +15766,24 @@ "punycode": "^2.1.0" } }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, "vite": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", @@ -14206,6 +16061,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" } } } diff --git a/client/package.json b/client/package.json index 8e5a609..a84494f 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "private": true, "dependencies": { "@popperjs/core": "^2.11.8", - "@types/node": "^22.13.8", + "@types/node": "^22.13.9", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "bootstrap": "^5.3.3", @@ -24,10 +24,11 @@ "react-bootstrap-icons": "^1.11.5", "react-charts": "^3.0.0-beta.57", "react-dom": "^19.0.0", - "react-i18next": "^15.4.0", + "react-i18next": "^15.4.1", + "react-markdown": "^10.1.0", "react-router": "^7.2.0", "react-router-bootstrap": "^0.26.3", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "vite": "^6.2.0" }, "scripts": { @@ -59,7 +60,7 @@ }, "devDependencies": { "@eslint/compat": "^1.2.7", - "@eslint/eslintrc": "^3.1.0", + "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", "@stylistic/eslint-plugin": "^3.0.1", "@testing-library/jest-dom": "^6.6.3", @@ -79,9 +80,9 @@ "globals": "^15.14.0", "jsdom": "^26.0.0", "prettier": "^3.5.2", - "sass": "^1.83.4", + "sass": "^1.85.1", "source-map-support": "^0.5.21", - "typescript-eslint": "^8.25.0", + "typescript-eslint": "^8.26.0", "vitest": "^3.0.7" } } diff --git a/client/src/__test__/__mocks__/authContextMock.ts b/client/src/__test__/__mocks__/authContextMock.ts index bb350b5..4b9b12d 100644 --- a/client/src/__test__/__mocks__/authContextMock.ts +++ b/client/src/__test__/__mocks__/authContextMock.ts @@ -7,7 +7,8 @@ const authContextMock = { signIn: vi.fn(), signOut: vi.fn(), register: vi.fn(), - isAdmin: false + isAdmin: false, + updateUser: vi.fn() }; export default authContextMock; diff --git a/client/src/__test__/components/AlertError.test.tsx b/client/src/__test__/components/AlertError.test.tsx new file mode 100644 index 0000000..0219820 --- /dev/null +++ b/client/src/__test__/components/AlertError.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import AlertError from '../../components/AlertError'; + +describe('AlertError Component tests', () => { + const dataTestId: string = "alert-error-msg-tests"; + const renderAlert = (message?: string) => { + return render( + + ); + }; + + it('should display the message if error', () => { + const errorMessage = "Oops, something went wrong"; + const { getByTestId } = renderAlert(errorMessage); + + expect(getByTestId(dataTestId)).toBeDefined(); + }); + + // Personal notes: queryByTestId should be used to find elements + // that possibly aren't in the document. getByTestId expect always + // to find the element. + it('should not display the message if no error', () => { + const errorMessage = ""; + const { queryByTestId } = renderAlert(errorMessage); + (errorMessage); + + expect(queryByTestId(dataTestId)).toBeNull(); + }); +}); diff --git a/client/src/__test__/components/ContentHeader.test.tsx b/client/src/__test__/components/ContentHeader.test.tsx new file mode 100644 index 0000000..9802dd2 --- /dev/null +++ b/client/src/__test__/components/ContentHeader.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MemoryRouter } from 'react-router'; +import ContentHeader from '../../components/ContentHeader'; + +const headerProps = { + h1TextRegular: "Title part one", + h1TextBold: "Title part two", + subtitle: "This is the subtitle", + h2BlackText: "Another text part one", + h2GreenText: "Another green text", + isHomeComponent: false +}; + +describe('ContentHeader test cases', () => { + it('should render all the text elements', () => { + const { getByText } = render(); + + expect(getByText(headerProps.h1TextRegular)).toBeDefined(); + expect(getByText(headerProps.h1TextBold)).toBeDefined(); + expect(getByText(headerProps.subtitle)).toBeDefined(); + expect(getByText(headerProps.h2BlackText)).toBeDefined(); + expect(getByText(headerProps.h2GreenText)).toBeDefined(); + }); + + it('should render all the text elements', () => { + const { getByText } = render( + + + + ); + + expect(getByText(headerProps.h1TextRegular)).toBeDefined(); + expect(getByText(headerProps.h1TextBold)).toBeDefined(); + expect(getByText(headerProps.subtitle)).toBeDefined(); + expect(getByText(headerProps.h2BlackText)).toBeDefined(); + expect(getByText(headerProps.h2GreenText)).toBeDefined(); + + // renders only if it's the home (dashboard) + expect(getByText("Add note")).toBeDefined(); + expect(getByText("Add task")).toBeDefined(); + }); +}); diff --git a/client/src/__test__/components/ModalMarkdown.test.tsx b/client/src/__test__/components/ModalMarkdown.test.tsx new file mode 100644 index 0000000..848928a --- /dev/null +++ b/client/src/__test__/components/ModalMarkdown.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { describe, vi, it, expect } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ModalMarkdown from '../../components/ModalMarkdown'; + +describe('ModalMarkdown Component', () => { + const props = { + show: true, + title: 'Test Title', + markdownText: '# Test Markdown', + onHide: vi.fn(), + }; + + it('should render the modal with the correct title and markdown text', () => { + render(); + + expect(screen.getByTestId('modal-header-title').innerHTML).toBe('Test Title'); + expect(screen.getByText('Test Markdown')).toBeDefined(); + }); + + it('should call onHide when the close button is clicked', () => { + render(); + + fireEvent.click(screen.getByText('Close')); + expect(props.onHide).toHaveBeenCalled(); + }); + + it('should not render the modal when show is false', () => { + render(); + + expect(screen.queryByTestId('modal-header-title')).toBeNull(); + }); + + it('should render "No title" when title is an empty string', () => { + render(); + + expect(screen.getByTestId('modal-header-title').innerHTML).toBe('No title'); + }); +}); \ No newline at end of file diff --git a/client/src/__test__/components/NoteTitle.test.tsx b/client/src/__test__/components/NoteTitle.test.tsx new file mode 100644 index 0000000..48df10c --- /dev/null +++ b/client/src/__test__/components/NoteTitle.test.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import NoteTitle from '../../components/NoteTitle'; + +describe('NoteTitle test cases', () => { + it('should render the note title', () => { + const { getByText } = render() + + expect(getByText('Test Title')).toBeDefined(); + }); +}); diff --git a/client/src/__test__/views/About.test.tsx b/client/src/__test__/views/About.test.tsx index e99f339..53a5cfa 100644 --- a/client/src/__test__/views/About.test.tsx +++ b/client/src/__test__/views/About.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; // import { MemoryRouter } from 'react-router'; -import { render, waitFor } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import About from '../../views/About'; import '../../i18n'; @@ -15,4 +15,16 @@ describe('Renders the about view', () => { expect(document.querySelector('.poppins-medium')).toBeDefined(); expect(document.querySelector('.poppins-light')).toBeDefined(); }); + + it('should render text based on new contentHeader component', () => { + const { getByText } = render( + + ); + + expect(getByText('About the')).toBeDefined(); + expect(getByText('TaskNote App')).toBeDefined(); + expect(getByText('Find more information about us and the app')).toBeDefined(); + expect(getByText('Tasks and notes made')).toBeDefined(); + expect(getByText('Easy')).toBeDefined(); + }); }); diff --git a/client/src/__test__/views/Account.test.tsx b/client/src/__test__/views/Account.test.tsx index 61d6005..2c9d89c 100644 --- a/client/src/__test__/views/Account.test.tsx +++ b/client/src/__test__/views/Account.test.tsx @@ -17,6 +17,7 @@ vi.mock('react-i18next', () => ({ useTranslation: () => ({ i18n: { changeLanguage: changeLanguageMock, + language: 'en' }, t: (key: string) => key, }), @@ -73,7 +74,7 @@ describe('Account Component', () => { const { getByText } = renderAccount(); const deleteButton = getByText('account_privacy_delete_btn'); fireEvent.click(deleteButton); - expect(getByText('account_delete_tittle')).toBeDefined(); + expect(getByText('account_delete_title')).toBeDefined(); }) it('should call deleteAccount API and signOut when delete is confirmed', async () => { @@ -112,4 +113,14 @@ describe('Account Component', () => { mockPatchJSON.mockRestore(); }); + + it('should render text based on new contentHeader component', () => { + const { getByText } = renderAccount(); + + expect(getByText('My')).toBeDefined(); + expect(getByText('Account')).toBeDefined(); + expect(getByText('account_my_account_hello')).toBeDefined(); + expect(getByText('Update and Manage, Your')).toBeDefined(); + expect(getByText('Data')).toBeDefined(); + }); }); diff --git a/client/src/__test__/views/Home.test.tsx b/client/src/__test__/views/Home.test.tsx new file mode 100644 index 0000000..1aa69c2 --- /dev/null +++ b/client/src/__test__/views/Home.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +// import { MemoryRouter } from 'react-router'; +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MemoryRouter } from 'react-router'; +import Home from '../../views/Home'; +import '../../i18n'; +import AuthContext from '../../context/AuthContext'; + +// Mock the Chart component +vi.mock('react-charts', () => ({ + Chart: ({ options }) =>
Mocked Chart
+})); + +vi.mock('../../api-service/api'); + +const authContextMock = { + signed: true, + user: { + userId: 1, + name: 'Ricardo', + email: 'ricardo@campos.com', + admin: false, + createdAt: new Date() + }, + checkCurrentAuthUser: vi.fn(), + signIn: vi.fn(), + signOut: vi.fn(), + register: vi.fn(), + isAdmin: false, + updateUser: vi.fn(), +}; + +describe('Renders the home view', () => { + it('should render text based on new contentHeader component', () => { + const { getByText } = render( + + + + + + ); + + expect(getByText('Hello,')).toBeDefined(); + expect(getByText('Ricardo')).toBeDefined(); + expect(getByText('Welcome to TaskNote! Get ready to complete your pending tasks')).toBeDefined(); + expect(getByText('Start Your Day, Be')).toBeDefined(); + expect(getByText('Productive')).toBeDefined(); + }); +}); diff --git a/client/src/__test__/views/Note.test.tsx b/client/src/__test__/views/Note.test.tsx new file mode 100644 index 0000000..0bcbee2 --- /dev/null +++ b/client/src/__test__/views/Note.test.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import api from '../../api-service/api'; +import { NoteResponse } from '../../types/NoteResponse'; +import ApiConfig from '../../api-service/apiConfig'; +import Note from '../../views/Note'; +import { MemoryRouter } from 'react-router'; + +vi.mock('../../api-service/api'); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + i18n: { + changeLanguage: vi.fn(), + language: 'en', + }, + t: (key: string) => key, + }), + initReactI18next: { + type: '3rdParty', + init: vi.fn(), + }, + I18nextProvider: ({ children }: any) => children, +})); + +const mockNotes: NoteResponse[] = [ + { + id: 1, + title: 'Test Note 1', + description: 'This is a test note 1', + url: 'http://example.com/1', + }, + { + id: 2, + title: 'Test Note 2', + description: 'This is a test note 2', + url: 'http://example.com/2', + }, +]; + +describe('Note Component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderNote = () => { + return render( + + + + ); + }; + + it('should render the Note component and load notes', async () => { + vi.spyOn(api, 'getJSON').mockResolvedValue(mockNotes); + + renderNote(); + + await waitFor(() => { + expect(screen.getByText('Test Note 1')).toBeDefined(); + expect(screen.getByText('Test Note 2')).toBeDefined(); + }); + }); + + it('should filter notes based on input', async () => { + vi.spyOn(api, 'getJSON').mockResolvedValue(mockNotes); + + renderNote(); + + await waitFor(() => { + expect(screen.getByText('Test Note 1')).toBeDefined(); + expect(screen.getByText('Test Note 2')).toBeDefined(); + }); + + fireEvent.change(screen.getByPlaceholderText('Filter notes'), { target: { value: '1' } }); + + expect(screen.getByText('Test Note 1')).toBeDefined(); + expect(screen.queryByText('Test Note 2')).toBeNull(); + }); + + it('should show the markdown preview modal when "Preview Markdown" is clicked', async () => { + vi.spyOn(api, 'getJSON').mockResolvedValue(mockNotes); + + renderNote(); + + await waitFor(() => { + expect(screen.getByText('Test Note 1')).toBeDefined(); + }); + + fireEvent.click(screen.getByTestId('preview-markdown-link-1')); + + expect(screen.getByTestId('modal-header-title').innerHTML).toBe('Test Note 1'); + expect(screen.getByText('This is a test note 1')).toBeDefined(); + }); + + it('should call deleteNote when delete action is clicked', async () => { + vi.spyOn(api, 'getJSON').mockResolvedValue(mockNotes); + vi.spyOn(api, 'deleteNoContent').mockResolvedValue(mockNotes); + + renderNote(); + + await waitFor(() => { + expect(screen.getByText('Test Note 1')).toBeDefined(); + }); + + fireEvent.click(screen.getByText('Test Note 1')); + fireEvent.click(screen.getByTestId('note-dropdown-menu-1')); + fireEvent.click(screen.getByTestId('note-dropdown-delete-item-1')); + + await waitFor(() => { + expect(api.deleteNoContent).toHaveBeenCalledWith(`${ApiConfig.notesUrl}/1`); + }); + }); +}); diff --git a/client/src/__test__/views/NoteAdd.test.tsx b/client/src/__test__/views/NoteAdd.test.tsx new file mode 100644 index 0000000..dec0d37 --- /dev/null +++ b/client/src/__test__/views/NoteAdd.test.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { render, fireEvent, waitFor, getByText, getByTestId } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MemoryRouter } from 'react-router'; +import { I18nextProvider } from 'react-i18next'; +import NoteAdd from '../../views/NoteAdd'; +import AuthContext from '../../context/AuthContext'; +import i18n from '../../i18n'; +import api from '../../api-service/api'; +import ApiConfig from '../../api-service/apiConfig'; +import TaskNoteRequest from '../../types/TaskNoteRequest'; +import { NoteResponse } from '../../types/NoteResponse'; + +vi.mock('../../api-service/api'); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + i18n: { + changeLanguage: vi.fn(), + language: 'en', + }, + t: (key: string) => key, + }), + initReactI18next: { + type: '3rdParty', + init: vi.fn(), + }, + I18nextProvider: ({ children }: any) => children, +})); + +const authContextMock = { + signed: true, + user: { + userId: 1, + name: 'Ricardo', + email: 'test@example.com', + admin: false, + createdAt: new Date() + }, + checkCurrentAuthUser: vi.fn(), + signIn: vi.fn(), + signOut: vi.fn(), + register: vi.fn(), + isAdmin: false, + updateUser: vi.fn(), +}; + +describe('NoteAdd Component', () => { + const renderNoteAdd = () => { + return render( + + + + + + + + ); + }; + + it('should render the NoteAdd component', () => { + const { getByText } = renderNoteAdd(); + expect(getByText('note_form_title_label')).toBeDefined(); + expect(getByText('task_form_url_label')).toBeDefined(); + expect(getByText('note_form_content_label')).toBeDefined(); + expect(getByText('note_form_submit')).toBeDefined(); + }); + + it('should show error message when form is invalid', async () => { + const { getByText, getByRole } = renderNoteAdd(); + const submitButton = getByRole('button', { name: 'note_form_submit' }); + fireEvent.click(submitButton); + await waitFor(() => { + expect(getByText('Please fill in all the fields')).toBeDefined(); + }); + }); + + it('should add a new note when form is valid', async () => { + const { getByLabelText, getByTestId, getByRole } = renderNoteAdd(); + const descriptionInput = getByLabelText('note_form_title_label') as HTMLInputElement; + const noteContentInput = getByTestId('note-content-input-area') as HTMLAreaElement; + const submitButton = getByRole('button', { name: 'note_form_submit' }); + + fireEvent.change(descriptionInput, { target: { value: 'New Note' } }); + fireEvent.change(noteContentInput, { target: { value: 'Note content' } }); + fireEvent.click(submitButton); + + await waitFor(() => { + const newNote: NoteResponse = { + id: 0, + title: 'New Note', + description: 'Note content', + url: '' + } + expect(api.postJSON).toHaveBeenCalledWith(ApiConfig.notesUrl, newNote); + }); + }); + + it('should render text based on new contentHeader component', () => { + const { getByText } = renderNoteAdd(); + + expect(getByText('All')).toBeDefined(); + expect(getByText('Notes')).toBeDefined(); + expect(getByText('Save your notes in plain text or Markdown format')).toBeDefined(); + expect(getByText('Create, Filter, and Easily Find')).toBeDefined(); + expect(getByText('Them')).toBeDefined(); + }); +}); diff --git a/client/src/__test__/views/TaskAdd.test.tsx b/client/src/__test__/views/TaskAdd.test.tsx index a1c72b7..cf64bb0 100644 --- a/client/src/__test__/views/TaskAdd.test.tsx +++ b/client/src/__test__/views/TaskAdd.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react'; +import { render, fireEvent, waitFor, getByText } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MemoryRouter } from 'react-router'; import { I18nextProvider } from 'react-i18next'; @@ -16,6 +16,7 @@ vi.mock('react-i18next', () => ({ useTranslation: () => ({ i18n: { changeLanguage: vi.fn(), + language: 'en', }, t: (key: string) => key, }), @@ -66,11 +67,11 @@ describe('TaskAdd Component', () => { }); it('should show error message when form is invalid', async () => { - const { getByTestId, getByRole } = renderTaskAdd(); + const { getByText, getByRole } = renderTaskAdd(); const submitButton = getByRole('button', { name: 'task_form_submit' }); fireEvent.click(submitButton); await waitFor(() => { - expect(getByTestId('add-task-error-message')).toBeDefined(); + expect(getByText('Please fill in all the fields')).toBeDefined(); }); }); @@ -94,4 +95,14 @@ describe('TaskAdd Component', () => { expect(api.postJSON).toHaveBeenCalledWith(ApiConfig.tasksUrl, newTask); }); }); + + it('should render text based on new contentHeader component', () => { + const { getByText } = renderTaskAdd(); + + expect(getByText('All')).toBeDefined(); + expect(getByText('Tasks')).toBeDefined(); + expect(getByText('Be on top of your TODO list')).toBeDefined(); + expect(getByText('Create, Filter, and Easily Find')).toBeDefined(); + expect(getByText('Them')).toBeDefined(); + }); }); diff --git a/client/src/components/AlertError/index.tsx b/client/src/components/AlertError/index.tsx new file mode 100644 index 0000000..a802274 --- /dev/null +++ b/client/src/components/AlertError/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Alert, Col, Row } from 'react-bootstrap'; + +type Props = { + errorMessage?: string; + dataTestid?: string; +}; + +/** + * Renders an AlertError message banner. + * + * @param {Props} props the AlertError props with the message to be displayed. + * @param {string} [props.errorMessage] Optional error message. + * @param {string} [props.dataTestid] Optional data-testid property. + * @returns {React.ReactNode} the AlertError rendered component. + */ +const AlertError: React.FC = (props: Props): React.ReactNode => { + return props.errorMessage && props.errorMessage.length > 0 + ? ( + + + + { props.errorMessage } + + + + ) + : null; +}; + +export default AlertError; diff --git a/client/src/components/ContentHeader/index.tsx b/client/src/components/ContentHeader/index.tsx new file mode 100644 index 0000000..0212690 --- /dev/null +++ b/client/src/components/ContentHeader/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { PlusCircleFill } from 'react-bootstrap-icons'; +import { NavLink } from 'react-router'; + +type Props = { + h1TextRegular: string; + h1TextBold: string; + subtitle: string; + h2BlackText: string; + h2GreenText: string; + isHomeComponent?: boolean; +}; + +/** + * Renders the content header component with the title, subtitle and another subtitle. + * + * @param {Props} props the ContentHeader props with text to the displayed. + * @param {string} props.h1TextRegular The first part of the h1 text block. + * @param {string} props.h1TextBold The second part of the h1 text block. + * @param {string} props.subtitle The header subtitle, muted. + * @param {string} props.h2BlackText The first part of the h2 text block, black. + * @param {string} props.h2GreenText The second part of the h2 text block, green. + * @returns {React.ReactNode} The rendered ContentHeader component. + */ +const ContentHeader: React.FC = (props: Props): React.ReactNode => { + return ( + <> +

+ {props.h1TextRegular} + {' '} + {props.h1TextBold} +

+

+ {props.subtitle} +

+ + + {/* If it's not the home component, display full column */} + +

+ {props.h2BlackText} +

+

+ {props.h2GreenText} +

+ + {props.isHomeComponent && ( + + + + + + + + + )} +
+ + ); +}; + +export default ContentHeader; diff --git a/client/src/components/FormInput/index.tsx b/client/src/components/FormInput/index.tsx index 315ba5a..5cf81e1 100644 --- a/client/src/components/FormInput/index.tsx +++ b/client/src/components/FormInput/index.tsx @@ -44,7 +44,6 @@ function FormInput(props: React.PropsWithChildren): React.ReactNode { e.preventDefault(); e.stopPropagation(); - console.log('clicked to show password'); setShowingPwd((prevValue: boolean) => !prevValue); setFormType((previous: string) => previous === 'text' ? 'password' : 'text'); }; @@ -64,6 +63,12 @@ function FormInput(props: React.PropsWithChildren): React.ReactNode { )} + {props.name === 'tag' && ( + + {' '} + Comma or space separated + + )} diff --git a/client/src/components/ModalMarkdown/index.tsx b/client/src/components/ModalMarkdown/index.tsx new file mode 100644 index 0000000..8167b96 --- /dev/null +++ b/client/src/components/ModalMarkdown/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import Markdown from 'react-markdown'; +import './style.css'; + +type Props = { + show: boolean; + title: string; + markdownText: string; + onHide: () => void; +}; + +/** + * Renders a modal with Markdown format text. + * + * @param {Props} props The ModalMarkdown props with show, title and text. + * @param {boolean} props.show Defines when to display the modal. + * @param {string} props.title The modal title to be displayed. + * @param {string} props.markdownText The Markdown text to be rendered. + * @param {Function} props.onHide The function to be called when closing the modal. + * @returns {React.ReactNode} the Markdown component rendered. + */ +const ModalMarkdown: React.FC = (props: Props): React.ReactNode => { + return props.show + ? ( + + + + {props.title === '' ? 'No title' : props.title} + + + + {props.markdownText} + + + + + + ) + : null; +}; + +export default ModalMarkdown; diff --git a/client/src/components/ModalMarkdown/style.css b/client/src/components/ModalMarkdown/style.css new file mode 100644 index 0000000..70be99b --- /dev/null +++ b/client/src/components/ModalMarkdown/style.css @@ -0,0 +1,14 @@ +.markdown-modal { + h1 { + font-size: 2em; + } + h2 { + font-size: 1.5em; + } + h3 { + font: 1.25em; + } + p { + font-size: 14px; + } +} diff --git a/client/src/components/NoteTitle/index.tsx b/client/src/components/NoteTitle/index.tsx new file mode 100644 index 0000000..279b965 --- /dev/null +++ b/client/src/components/NoteTitle/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface Props { + readonly title: string; +} + +/** + * Renders the TaskTitle component, displaying an icon and the task title. + * + * @param {Props} props - The props for the component. + * @param {string} [props.title] - The title for the task. + * @returns {React.ReactNode} The rendered TaskTitle component. + */ +function NoteTitle(props: React.PropsWithChildren): React.ReactNode { + return ( + + + {props.title} + + + ); +} + +export default NoteTitle; diff --git a/client/src/constants/english.ts b/client/src/constants/english.ts index c2eab82..96fb31e 100644 --- a/client/src/constants/english.ts +++ b/client/src/constants/english.ts @@ -108,10 +108,10 @@ const enTranslations = { organized. You can reach out to me at `, about_dev_description_two: ' for any questions or feedback.', - account_my_account_tittle: 'My Account', + account_my_account_title: 'My Account', account_my_account_hello: 'Hello! This is where you can manage your preferences', account_my_account_logged: 'You\'re logged in as: ', - account_app_lang_tittle: 'App Language', + account_app_lang_title: 'App Language', account_app_lang_description: 'You can choose one of these languages:', account_privacy_little: 'Your Privacy Matters', account_privacy_text: `We're committed to protecting your privacy and @@ -119,7 +119,7 @@ const enTranslations = { deletion at any time. Once processed, all your personal information will be permanently removed from our servers.`, account_privacy_delete_btn: 'Delete all my data', - account_delete_tittle: 'You are about to delete your account!', + account_delete_title: 'You are about to delete your account!', account_delete_description: `This action cannot be undone. If you really want to delete all your data, click the button below. If not, close this message and your data will be safe.`, diff --git a/client/src/constants/portuguese.ts b/client/src/constants/portuguese.ts index cab2c53..2eda915 100644 --- a/client/src/constants/portuguese.ts +++ b/client/src/constants/portuguese.ts @@ -109,10 +109,10 @@ const ptBrTranslations = { Você pode me contactar em `, about_dev_description_two: ' para qualquer dúvida ou comentário.', - account_my_account_tittle: 'Minha conta', + account_my_account_title: 'Minha conta', account_my_account_hello: 'Olá! Aqui é onde você pode gerenciar suas preferências.', account_my_account_logged: 'Você está logado como: ', - account_app_lang_tittle: 'Idioma do App', + account_app_lang_title: 'Idioma do App', account_app_lang_description: 'Você pode escolher um destes idiomas:', account_privacy_little: 'Sua Privacidade Importa', account_privacy_text: `Estamos comprometidos em proteger sua privacidade e te dar @@ -120,7 +120,7 @@ const ptBrTranslations = { a qualquer hora. Uma vez processado, todos os seus dados pessoais serão excluídos permanentemente dos nossos servidores.`, account_privacy_delete_btn: 'Excluir minha conta', - account_delete_tittle: 'Você está prestes a deletar sua conta!', + account_delete_title: 'Você está prestes a deletar sua conta!', account_delete_description: `Esta ação não pode ser desfeita. Se você realmente quer deletar todos os seus dados, clique no botão abaixo. Caso contrário, feche esta mensagem e seus dados estarão seguros.`, diff --git a/client/src/constants/russian.ts b/client/src/constants/russian.ts index 866de69..94528b4 100644 --- a/client/src/constants/russian.ts +++ b/client/src/constants/russian.ts @@ -108,10 +108,10 @@ const ruTranslations = { организованной. Вы можете связаться со мной по адресу `, about_dev_description_two: ' для любых вопросов или отзывов.', - account_my_account_tittle: 'Мой аккаунт', + account_my_account_title: 'Мой аккаунт', account_my_account_hello: 'Привет! Здесь вы можете управлять своими предпочтениями.', account_my_account_logged: 'Вы вошли как:', - account_app_lang_tittle: 'Язык приложения', + account_app_lang_title: 'Язык приложения', account_app_lang_description: 'Вы можете выбрать один из этих языков:', account_privacy_little: 'Ваша конфиденциальность имеет значение', account_privacy_text: `Мы стремимся защищать вашу конфиденциальность и @@ -119,7 +119,7 @@ const ruTranslations = { аккаунта в любое время. После обработки вся ваша личная информация будет навсегда удалена с наших серверов.`, account_privacy_delete_btn: 'Удалить все мои данные', - account_delete_tittle: 'Вы собираетесь удалить свой аккаунт!', + account_delete_title: 'Вы собираетесь удалить свой аккаунт!', account_delete_description: `Это действие нельзя отменить. Если вы действительно хотите удалить все свои данные, нажмите кнопку ниже. Если нет, закройте это сообщение, и ваши данные будут в безопасности.`, diff --git a/client/src/constants/spanish.ts b/client/src/constants/spanish.ts index e6ce521..65b6029 100644 --- a/client/src/constants/spanish.ts +++ b/client/src/constants/spanish.ts @@ -108,10 +108,10 @@ const esTranslations = { Puedes contactarme en `, about_dev_description_two: ' para cualquier duda o comentario.', - account_my_account_tittle: 'Mi Cuenta', + account_my_account_title: 'Mi Cuenta', account_my_account_hello: '¡Hola! Aquí puedes gestionar tus preferencias.', account_my_account_logged: 'Has iniciado sesión como: ', - account_app_lang_tittle: 'Idioma del App', + account_app_lang_title: 'Idioma del App', account_app_lang_description: 'Puedes elegir uno de estos idiomas:', account_privacy_little: 'Su Privacidad es Importante', account_privacy_text: `Nos comprometemos a proteger su privacidad y a brindarle @@ -119,7 +119,7 @@ const esTranslations = { cuenta en cualquier momento. Una vez procesada, toda su información personal será eliminada permanentemente de nuestros servidores.`, account_privacy_delete_btn: 'Borrar todos mis datos', - account_delete_tittle: 'Estás a punto de eliminar tu cuenta!', + account_delete_title: 'Estás a punto de eliminar tu cuenta!', account_delete_description: `Esta acción no se puede deshacer. Si realmente desea eliminar todos sus datos, haga clic en el botón que aparece a continuación. Si no es así, cierre este mensaje y sus datos estarán seguros.`, diff --git a/client/src/routes/index.tsx b/client/src/routes/index.tsx index 670f4b8..553b2fa 100644 --- a/client/src/routes/index.tsx +++ b/client/src/routes/index.tsx @@ -7,6 +7,7 @@ import Task from '../views/Task'; import TaskAdd from '../views/TaskAdd'; import Note from '../views/Note'; import Account from '../views/Account'; +import NoteAdd from '../views/NoteAdd'; const BrowserRoutes: RouteObject[] = [ { @@ -51,6 +52,14 @@ const BrowserRoutes: RouteObject[] = [ path: '/notes', element: }, + { + path: '/notes/new', + element: + }, + { + path: '/notes/edit/:id', + element: + }, { path: '/account', element: diff --git a/client/src/styles/custom.scss b/client/src/styles/custom.scss index d488235..857753e 100644 --- a/client/src/styles/custom.scss +++ b/client/src/styles/custom.scss @@ -192,3 +192,7 @@ a:hover, .btn-link:hover { .home-productive { color: #4CD964; } + +.font-size-14 { + font-size: 14px; +} diff --git a/client/src/types/NoteResponse.ts b/client/src/types/NoteResponse.ts index 139c106..3dcd74f 100644 --- a/client/src/types/NoteResponse.ts +++ b/client/src/types/NoteResponse.ts @@ -1,13 +1,8 @@ -type NoteUrlResponse = { - id: number; - url: string; -}; - type NoteResponse = { id: number; title: string; description: string; - urls: NoteUrlResponse[]; + url: string | null; }; -export type { NoteUrlResponse, NoteResponse }; +export type { NoteResponse }; diff --git a/client/src/views/About/index.tsx b/client/src/views/About/index.tsx index 032b23c..0665677 100644 --- a/client/src/views/About/index.tsx +++ b/client/src/views/About/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Col, Container, Row } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import ContentHeader from '../../components/ContentHeader'; /** * About page component. @@ -15,21 +16,13 @@ function About(): React.ReactNode { return ( -

- About - {' '} - TaskNoteApp -

-

- Here you can find more information about this app and the developer -

- - - -

Tasks and notes made

-

Easy

- -
+ diff --git a/client/src/views/Account/index.tsx b/client/src/views/Account/index.tsx index 7ea02ca..dc1d631 100644 --- a/client/src/views/Account/index.tsx +++ b/client/src/views/Account/index.tsx @@ -12,6 +12,8 @@ import FormInput from '../../components/FormInput'; import { translateServerResponse } from '../../utils/TranslatorUtils'; import { UserPatchRequest } from '../../types/UserPatchRequest'; import { UserResponse } from '../../types/UserResponse'; +import ContentHeader from '../../components/ContentHeader'; +import AlertError from '../../components/AlertError'; import './styles.css'; /** @@ -26,7 +28,6 @@ function Account(): React.ReactNode { const { i18n, t } = useTranslation(); const [showAlert, setShowAlert] = useState(false); const [validated, setValidated] = useState(false); - const [formInvalid, setFormInvalid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [userName, setUserName] = useState(''); const [userEmail, setUserEmail] = useState(''); @@ -61,11 +62,9 @@ function Account(): React.ReactNode { const handleError = (e: unknown): void => { if (typeof e === 'string') { setErrorMessage(translateServerResponse(e, i18n.language)); - setFormInvalid(true); } else if (e instanceof Error) { setErrorMessage(translateServerResponse(e.message, i18n.language)); - setFormInvalid(true); } }; @@ -93,7 +92,6 @@ function Account(): React.ReactNode { updateUser(userUpdated); setValidated(false); - setFormInvalid(false); }; const handleSubmit = async (event: React.FormEvent): Promise => { @@ -121,21 +119,13 @@ function Account(): React.ReactNode { return ( -

- My - {' '} - Account -

-

- {t('account_my_account_hello')} -

- - - -

Update and Manage, Your

-

Information

- -
+ @@ -149,13 +139,7 @@ function Account(): React.ReactNode { Update only what you need. Blank fields will not be updated - {formInvalid - ? ( - - { errorMessage } - - ) - : null} +
{/* User name */} @@ -286,7 +270,7 @@ function Account(): React.ReactNode { {showAlert && ( setShowAlert(false)} dismissible> - {t('account_delete_tittle')} + {t('account_delete_title')}

{t('account_delete_description')}

- - - - - - + @@ -154,13 +124,7 @@ function Home(): React.ReactNode { {t('home_card_search_label')} - {formInvalid - ? ( - - { errorMessage } - - ) - : null} + diff --git a/client/src/views/Note/index.tsx b/client/src/views/Note/index.tsx index 602ba71..9f1455f 100644 --- a/client/src/views/Note/index.tsx +++ b/client/src/views/Note/index.tsx @@ -1,142 +1,75 @@ import React, { useEffect, useState } from 'react'; import { - Accordion, Alert, Button, Card, Col, Container, Form, Row + Button, + Card, + Col, + Container, + Dropdown, + Form, + Row } from 'react-bootstrap'; +import { NavLink } from 'react-router'; import { useTranslation } from 'react-i18next'; -import TaskNoteRequest from '../../types/TaskNoteRequest'; import { NoteResponse } from '../../types/NoteResponse'; import api from '../../api-service/api'; import ApiConfig from '../../api-service/apiConfig'; import { translateServerResponse } from '../../utils/TranslatorUtils'; +import { ThreeDotsVertical } from 'react-bootstrap-icons'; +import TaskUrl from '../../components/TaskUrl'; +import NoteTitle from '../../components/NoteTitle'; +import ModalMarkdown from '../../components/ModalMarkdown'; +import ContentHeader from '../../components/ContentHeader'; +import AlertError from '../../components/AlertError'; import './style.css'; -type NoteAction = 'add' | 'edit'; - /** + * The Note component is a view that displays a list of notes. * + * @returns {React.ReactNode} The Task component */ function Note(): React.ReactNode { - const [validated, setValidated] = useState(false); - const [formInvalid, setFormInvalid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [notes, setNotes] = useState([]); - const [noteId, setNoteId] = useState(0); - const [noteTitle, setNoteTitle] = useState(''); - const [noteDescription, setNoteDescription] = useState(''); - const [action, setAction] = useState('add'); - const [textSizeLeft, setTextSizeLeft] = useState(2000); + const [savedNotes, setSavedNotes] = useState([]); + const [filterText, setFilterText] = useState(''); + const [showPreviewMd, setShowPreviewMd] = useState(false); + const [modalTitle, setModalTitle] = useState(''); + const [modalContent, setModalContent] = useState(''); const { i18n, t } = useTranslation(); + /** + * Handle errors from server requests and translate them if required. + * + * @param {unknown} e The error to be handled. + */ const handleError = (e: unknown): void => { if (typeof e === 'string') { setErrorMessage(translateServerResponse(e, i18n.language)); - setFormInvalid(true); } else if (e instanceof Error) { setErrorMessage(translateServerResponse(e.message, i18n.language)); - setFormInvalid(true); } }; + /** + * Load notes from the server. + */ const loadNotes = async () => { try { const notesFetched: NoteResponse[] = await api.getJSON(ApiConfig.notesUrl); - setNotes(notesFetched); + setNotes([...notesFetched]); + setSavedNotes([...notesFetched]); } catch (e) { handleError(e); } }; - const addNote = async (payload: TaskNoteRequest): Promise => { - try { - await api.postJSON(ApiConfig.notesUrl, payload); - loadNotes(); - return true; - } - catch (e) { - handleError(e); - } - - return false; - }; - - const submitEditNote = async (payload: NoteResponse): Promise => { - try { - await api.patchJSON(`${ApiConfig.notesUrl}/${payload.id}`, payload); - loadNotes(); - return true; - } - catch (e) { - handleError(e); - } - return false; - }; - - const resetInputs = () => { - setNoteId(0); - setNoteTitle(''); - setNoteDescription(''); - setTextSizeLeft(2000); - - setAction('add'); - setValidated(false); - setFormInvalid(false); - }; - - const handleSubmit = async (event: React.FormEvent): Promise => { - event.preventDefault(); - event.stopPropagation(); - setValidated(true); - - const form = event.currentTarget; - if (form.checkValidity() === false) { - setFormInvalid(true); - setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language)); - return; - } - - if (form.note_description.value.length > 2000) { - setFormInvalid(true); - setErrorMessage(translateServerResponse('The maximum text length is 2000', i18n.language)); - return; - } - - setFormInvalid(false); - - const title: string = form.note_title.value; - const description: string = form.note_description.value; - - if (action === 'add') { - const payload: TaskNoteRequest = { - title, - description, - urls: [], - tag: '' - }; - - const added: boolean = await addNote(payload); - if (added) { - form.reset(); - resetInputs(); - } - } - else if (action === 'edit') { - const payload: NoteResponse = { - id: noteId, - title, - description, - urls: [] - }; - - const edited: boolean = await submitEditNote(payload); - if (edited) { - form.reset(); - resetInputs(); - } - } - }; - + /** + * Delete a note. + * + * @param {number} noteIdParam The note ID to be deleted. + */ const deleteNote = async (noteIdParam: number) => { try { await api.deleteNoContent(`${ApiConfig.notesUrl}/${noteIdParam}`); @@ -147,125 +80,133 @@ function Note(): React.ReactNode { } }; - const editNote = async (note: NoteResponse) => { - setNoteId(note.id); - setNoteTitle(note.title); - setNoteDescription(note.description); - setAction('edit'); + /** + * Filter notes by a given text. + */ + const filterNotes = (text: string): void => { + setFilterText(text); + + if (!text) { + setNotes([...savedNotes]); + return; + } + + const filteredTasks = savedNotes.filter((note: NoteResponse) => { + const anyTitleMatch = note.title.toLowerCase().includes(text.toLowerCase()); + const anyContentMatch = note.description.toLowerCase().includes(text.toLowerCase()); + const anyUrlMatch = note.url?.includes(text.toLowerCase()); + return anyTitleMatch || anyContentMatch || anyUrlMatch; + }); + + setNotes([...filteredTasks]); }; + const handleCloseModal = () => setShowPreviewMd(false); + useEffect(() => { loadNotes(); }, []); return ( - - - - - {t('note_form_title')} + - {formInvalid - ? ( - - { errorMessage } - - ) - : null} + - - - {t('note_form_title_label')} - ) => { - setNoteTitle(e.target.value); - }} - placeholder={t('note_form_title_placeholder')} - /> - - - - {t('note_form_content_label')} - ) => { - setNoteDescription(e.target.value); - setTextSizeLeft(2000 - e.target.value.length); - }} - /> - - - The text area can have text length up to 2000. You still can type - {' '} - {textSizeLeft} - {' '} - characters. - - - - - - - + + + ) => filterNotes(e.target.value)} + /> + + + +
+ +
+
+ - - - {notes.map((note: NoteResponse) => ( - - - {note.title} - - - - { note.description } - + {notes.map((note: NoteResponse) => ( + + + + + + + + + + + + + + + + + + {t('task_table_action_edit')} + + + deleteNote(note.id)} + data-testid={`note-dropdown-delete-item-${note.id}`} + > + {t('task_table_action_delete')} + + + + + -
- - - -
-
- ))} -
- - {/* add pagination here */} - - + {note.url && note.url.length > 0 && ( + + )} +
+ + ) => { + e.preventDefault(); + e.stopPropagation(); + setModalTitle(note.title); + setModalContent(note.description); + setShowPreviewMd(true); + }} + data-testid={`preview-markdown-link-${note.id}`} + > + {' '} + Preview Markdown + + + + + ))}
+ +
); } diff --git a/client/src/views/NoteAdd/index.tsx b/client/src/views/NoteAdd/index.tsx new file mode 100644 index 0000000..36ef5e7 --- /dev/null +++ b/client/src/views/NoteAdd/index.tsx @@ -0,0 +1,291 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Card, + Col, + Container, + Form, + Row +} from 'react-bootstrap'; +import { useNavigate, useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import { NoteResponse } from '../../types/NoteResponse'; +import api from '../../api-service/api'; +import ApiConfig from '../../api-service/apiConfig'; +import { translateServerResponse } from '../../utils/TranslatorUtils'; +import FormInput from '../../components/FormInput'; +import ModalMarkdown from '../../components/ModalMarkdown'; +import AlertError from '../../components/AlertError'; +import ContentHeader from '../../components/ContentHeader'; + +type NoteAction = 'add' | 'edit'; + +/** + * NoteAdd component for adding and editing notes. + * + * @returns {React.ReactNode} The rendered NoteAdd component. + */ +function NoteAdd(): React.ReactNode { + const [validated, setValidated] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [noteId, setNoteId] = useState(0); + const [noteTitle, setNoteTitle] = useState(''); + const [noteContent, setNoteContent] = useState(''); + const [noteUrl, setNoteUrl] = useState(''); + const [action, setAction] = useState('add'); + const [showPreviewMd, setShowPreviewMd] = useState(false); + const { i18n, t } = useTranslation(); + const params = useParams(); + const navigate = useNavigate(); + + /** + * Handles errors by setting the error message and form invalid state. + * + * @param {unknown} e - The error to handle. + */ + const handleError = (e: unknown): void => { + if (typeof e === 'string') { + setErrorMessage(translateServerResponse(e, i18n.language)); + } + else if (e instanceof Error) { + setErrorMessage(translateServerResponse(e.message, i18n.language)); + } + }; + + /** + * Adds a new note. + * + * @param {TaskNoteRequest} payload - The task data to add. + * @returns {Promise} True if the task was added successfully, false otherwise. + */ + const addNote = async (payload: NoteResponse): Promise => { + try { + await api.postJSON(ApiConfig.notesUrl, payload); + return true; + } + catch (e) { + handleError(e); + } + + return false; + }; + + /** + * Submits the edited task. + * + * @param {TaskResponse} payload - The task data to edit. + * @returns {Promise} True if the task was edited successfully, false otherwise. + */ + const submitEditNote = async (payload: NoteResponse): Promise => { + try { + await api.patchJSON(`${ApiConfig.notesUrl}/${payload.id}`, payload); + return true; + } + catch (e) { + handleError(e); + } + return false; + }; + + /** + * Resets the input fields to their default values. + */ + const resetInputs = () => { + setNoteId(0); + setNoteTitle(''); + setNoteContent(''); + + setAction('add'); + setValidated(false); + }; + + /** + * Handles the form submission. + * + * @param {React.FormEvent} event - The form submission event. + */ + const handleSubmit = async (event: React.FormEvent): Promise => { + event.preventDefault(); + event.stopPropagation(); + setValidated(true); + + const form = event.currentTarget; + if (form.checkValidity() === false) { + setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language)); + return; + } + + if (action === 'add') { + const payload: NoteResponse = { + id: 0, + title: noteTitle, + description: noteContent, + url: noteUrl + }; + + const added: boolean = await addNote(payload); + if (added) { + form.reset(); + resetInputs(); + navigate('/notes'); + } + } + else if (action === 'edit') { + const payload: NoteResponse = { + id: noteId, + title: noteTitle, + description: noteContent, + url: noteUrl + }; + + const edited: boolean = await submitEditNote(payload); + if (edited) { + form.reset(); + resetInputs(); + navigate('/notes'); + } + } + }; + + /** + * Checks if the URL is for editing a task and loads the task data if it is. + */ + const checkEditUrl = async (): Promise => { + if (params.id) { + try { + const noteToEdit: NoteResponse = await api.getJSON(`${ApiConfig.notesUrl}/${params.id}`); + setNoteId(noteToEdit.id); + setNoteTitle(noteToEdit.title); + setNoteContent(noteToEdit.description); + setAction('edit'); + } + catch (e) { + handleError(e); + } + } + }; + + /** + * Display the Markdown text in Markdown format on a modal. + * + * @param {React.MouseEvent} e The mouse click event. + */ + const previewMarkdown = (e: React.MouseEvent): void => { + e.preventDefault(); + e.stopPropagation(); + setShowPreviewMd(noteContent.length > 0); + }; + + /** + * Closes the Markdown preview modal. + */ + const handleCloseModal = (): void => setShowPreviewMd(false); + + useEffect(() => { + checkEditUrl(); + }, []); + + return ( + + + + + + + + {t('note_form_title')} + + + +
+ {/* Note title */} + ) => { + setNoteTitle(e.target.value); + }} + /> + + {/* Note URL */} + ) => { + setNoteUrl(e.target.value); + }} + /> + + + + {t('note_form_content_label')} + + + {' '} + Preview Markdown + + + + ) => { + setNoteContent(e.target.value); + }} + data-testid="note-content-input-area" + /> + + + + + +
+
+ +
+ + +
+ ); +} + +export default NoteAdd; diff --git a/client/src/views/Task/index.tsx b/client/src/views/Task/index.tsx index eadefcb..40c30cc 100644 --- a/client/src/views/Task/index.tsx +++ b/client/src/views/Task/index.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import { NavLink } from 'react-router'; import { useTranslation } from 'react-i18next'; import { - Alert, Button, Card, Col, @@ -21,6 +20,8 @@ import TaskUrl from '../../components/TaskUrl'; import TaskTag from '../../components/TaskTag'; import TaskTitle from '../../components/TaskTitle'; import './style.css'; +import ContentHeader from '../../components/ContentHeader'; +import AlertError from '../../components/AlertError'; /** * The Task component is a view that displays a list of tasks. @@ -28,7 +29,6 @@ import './style.css'; * @returns {React.ReactNode} The Task component */ function Task(): React.ReactNode { - const [displayError, setDisplayError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [tasks, setTasks] = useState([]); const [savedTasks, setSavedTasks] = useState([]); @@ -43,11 +43,9 @@ function Task(): React.ReactNode { const handleError = (e: unknown): void => { if (typeof e === 'string') { setErrorMessage(translateServerResponse(e, i18n.language)); - setDisplayError(true); } else if (e instanceof Error) { setErrorMessage(translateServerResponse(e.message, i18n.language)); - setDisplayError(true); } }; @@ -118,7 +116,10 @@ function Task(): React.ReactNode { } const filteredTasks = savedTasks.filter((task: TaskResponse) => { - return task.description.toLowerCase().includes(text.toLowerCase()); + const shouldFilter = task.description.toLowerCase().includes(text.toLowerCase()) + || task.tag.toLowerCase().includes(text.toLowerCase()) + || task.urls.filter((url: string) => url.includes(text.toLowerCase())).length > 0; + return shouldFilter; }); setTasks([...filteredTasks]); @@ -130,18 +131,15 @@ function Task(): React.ReactNode { return ( - {/* Tasks added */} - - - {displayError - ? ( - - { errorMessage } - - ) - : null} - - + + + diff --git a/client/src/views/TaskAdd/index.tsx b/client/src/views/TaskAdd/index.tsx index 01bc40a..cb7ea24 100644 --- a/client/src/views/TaskAdd/index.tsx +++ b/client/src/views/TaskAdd/index.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { - Alert, Button, Card, Col, @@ -16,6 +15,8 @@ import api from '../../api-service/api'; import ApiConfig from '../../api-service/apiConfig'; import { translateServerResponse } from '../../utils/TranslatorUtils'; import FormInput from '../../components/FormInput'; +import ContentHeader from '../../components/ContentHeader'; +import AlertError from '../../components/AlertError'; type TaskAction = 'add' | 'edit'; @@ -26,7 +27,6 @@ type TaskAction = 'add' | 'edit'; */ function TaskAdd(): React.ReactNode { const [validated, setValidated] = useState(false); - const [formInvalid, setFormInvalid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [taskId, setTaskId] = useState(0); const [taskDescription, setTaskDescription] = useState(''); @@ -48,11 +48,9 @@ function TaskAdd(): React.ReactNode { const handleError = (e: unknown): void => { if (typeof e === 'string') { setErrorMessage(translateServerResponse(e, i18n.language)); - setFormInvalid(true); } else if (e instanceof Error) { setErrorMessage(translateServerResponse(e.message, i18n.language)); - setFormInvalid(true); } }; @@ -105,7 +103,6 @@ function TaskAdd(): React.ReactNode { setAction('add'); setValidated(false); - setFormInvalid(false); }; /** @@ -120,13 +117,10 @@ function TaskAdd(): React.ReactNode { const form = event.currentTarget; if (form.checkValidity() === false) { - setFormInvalid(true); setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language)); return; } - setFormInvalid(false); - if (action === 'add') { const addPayload: TaskNoteRequest = { description: taskDescription.trim(), @@ -197,22 +191,29 @@ function TaskAdd(): React.ReactNode { return ( + + {/* Form to add, edit and save tasks */} - + {t('task_form_title')} - {formInvalid - ? ( - - { errorMessage } - - ) - : null} + -
+ {/* Description */} LINE COVEREDRATIO - 45% + 78% diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java index fad579a..1c52fcb 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java @@ -1,6 +1,7 @@ package br.com.tasknoteapp.server.controller; import br.com.tasknoteapp.server.entity.NoteEntity; +import br.com.tasknoteapp.server.exception.NoteNotFoundException; import br.com.tasknoteapp.server.request.NotePatchRequest; import br.com.tasknoteapp.server.request.NoteRequest; import br.com.tasknoteapp.server.response.NoteResponse; @@ -61,6 +62,42 @@ public class NoteController { return noteService.getAllNotes(); } + /** + * Get a note by its ID. + * + * @param id Note identification. + * @return NoteResponse with note data and its urls, if any. + * @throws NoteNotFoundException when note not found. + */ + @GetMapping("/{id}") + @Operation( + summary = "Get a note by its ID", + description = "Get a note by its ID and its urls, if any.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Return the found Note and its urls, if any."), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Access Denied", + content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse( + responseCode = "404", + description = "Note not found", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public NoteResponse getTaskById( + @Parameter( + name = "id", + in = ParameterIn.PATH, + description = "Note id to be fetched.", + required = true, + schema = @Schema(type = "integer", format = "int64")) + @PathVariable + Long id) { + return noteService.getNoteById(id); + } + /** * Patch a note. * diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java index c96ea25..f449bda 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java @@ -8,9 +8,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; -import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -29,14 +28,15 @@ public class NoteEntity { private String title; + @Column(columnDefinition = "TEXT") private String description; @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false, updatable = false) @ManyToOne(fetch = FetchType.LAZY) private UserEntity user; - @OneToMany(mappedBy = "note", fetch = FetchType.LAZY) - private List urls; + @OneToOne(mappedBy = "note", fetch = FetchType.LAZY) + private NoteUrlEntity noteUrl; @Column(name = "tag", nullable = true, length = 30) private String tag; diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteUrlEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteUrlEntity.java index 3c14cd5..e225613 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteUrlEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteUrlEntity.java @@ -6,7 +6,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; @@ -27,6 +27,6 @@ public class NoteUrlEntity { private String url; @JoinColumn(name = "note_id", referencedColumnName = "id", nullable = false, updatable = false) - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) private NoteEntity note; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/NotesCreatedEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/NotesCreatedEntity.java new file mode 100644 index 0000000..7f6f3af --- /dev/null +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/NotesCreatedEntity.java @@ -0,0 +1,30 @@ +package br.com.tasknoteapp.server.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** This class represents a note created for the user in the database. */ +@Setter +@Getter +@Entity +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "notes_created") +public class NotesCreatedEntity { + + @Id + Long userId; + + @Column + Integer count; +} diff --git a/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java b/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java index 4660252..d0a1b20 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java +++ b/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java @@ -51,43 +51,34 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return; } - try { - String jwtToken = authorizationHeader; - - if (authorizationHeader.startsWith("Bearer ")) { - jwtToken = authorizationHeader.substring(7); - } - - String email = jwtService.getEmailFromToken(jwtToken); - - if (Objects.isNull(email)) { - throw new ServletException("Invalid token: email not found"); - } - - UserDetails user = userService.userDetailsService().loadUserByUsername(email); - - if (!jwtService.validateTokenAndUser(jwtToken, user)) { - throw new ServletException("Invalid token for user"); - } - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( - user.getUsername(), user.getPassword(), user.getAuthorities()); - - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - context.setAuthentication(authToken); - SecurityContextHolder.setContext(context); - - filterChain.doFilter(request, response); + String jwtToken = authorizationHeader; + + if (authorizationHeader.startsWith("Bearer ")) { + jwtToken = authorizationHeader.substring(7); } - catch (Exception e) { - log.error("Error authenticating user", e); - SecurityContextHolder.clearContext(); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Unauthorized: " + e.getMessage()); - return; + + String email = jwtService.getEmailFromToken(jwtToken); + + if (Objects.isNull(email)) { + throw new ServletException("Invalid token: email not found"); } + + UserDetails user = userService.userDetailsService().loadUserByUsername(email); + + if (!jwtService.validateTokenAndUser(jwtToken, user)) { + throw new ServletException("Invalid token for user"); + } + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + user.getUsername(), user.getPassword(), user.getAuthorities()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + context.setAuthentication(authToken); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java index 846de16..27671b7 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java @@ -1,11 +1,10 @@ package br.com.tasknoteapp.server.repository; import br.com.tasknoteapp.server.entity.NoteUrlEntity; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; /** This interface represents a note url repository, for database access. */ public interface NoteUrlRepository extends JpaRepository { - void deleteAllByIdIn(List ids); + void deleteByNote_id(Long noteId); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/NotesCreatedRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/NotesCreatedRepository.java new file mode 100644 index 0000000..70607ab --- /dev/null +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/NotesCreatedRepository.java @@ -0,0 +1,7 @@ +package br.com.tasknoteapp.server.repository; + +import br.com.tasknoteapp.server.entity.NotesCreatedEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +/** This interface represents a notes created for a user in the database. */ +public interface NotesCreatedRepository extends JpaRepository {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/UserTasksDoneRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/UserTasksDoneRepository.java index e0106a9..aee95da 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/UserTasksDoneRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/UserTasksDoneRepository.java @@ -10,4 +10,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface UserTasksDoneRepository extends JpaRepository { List findAllByDoneDateAfterAndId_userId(LocalDateTime date, Long userId); + + List findAllById_userId(Long userId); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java index 4f3e00d..00d6077 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java @@ -1,11 +1,10 @@ package br.com.tasknoteapp.server.request; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; /** This record represents a note patch payload. */ @Schema(description = "Note patch payload.") public record NotePatchRequest( @Schema(description = "Note title. Optional.") String title, @Schema(description = "Note description. Optional.") String description, - @Schema(description = "Note urls. Optional.") List urls) {} + @Schema(description = "Note urls. Optional.") String url) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java index b3466b6..d207a2f 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java @@ -2,11 +2,10 @@ package br.com.tasknoteapp.server.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.util.List; /** This record represents a note request to be created. */ @Schema(description = "Note request to be created.") public record NoteRequest( @Schema(description = "Note title.") @NotNull String title, @Schema(description = "Note description.") @NotNull String description, - @Schema(description = "Note urls. Optional.") List urls) {} + @Schema(description = "Note urls. Optional.") String url) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java index a11583f..8fd99ec 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java @@ -3,8 +3,6 @@ package br.com.tasknoteapp.server.response; import br.com.tasknoteapp.server.entity.NoteEntity; import br.com.tasknoteapp.server.entity.NoteUrlEntity; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; /** This record represents a task and its urls object to be returned. */ @@ -13,8 +11,7 @@ public record NoteResponse( @Schema(description = "The id of the note", example = "1") Long id, @Schema(description = "The title of the note", example = "Note 1") String title, @Schema(description = "The description of the note", example = "Note desc") String description, - @Schema(description = "The urls of the task, zero, one or more.", example = "[]") - List urls, + @Schema(description = "The urls of the task, zero, one or more.", example = "[]") String url, @Schema(description = "Task tag, optional.") String tag) { /** @@ -24,18 +21,10 @@ public record NoteResponse( * @return NoteResponse instance with all note data and urls, if any. */ public static NoteResponse fromEntity(NoteEntity entity) { - List urls = entity.getUrls(); - List urlsResponse = new ArrayList<>(); - if (Objects.isNull(urls)) { - urls = List.of(); - } else { - for (NoteUrlEntity url : urls) { - NoteUrlResponse urlResponse = new NoteUrlResponse(url.getId(), url.getUrl()); - urlsResponse.add(urlResponse); - } - } + NoteUrlEntity noteUrl = entity.getNoteUrl(); + String url = Objects.isNull(noteUrl) ? null : noteUrl.getUrl(); return new NoteResponse( - entity.getId(), entity.getTitle(), entity.getDescription(), urlsResponse, entity.getTag()); + entity.getId(), entity.getTitle(), entity.getDescription(), url, entity.getTag()); } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/HomeService.java b/server/src/main/java/br/com/tasknoteapp/server/service/HomeService.java index 3aed389..83c8e48 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/HomeService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/HomeService.java @@ -1,7 +1,9 @@ package br.com.tasknoteapp.server.service; +import br.com.tasknoteapp.server.entity.NotesCreatedEntity; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.entity.UserTasksDone; +import br.com.tasknoteapp.server.repository.NotesCreatedRepository; import br.com.tasknoteapp.server.repository.UserTasksDoneRepository; import br.com.tasknoteapp.server.response.NoteResponse; import br.com.tasknoteapp.server.response.SearchResponse; @@ -37,6 +39,8 @@ public class HomeService { private final AuthService authService; + private final NotesCreatedRepository notesCreatedRepository; + /** * Get summary for the home page. * @@ -44,14 +48,24 @@ public class HomeService { */ public SummaryResponse getSummary() { log.info("Getting summary"); + + Optional currentUserEmail = authUtil.getCurrentUserEmail(); + String email = currentUserEmail.orElseThrow(); + UserEntity user = authService.findByEmail(email).orElseThrow(); + + // Pending tasks List tasks = taskService.getAllTasks(); - List notes = noteService.getAllNotes(); - - long doneCount = tasks.stream().filter(t -> t.done() == true).count(); long undoneCount = tasks.stream().filter(t -> t.done() == false).count(); - int taskCount = notes.size(); - return new SummaryResponse((int) undoneCount, (int) doneCount, taskCount); + // All notes created for the user (including deleted ones) + Optional userNotes = notesCreatedRepository.findById(user.getId()); + int noteCount = userNotes.isPresent() ? userNotes.get().getCount() : 0; + + // All completed tasks (including deleted ones) + List doneTasks = userTasksDoneRepository.findAllById_userId(user.getId()); + int doneCount = doneTasks.size(); + + return new SummaryResponse((int) undoneCount, doneCount, noteCount); } /** diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java index bf2c68f..d38c88d 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java @@ -2,17 +2,18 @@ package br.com.tasknoteapp.server.service; import br.com.tasknoteapp.server.entity.NoteEntity; import br.com.tasknoteapp.server.entity.NoteUrlEntity; +import br.com.tasknoteapp.server.entity.NotesCreatedEntity; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.exception.NoteNotFoundException; +import br.com.tasknoteapp.server.exception.TaskNotFoundException; import br.com.tasknoteapp.server.repository.NoteRepository; import br.com.tasknoteapp.server.repository.NoteUrlRepository; +import br.com.tasknoteapp.server.repository.NotesCreatedRepository; import br.com.tasknoteapp.server.request.NotePatchRequest; import br.com.tasknoteapp.server.request.NoteRequest; -import br.com.tasknoteapp.server.request.NoteUrlPatchRequest; import br.com.tasknoteapp.server.response.NoteResponse; import br.com.tasknoteapp.server.util.AuthUtil; import jakarta.transaction.Transactional; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -34,6 +35,8 @@ public class NoteService { private final NoteUrlRepository noteUrlRepository; + private final NotesCreatedRepository notesCreatedRepository; + /** * Get all notes for the current user. * @@ -50,6 +53,25 @@ public class NoteService { return notes.stream().map(NoteResponse::fromEntity).toList(); } + /** + * Get a note by its id. + * + * @param noteId The task id in the database. + * @return {@link NoteResponse} with the found task or throw a {@link TaskNotFoundException}. + */ + public NoteResponse getNoteById(Long noteId) { + UserEntity user = getCurrentUser(); + log.info("Get note {} to user {}", noteId, user.getId()); + + Optional task = noteRepository.findById(noteId); + if (task.isEmpty()) { + throw new NoteNotFoundException(); + } + + log.info("Note found! Id {}", noteId); + return NoteResponse.fromEntity(task.get()); + } + /** * Create a note for the user. * @@ -67,11 +89,24 @@ public class NoteService { note.setUser(user); NoteEntity created = noteRepository.save(note); - if (!Objects.isNull(noteRequest.urls()) && !noteRequest.urls().isEmpty()) { - List urls = saveUrls(note, noteRequest.urls()); - note.setUrls(urls); + if (!Objects.isNull(noteRequest.url()) && !noteRequest.url().isEmpty()) { + NoteUrlEntity urlEntity = saveUrl(note, noteRequest.url()); + note.setNoteUrl(urlEntity); } + Optional userNote = notesCreatedRepository.findById(user.getId()); + NotesCreatedEntity userNoteEntity = null; + if (userNote.isPresent()) { + userNoteEntity = userNote.get(); + userNoteEntity.setCount(userNoteEntity.getCount() + 1); + } else { + userNoteEntity = new NotesCreatedEntity(); + userNoteEntity.setCount(1); + userNoteEntity.setUserId(user.getId()); + } + + notesCreatedRepository.save(userNoteEntity); + log.info("Note created! Id {}", created.getId()); return created; } @@ -102,21 +137,14 @@ public class NoteService { noteEntity.setDescription(patch.description()); } - if (!Objects.isNull(patch.urls())) { - List urlIds = - patch.urls().stream().filter(p -> p.id() != null).map(NoteUrlPatchRequest::id).toList(); - if (!urlIds.isEmpty()) { - noteUrlRepository.deleteAllByIdIn(urlIds); - log.info("Deleted {} urls from task {}", urlIds.size(), noteId); - } else { - log.info("No urls to patch for task {}", noteId); - } + noteUrlRepository.deleteByNote_id(noteId); + log.info("URL deleted from task {}", noteId); - List urlsList = - patch.urls().stream().filter(p -> p.id() == null).map(NoteUrlPatchRequest::url).toList(); - List urls = saveUrls(noteEntity, urlsList); - - noteEntity.setUrls(urls); + if (!Objects.isNull(patch.url()) && !patch.url().isBlank()) { + NoteUrlEntity urlEntity = saveUrl(noteEntity, patch.url()); + noteEntity.setNoteUrl(urlEntity); + } else { + log.info("No urls to patch for task {}", noteId); } NoteEntity patchedNote = noteRepository.save(noteEntity); @@ -142,10 +170,10 @@ public class NoteService { throw new NoteNotFoundException(); } - List urls = note.get().getUrls(); - if (!urls.isEmpty()) { - noteUrlRepository.deleteAll(urls); - log.info("Deleted {} urls from task {}", urls.size(), noteId); + NoteUrlEntity noteUrl = note.get().getNoteUrl(); + if (!Objects.isNull(noteUrl)) { + noteUrlRepository.delete(noteUrl); + log.info("URL Deleted from task {}", noteId); } else { log.info("No urls to delete for task {}", noteId); } @@ -179,18 +207,14 @@ public class NoteService { return authService.findByEmail(email).orElseThrow(); } - private List saveUrls(NoteEntity noteEntity, List urls) { - List tasksUrl = new ArrayList<>(); - for (String url : urls) { - NoteUrlEntity taskUrl = new NoteUrlEntity(); - taskUrl.setUrl(url); - taskUrl.setNote(noteEntity); - tasksUrl.add(taskUrl); - } + private NoteUrlEntity saveUrl(NoteEntity noteEntity, String url) { + NoteUrlEntity taskUrl = new NoteUrlEntity(); + taskUrl.setUrl(url); + taskUrl.setNote(noteEntity); - List savedUrls = noteUrlRepository.saveAll(tasksUrl); - log.info("Saved {} urls to note {}", savedUrls.size(), noteEntity.getId()); + NoteUrlEntity savedUrl = noteUrlRepository.save(taskUrl); + log.info("URL saved to note {}", noteEntity.getId()); - return savedUrls; + return savedUrl; } } diff --git a/server/src/main/resources/db/migration/V202503071059__drop_taskdone_fk.sql b/server/src/main/resources/db/migration/V202503071059__drop_taskdone_fk.sql new file mode 100644 index 0000000..bb41ded --- /dev/null +++ b/server/src/main/resources/db/migration/V202503071059__drop_taskdone_fk.sql @@ -0,0 +1,8 @@ +ALTER TABLE tasknote.user_tasks_done + DROP CONSTRAINT user_tasks_done_task_id_fk; + +ALTER TABLE tasknote.notes + ALTER COLUMN description TYPE TEXT; + +ALTER TABLE tasknote.notes + DROP CONSTRAINT chk_max_length; diff --git a/server/src/main/resources/db/migration/V202503080944__create_note_created_table.sql b/server/src/main/resources/db/migration/V202503080944__create_note_created_table.sql new file mode 100644 index 0000000..63a2334 --- /dev/null +++ b/server/src/main/resources/db/migration/V202503080944__create_note_created_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS tasknote.notes_created ( + user_id INTEGER NOT NULL, + count INTEGER NOT NULL, + CONSTRAINT notes_created_pk PRIMARY KEY (user_id), + CONSTRAINT notes_created_user_id_fk FOREIGN KEY (user_id) REFERENCES tasknote.users (id) +); diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/HomeControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/HomeControllerTest.java index fdf142e..deb09cb 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/HomeControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/HomeControllerTest.java @@ -79,7 +79,7 @@ class HomeControllerTest { TaskResponse task = new TaskResponse( 1L, "Task 1", false, true, LocalDate.now(), "2024-11-20", "now", "tag", List.of()); - NoteResponse note = new NoteResponse(1L, "Note 1", "Note desc", List.of(), "tag"); + NoteResponse note = new NoteResponse(1L, "Note 1", "Note desc", null, "tag"); SearchResponse response = new SearchResponse(List.of(task), List.of(note)); when(homeService.search("term")).thenReturn(response); @@ -100,7 +100,7 @@ class HomeControllerTest { .andExpect(jsonPath("$.notes[0].id").value(note.id())) .andExpect(jsonPath("$.notes[0].title").value(note.title())) .andExpect(jsonPath("$.notes[0].description").value(note.description())) - .andExpect(jsonPath("$.notes[0].urls", Matchers.empty())) + .andExpect(jsonPath("$.notes[0].url", Matchers.nullValue())) .andReturn(); } diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java index 8c847ac..dc046c0 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java @@ -43,7 +43,7 @@ class NoteControllerTest { @WithMockUser(username = "user@domain.com", password = "abcde123456A@") void getAllNotes_notesFound_shouldSucceed() throws Exception { NoteUrlResponse noteUrl = new NoteUrlResponse(111L, "https://test.com"); - NoteResponse note = new NoteResponse(111L, "title", "description", List.of(noteUrl), "tag"); + NoteResponse note = new NoteResponse(111L, "title", "description", "https://test.com", "tag"); when(noteService.getAllNotes()).thenReturn(List.of(note)); @@ -57,8 +57,7 @@ class NoteControllerTest { .andExpect(jsonPath("$[0].id").value(note.id())) .andExpect(jsonPath("$[0].title").value(note.title())) .andExpect(jsonPath("$[0].description").value(note.description())) - .andExpect(jsonPath("$[0].urls[0].id").value(noteUrl.id())) - .andExpect(jsonPath("$[0].urls[0].url").value(noteUrl.url())) + .andExpect(jsonPath("$[0].url").value(noteUrl.url())) .andReturn(); } @@ -97,10 +96,10 @@ class NoteControllerTest { @WithMockUser(username = "user@domain.com", password = "abcde123456A@") void patchNote_happyPath_shouldSucceed() throws Exception { Long noteId = 123L; - NotePatchRequest patchRequest = new NotePatchRequest("New title", "New description", List.of()); + NotePatchRequest patchRequest = new NotePatchRequest("New title", "New description", null); NoteResponse response = - new NoteResponse(noteId, patchRequest.title(), patchRequest.description(), List.of(), "tag"); + new NoteResponse(noteId, patchRequest.title(), patchRequest.description(), null, "tag"); when(noteService.patchNote(noteId, patchRequest)).thenReturn(response); @@ -109,7 +108,7 @@ class NoteControllerTest { { "title": "New title", "description": "New description", - "urls": [] + "url": null } """; @@ -124,7 +123,7 @@ class NoteControllerTest { .andExpect(jsonPath("$.id").value(response.id())) .andExpect(jsonPath("$.title").value(response.title())) .andExpect(jsonPath("$.description").value(response.description())) - .andExpect(jsonPath("$.urls", Matchers.empty())) + .andExpect(jsonPath("$.url", Matchers.nullValue())) .andReturn(); } @@ -133,7 +132,7 @@ class NoteControllerTest { @WithMockUser(username = "user@domain.com", password = "abcde123456A@") void patchNote_notFound_shouldFail() throws Exception { Long noteId = 123L; - NotePatchRequest patchRequest = new NotePatchRequest("New title", "New description", List.of()); + NotePatchRequest patchRequest = new NotePatchRequest("New title", "New description", null); when(noteService.patchNote(noteId, patchRequest)).thenThrow(new NoteNotFoundException()); @@ -142,7 +141,7 @@ class NoteControllerTest { { "title": "New title", "description": "New description", - "urls": [] + "url": null } """; @@ -185,8 +184,8 @@ class NoteControllerTest { @Test @DisplayName("Post create note happy path should succeed and return 201") @WithMockUser(username = "user@domain.com", password = "abcde123456A@") - void postNotes_happyPath_shoundSucceed() throws Exception { - NoteRequest request = new NoteRequest("Title", "Description", List.of()); + void postNotes_happyPath_shouldSucceed() throws Exception { + NoteRequest request = new NoteRequest("Title", "Description", null); NoteEntity entity = new NoteEntity(); entity.setId(1L); @@ -200,7 +199,7 @@ class NoteControllerTest { { "title": "Title", "description": "Description", - "urls": [] + "url": null } """; @@ -215,7 +214,7 @@ class NoteControllerTest { .andExpect(jsonPath("$.id").value(entity.getId())) .andExpect(jsonPath("$.title").value(entity.getTitle())) .andExpect(jsonPath("$.description").value(entity.getDescription())) - .andExpect(jsonPath("$.urls", Matchers.empty())) + .andExpect(jsonPath("$.url", Matchers.nullValue())) .andReturn(); } diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/HomeServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/HomeServiceTest.java index 2042b92..2fd3e49 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/HomeServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/HomeServiceTest.java @@ -3,9 +3,11 @@ package br.com.tasknoteapp.server.service; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import br.com.tasknoteapp.server.entity.NotesCreatedEntity; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.entity.UserTasksDone; import br.com.tasknoteapp.server.entity.UserTasksDonePk; +import br.com.tasknoteapp.server.repository.NotesCreatedRepository; import br.com.tasknoteapp.server.repository.UserTasksDoneRepository; import br.com.tasknoteapp.server.response.NoteResponse; import br.com.tasknoteapp.server.response.SearchResponse; @@ -37,6 +39,8 @@ class HomeServiceTest { @Mock private AuthService authService; + @Mock private NotesCreatedRepository notesCreatedRepository; + private HomeService homeService; private List tasks; @@ -45,7 +49,13 @@ class HomeServiceTest { @BeforeEach void setUp() { homeService = - new HomeService(taskService, noteService, userTasksDoneRepository, authUtil, authService); + new HomeService( + taskService, + noteService, + userTasksDoneRepository, + authUtil, + authService, + notesCreatedRepository); TaskResponse task1 = new TaskResponse(2L, "Task 1", false, false, null, null, null, "tag", List.of()); @@ -53,15 +63,25 @@ class HomeServiceTest { new TaskResponse(3L, "Task 2", false, false, null, null, null, "tag", List.of()); tasks = List.of(task1, task2); - NoteResponse note1 = new NoteResponse(453L, "Note 1", "desc", List.of(), "tag"); - NoteResponse note2 = new NoteResponse(455L, "Note 2", "desc", List.of(), "tag"); + NoteResponse note1 = new NoteResponse(453L, "Note 1", "desc", null, "tag"); + NoteResponse note2 = new NoteResponse(455L, "Note 2", "desc", null, "tag"); notes = List.of(note1, note2); } @Test void getSummary_shouldReturnSummaryResponse() { + String userEmail = "user@domain.com"; + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(userEmail)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(1L); + userEntity.setEmail(userEmail); + when(authService.findByEmail(userEmail)).thenReturn(Optional.of(userEntity)); + when(taskService.getAllTasks()).thenReturn(tasks); - when(noteService.getAllNotes()).thenReturn(notes); + + NotesCreatedEntity notesCreated = new NotesCreatedEntity(userEntity.getId(), 2); + when(notesCreatedRepository.findById(userEntity.getId())).thenReturn(Optional.of(notesCreated)); SummaryResponse summary = homeService.getSummary(); diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java new file mode 100644 index 0000000..2826b0f --- /dev/null +++ b/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java @@ -0,0 +1,184 @@ +package br.com.tasknoteapp.server.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import br.com.tasknoteapp.server.entity.NoteEntity; +import br.com.tasknoteapp.server.entity.NoteUrlEntity; +import br.com.tasknoteapp.server.entity.NotesCreatedEntity; +import br.com.tasknoteapp.server.entity.UserEntity; +import br.com.tasknoteapp.server.exception.NoteNotFoundException; +import br.com.tasknoteapp.server.repository.NoteRepository; +import br.com.tasknoteapp.server.repository.NoteUrlRepository; +import br.com.tasknoteapp.server.repository.NotesCreatedRepository; +import br.com.tasknoteapp.server.request.NotePatchRequest; +import br.com.tasknoteapp.server.request.NoteRequest; +import br.com.tasknoteapp.server.response.NoteResponse; +import br.com.tasknoteapp.server.util.AuthUtil; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NoteServiceTest { + + @Mock private NoteRepository noteRepository; + + @Mock private AuthService authService; + + @Mock private AuthUtil authUtil; + + @Mock private NoteUrlRepository noteUrlRepository; + + @Mock private NotesCreatedRepository notesCreatedRepository; + + @InjectMocks private NoteService noteService; + + private UserEntity user; + private NoteEntity note; + private NoteRequest noteRequest; + private NotePatchRequest notePatchRequest; + + @BeforeEach + void setUp() { + user = new UserEntity(); + user.setId(1L); + user.setEmail("test@example.com"); + + note = new NoteEntity(); + note.setId(1L); + note.setTitle("Test Note"); + note.setDescription("Test Description"); + note.setUser(user); + + noteRequest = new NoteRequest("Test Note", "Test Description", "http://example.com"); + notePatchRequest = + new NotePatchRequest("Updated Note", "Updated Description", "http://example.com"); + } + + @Test + void getAllNotes() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findAllByUser_id(user.getId())).thenReturn(List.of(note)); + + List notes = noteService.getAllNotes(); + + assertEquals(1, notes.size()); + assertEquals("Test Note", notes.get(0).title()); + verify(noteRepository, times(1)).findAllByUser_id(user.getId()); + } + + @Test + void getNoteById() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + + NoteResponse noteResponse = noteService.getNoteById(note.getId()); + + assertEquals("Test Note", noteResponse.title()); + verify(noteRepository, times(1)).findById(note.getId()); + } + + @Test + void getNoteById_NotFound() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findById(note.getId())).thenReturn(Optional.empty()); + Long noteId = note.getId(); + + assertThrows(NoteNotFoundException.class, () -> noteService.getNoteById(noteId)); + } + + @Test + void createNote() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); + when(noteUrlRepository.save(any(NoteUrlEntity.class))).thenReturn(new NoteUrlEntity()); + when(notesCreatedRepository.findById(user.getId())).thenReturn(Optional.empty()); + + NoteEntity createdNote = noteService.createNote(noteRequest); + + assertEquals("Test Note", createdNote.getTitle()); + verify(noteRepository, times(1)).save(any(NoteEntity.class)); + verify(noteUrlRepository, times(1)).save(any(NoteUrlEntity.class)); + verify(notesCreatedRepository, times(1)).save(any(NotesCreatedEntity.class)); + } + + @Test + void createNote_withExistingCount() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + NotesCreatedEntity noteUrlEntity = new NotesCreatedEntity(); + noteUrlEntity.setCount(6); + when(notesCreatedRepository.findById(user.getId())).thenReturn(Optional.of(noteUrlEntity)); + when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); + when(noteUrlRepository.save(any(NoteUrlEntity.class))).thenReturn(new NoteUrlEntity()); + + NoteEntity createdNote = noteService.createNote(noteRequest); + + assertEquals("Test Note", createdNote.getTitle()); + verify(noteRepository, times(1)).save(any(NoteEntity.class)); + verify(noteUrlRepository, times(1)).save(any(NoteUrlEntity.class)); + verify(notesCreatedRepository, times(1)).save(any(NotesCreatedEntity.class)); + } + + @Test + void patchNote() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); + + NoteResponse patchedNote = noteService.patchNote(note.getId(), notePatchRequest); + + assertEquals("Updated Note", patchedNote.title()); + verify(noteRepository, times(1)).findById(note.getId()); + verify(noteRepository, times(1)).save(any(NoteEntity.class)); + } + + @Test + void patchNote_notFound() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findById(note.getId())).thenReturn(Optional.empty()); + Long noteId = note.getId(); + + assertThrows( + NoteNotFoundException.class, () -> noteService.patchNote(noteId, notePatchRequest)); + } + + @Test + void deleteNote() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + + noteService.deleteNote(note.getId()); + + verify(noteRepository, times(1)).findById(note.getId()); + verify(noteRepository, times(1)).delete(note); + } + + @Test + void searchNotes() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); + when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(noteRepository.findAllBySearchTerm(anyString(), eq(user.getId()))) + .thenReturn(List.of(note)); + + List notes = noteService.searchNotes("Test"); + + assertEquals(1, notes.size()); + assertEquals("Test Note", notes.get(0).title()); + verify(noteRepository, times(1)).findAllBySearchTerm(anyString(), eq(user.getId())); + } +}