diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 24d3815..cb9b9a3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,17 +1,16 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: awaiting triage, bug assignees: mtbitcr - --- **Internal Bug Number:** [Internal tracking code according to the bug tracking table] -**Priority:** [Low | Medium | High] *(Please select one)* +**Priority:** [Low | Medium | High] _(Please select one)_ -**Bug Type:** [Functional | Design | Both] *(Please select one)* +**Bug Type:** [Functional | Design | Both] _(Please select one)_ **Affected Component:** [Specific UI component name, e.g., "Accept Quote button"] @@ -21,20 +20,21 @@ assignees: mtbitcr **Expected Behavior:** [Describe what should be happening. E.g., "Clicking the Quote button should redirect the user to the "View Quotes" screen"] -**Screenshots/Videos:** [Attach screenshots or videos clearly demonstrating the bug. Annotate the screenshots to highlight the affected area. If possible, include screen recordings to show the steps to reproduce the bug.] +**Screenshots/Videos:** [Attach screenshots or videos clearly demonstrating the bug. Annotate the screenshots to highlight the affected area. If possible, include screen recordings to show the steps to reproduce the bug.] **Environment:** -* **Device:** [Desktop | Mobile | Tablet] *(Please select one)* -* **OS:** [e.g., macOS Ventura, Windows 11, iOS 16, Android 13] -* **Browser:** [e.g., Chrome 114, Safari 16, Firefox 110, Edge 114] -* **Browser Version (if applicable):** [e.g., 114.0.5735.199] -* **Resolution (if applicable):** [e.g., 1920x1080] -* **App Version (if applicable):** [e.g., v1.2.3] +- **Device:** [Desktop | Mobile | Tablet] _(Please select one)_ +- **OS:** [e.g., macOS Ventura, Windows 11, iOS 16, Android 13] +- **Browser:** [e.g., Chrome 114, Safari 16, Firefox 110, Edge 114] +- **Browser Version (if applicable):** [e.g., 114.0.5735.199] +- **Resolution (if applicable):** [e.g., 1920x1080] +- **App Version (if applicable):** [e.g., v1.2.3] **Additional Context:** [Add any other relevant information, such as: -* Impact of the bug -* Workarounds (if any) -* Frequency of occurrence (e.g., always, sometimes, rarely) -* Related issues (if any) -* User feedback (if any)] + +- Impact of the bug +- Workarounds (if any) +- Frequency of occurrence (e.g., always, sometimes, rarely) +- Related issues (if any) +- User feedback (if any)] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4233e2c..f2bb2f4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' +title: "" labels: awaiting triage, enhancement, new feature -assignees: '' - +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 692c1be..8c9818d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: Validate (style, lint, test, build) runs-on: ubuntu-latest env: - NODE_VERSION: 22 + NODE_VERSION: 25 steps: - name: Checkout code diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 46eec18..9e8b4b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,35 +3,33 @@ name: Deploy wildcat-dashboard-ui (Cloudflare Pages) on: push: tags: - - 'v*' + - "v*" branches: - - 'master' + - "master" workflow_dispatch: inputs: - environment: - description: 'target' - required: true - default: 'production' - type: choice - options: - - dev - - staging - - production - + environment: + description: "target" + required: true + default: "production" + type: choice + options: + - dev + - staging + - production jobs: - -###################################################################### -# ENV: dev -# CF project: wildcat-dashboard -###################################################################### + ###################################################################### + # ENV: dev + # CF project: wildcat-dashboard + ###################################################################### deploy-dev: runs-on: ubuntu-latest permissions: contents: read deployments: write concurrency: - group: 'deploy-dev-${{ github.ref_name }}' + group: "deploy-dev-${{ github.ref_name }}" cancel-in-progress: true # SET ENVIRONMENT @@ -56,7 +54,7 @@ jobs: with: ref: ${{ github.ref }} fetch-depth: 0 - + # for dev: only allow dev branch - name: Validate correct branch on manual dispatch run: | @@ -71,7 +69,7 @@ jobs: uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + cache: "npm" - name: Install dependencies run: npm ci @@ -87,22 +85,21 @@ jobs: id: deploy-dev-dispatch uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=dev --commit-message="MANUAL DISPATCH ${{ github.sha }}" --commit-hash=${{ github.sha }} --commit-dirty=true - -###################################################################### -# ENV: staging -# CF project: wildcat-dashboard-staging -###################################################################### + ###################################################################### + # ENV: staging + # CF project: wildcat-dashboard-staging + ###################################################################### deploy-staging: runs-on: ubuntu-latest permissions: contents: read deployments: write concurrency: - group: 'deploy-staging-${{ github.ref_name }}' + group: "deploy-staging-${{ github.ref_name }}" cancel-in-progress: true # SET ENVIRONMENT @@ -144,7 +141,7 @@ jobs: uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + cache: "npm" - name: Install dependencies run: npm ci @@ -154,15 +151,15 @@ jobs: ############################################################################## # Customize deployment triggers - + # STAGING PREVIEW: on push, branch master - name: Deploy ${{ github.ref_name }} to STAGING PREVIEW of ${{ env.CLOUDFLARE_PROJECT }} project if: github.event_name == 'push' && github.ref == 'refs/heads/master' id: deploy-staging-preview-push-master uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=preview --commit-message="PREVIEW ${{ github.ref_name }}" --commit-hash=${{ github.sha }} --commit-dirty=true # STAGING PROD: on push, with tag @@ -171,8 +168,8 @@ jobs: id: deploy-staging-push-tag uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=master --commit-message="${{ github.ref_name }} (TAG PUSH)" --commit-hash=${{ github.sha }} --commit-dirty=true # STAGING PROD: on manual dispatch @@ -181,22 +178,21 @@ jobs: if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging' uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=master --commit-message="${{ github.ref_name }} (DISPATCH)" --commit-hash=${{ github.sha }} --commit-dirty=true - -###################################################################### -# ENV: production -# CF project: wildcat-dashboard-production -###################################################################### + ###################################################################### + # ENV: production + # CF project: wildcat-dashboard-production + ###################################################################### deploy-production: runs-on: ubuntu-latest permissions: contents: read deployments: write concurrency: - group: 'deploy-production-${{ github.ref_name }}' + group: "deploy-production-${{ github.ref_name }}" cancel-in-progress: true # SET ENVIRONMENT @@ -238,7 +234,7 @@ jobs: uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + cache: "npm" - name: Install dependencies run: npm ci @@ -246,18 +242,17 @@ jobs: - name: Build app run: npm run build - ############################################################################## # Customize deployment triggers - + # PRODUCTION PREVIEW: on push, with tag - name: Deploy ${{ github.ref_name }} to PRODUCTION PREVIEW of ${{ env.CLOUDFLARE_PROJECT }} project if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') id: deploy-production-preview-push-tag uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=preview --commit-message="PREVIEW ${{ github.ref_name }}" --commit-hash=${{ github.sha }} --commit-dirty=true # PRODUCTION PROD: manual dispatch, with tag @@ -266,6 +261,6 @@ jobs: if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production' uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=master --commit-message="${{ github.ref_name }}" --commit-hash=${{ github.sha }} --commit-dirty=true \ No newline at end of file + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PROJECT }} --branch=master --commit-message="${{ github.ref_name }}" --commit-hash=${{ github.sha }} --commit-dirty=true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0d43069..8ac131c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: true env: - IMAGE_NAME: "bcr-wdc-dashboard-ui" + IMAGE_NAME: "bcr-wdc-dashboard-ui" jobs: build: @@ -20,37 +20,37 @@ jobs: packages: write steps: - - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Login to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - id: meta - name: Metadata - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: | - ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=nightly - - - id: push - name: Build & push ${{ env.IMAGE_NAME }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} - cache-to: type=gha,mode=max,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Login to GHCR + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - id: meta + name: Metadata + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: | + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=nightly + + - id: push + name: Build & push ${{ env.IMAGE_NAME }} + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} + cache-to: type=gha,mode=max,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c99fd1..5b93e34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: true env: - IMAGE_NAME: "bcr-wdc-dashboard-ui" + IMAGE_NAME: "bcr-wdc-dashboard-ui" jobs: build: @@ -20,37 +20,37 @@ jobs: packages: write steps: - - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Login to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - id: meta - name: Metadata - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: | - ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - - - id: push - name: Build & push ${{ env.IMAGE_NAME }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} - cache-to: type=gha,mode=max,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} \ No newline at end of file + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Login to GHCR + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - id: meta + name: Metadata + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: | + ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + + - id: push + name: Build & push ${{ env.IMAGE_NAME }} + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} + cache-to: type=gha,mode=max,scope=${{ github.repository }}/${{ env.IMAGE_NAME }} diff --git a/.prettierrc b/.prettierrc index 37dedb1..e69d8aa 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,12 @@ { + "printWidth": 80, "tabWidth": 2, - "semi": false, + "useTabs": false, + "semi": true, "singleQuote": false, "trailingComma": "all", - "printWidth": 120, - "useTabs": false, - "endOfLine":"auto" + "arrowParens": "always", + "singleAttributePerLine": true, + "bracketSpacing": true, + "endOfLine": "lf" } diff --git a/Dockerfile b/Dockerfile index 78b8322..98b5b51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG NODE_ENV ARG VITE_MODE -FROM node:24.2.0-slim@sha256:b30c143a092c7dced8e17ad67a8783c03234d4844ee84c39090c9780491aaf89 AS builder +FROM node:25-slim AS builder ARG NODE_ENV ARG VITE_MODE ENV NODE_ENV=${NODE_ENV:-production} diff --git a/README.md b/README.md index 497b774..b96456a 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,39 @@ ## Development +Prerequisite: Node.js 25.x (see `.nvmrc`). + ### Install + ``` npm install ``` ### Run Development + ``` npm run dev ``` ### Run Docker + Copy `.env.example` to `.env` and adjust + ``` just build just run ``` ### shadcn + Add a shadcn component: + ```shell npx shadcn@canary add button ``` ## Configuration + `VITE_API_BASE_URL="http://localhost:4242"` needs to point to the Wildcat BFF Dashboard Envoy Service ``` diff --git a/components.json b/components.json index 640eed6..d72dbcd 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 3c7d97f..a45555c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,15 @@ services: - VITE_API_MOCKING_ENABLED=${VITE_API_MOCKING_ENABLED} - VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING=${VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING} healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"] + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:80", + ] interval: 30s timeout: 10s retries: 3 diff --git a/eslint.config.js b/eslint.config.js index c36899f..da2da9c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,54 +1,62 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import react from 'eslint-plugin-react' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import react from "eslint-plugin-react"; -import eslintConfigPrettier from 'eslint-config-prettier' +import eslintConfigPrettier from "eslint-config-prettier"; export default tseslint.config( { languageOptions: { parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, }, - }, { + }, + { ignores: [ - 'dist', - '.storybook', - 'public/mockServiceWorker.js', - 'src/generated', - ] - }, { + "dist", + ".storybook", + "coverage", + "opt", + "public/mockServiceWorker.js", + "src/generated", + ], + }, + { extends: [ - js.configs.recommended, + js.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked, ], - ignores: ['src/components/ui/*'], - files: ['**/*.{ts,tsx}'], + ignores: ["src/components/ui/*"], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, - settings: { react: { version: '19.0.0' } }, + settings: { react: { version: "19.0.0" } }, plugins: { react, - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-hooks/immutability": "off", + "react-hooks/purity": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/incompatible-library": "off", + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, }, eslintConfigPrettier, -) +); diff --git a/index.html b/index.html index c5f713b..a0efdd2 100644 --- a/index.html +++ b/index.html @@ -2,15 +2,35 @@ - - - - + + + + Wildcat Dashboard
- + diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index fb98733..7ef6a7f 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -1,19 +1,19 @@ -import { defineConfig } from '@hey-api/openapi-ts'; +import { defineConfig } from "@hey-api/openapi-ts"; export default defineConfig({ - input: 'opt/wildcat/openapi.json', + input: "opt/wildcat/openapi.json", output: { - format: 'prettier', - lint: 'eslint', - path: './src/generated/client', + format: "prettier", + lint: "eslint", + path: "./src/generated/client", }, plugins: [ - '@hey-api/client-fetch', - '@hey-api/sdk', + "@hey-api/client-fetch", + "@hey-api/sdk", /* '@hey-api/schemas', { enums: 'javascript', name: '@hey-api/typescript', },*/ - '@tanstack/react-query', + "@tanstack/react-query", ], -}) +}); diff --git a/opt/wildcat/openapi.json b/opt/wildcat/openapi.json index 7e6b3b7..cf21b1d 100644 --- a/opt/wildcat/openapi.json +++ b/opt/wildcat/openapi.json @@ -832,7 +832,12 @@ }, "BillAcceptanceStatus": { "type": "object", - "required": ["requested_to_accept", "accepted", "request_to_accept_timed_out", "rejected_to_accept"], + "required": [ + "requested_to_accept", + "accepted", + "request_to_accept_timed_out", + "rejected_to_accept" + ], "properties": { "time_of_request_to_accept": { "type": ["integer", "null"], @@ -1007,7 +1012,16 @@ }, "BillInfo": { "type": "object", - "required": ["id", "drawee", "drawer", "payee", "endorsees", "sum", "maturity_date", "file_urls"], + "required": [ + "id", + "drawee", + "drawer", + "payee", + "endorsees", + "sum", + "maturity_date", + "file_urls" + ], "properties": { "id": { "type": "string" @@ -1077,7 +1091,14 @@ }, "BillParticipants": { "type": "object", - "required": ["drawee", "drawer", "payee", "endorsements", "endorsements_count", "all_participant_node_ids"], + "required": [ + "drawee", + "drawer", + "payee", + "endorsements", + "endorsements_count", + "all_participant_node_ids" + ], "properties": { "drawee": { "$ref": "#/components/schemas/BillIdentParticipant" @@ -1119,7 +1140,12 @@ }, "BillPaymentStatus": { "type": "object", - "required": ["requested_to_pay", "paid", "request_to_pay_timed_out", "rejected_to_pay"], + "required": [ + "requested_to_pay", + "paid", + "request_to_pay_timed_out", + "rejected_to_pay" + ], "properties": { "time_of_request_to_pay": { "type": ["integer", "null"], @@ -1180,7 +1206,12 @@ }, "BillSellStatus": { "type": "object", - "required": ["sold", "offered_to_sell", "offer_to_sell_timed_out", "rejected_offer_to_sell"], + "required": [ + "sold", + "offered_to_sell", + "offer_to_sell_timed_out", + "rejected_offer_to_sell" + ], "properties": { "time_of_last_offer_to_sell": { "type": ["integer", "null"], @@ -1374,7 +1405,13 @@ "ClowderNodeInfo": { "type": "object", "description": "--------------------------- Clowder Node Information", - "required": ["change_address", "node_id", "uptime_timestamp", "version", "network"], + "required": [ + "change_address", + "node_id", + "uptime_timestamp", + "version", + "network" + ], "properties": { "change_address": { "type": "string" @@ -1639,7 +1676,14 @@ }, "Identity": { "type": "object", - "required": ["node_id", "name", "bitcoin_public_key", "npub", "postal_address", "nostr_relays"], + "required": [ + "node_id", + "name", + "bitcoin_public_key", + "npub", + "postal_address", + "nostr_relays" + ], "properties": { "node_id": { "type": "string" @@ -1705,7 +1749,13 @@ "oneOf": [ { "type": "object", - "required": ["id", "bill", "submitted", "suggested_expiration", "status"], + "required": [ + "id", + "bill", + "submitted", + "suggested_expiration", + "status" + ], "properties": { "id": { "type": "string", @@ -1751,7 +1801,14 @@ }, { "type": "object", - "required": ["id", "bill", "ttl", "keyset_id", "discounted", "status"], + "required": [ + "id", + "bill", + "ttl", + "keyset_id", + "discounted", + "status" + ], "properties": { "id": { "type": "string", @@ -1878,7 +1935,14 @@ }, { "type": "object", - "required": ["id", "bill", "keyset_id", "discounted", "fee", "status"], + "required": [ + "id", + "bill", + "keyset_id", + "discounted", + "fee", + "status" + ], "properties": { "id": { "type": "string", @@ -1909,7 +1973,16 @@ }, "InfoReplyDiscriminants": { "type": "string", - "enum": ["Pending", "Canceled", "Offered", "OfferExpired", "Denied", "Accepted", "Rejected", "MintingEnabled"] + "enum": [ + "Pending", + "Canceled", + "Offered", + "OfferExpired", + "Denied", + "Accepted", + "Rejected", + "MintingEnabled" + ] }, "KeySetInfo": { "type": "object", @@ -2095,7 +2168,13 @@ }, "Notification": { "type": "object", - "required": ["id", "notification_type", "description", "datetime", "active"], + "required": [ + "id", + "notification_type", + "description", + "datetime", + "active" + ], "properties": { "id": { "type": "string" diff --git a/package-lock.json b/package-lock.json index c234a99..c42ef73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,76 +8,85 @@ "name": "wildcat-dashboard-ui", "version": "0.1.1", "dependencies": { - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-hover-card": "^1.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query": "^5.67.3", - "big.js": "^6.2.2", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", + "big.js": "^7.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "keycloak-js": "^26.2.0", - "lucide-react": "^0.479.0", + "keycloak-js": "^26.2.3", + "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "qrcode": "^1.5.4", "qrcode.react": "^4.2.0", - "react": "^19.0.0", - "react-day-picker": "^9.6.4", - "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2", - "react-intl": "^8.0.11", - "react-router": "7.13.0", - "recharts": "^3.7.0", - "sonner": "^2.0.1", - "tailwind-merge": "^3.0.2", - "tailwindcss": "^4.1.18", + "react": "^19.2.4", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.4", + "react-hook-form": "^7.71.2", + "react-intl": "^8.1.3", + "react-router": "7.13.1", + "recharts": "^3.8.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.1", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2" }, "devDependencies": { - "@eslint/js": "^9.22.0", - "@eslint/plugin-kit": ">=0.3.4", - "@hey-api/openapi-ts": "^0.91.1", + "@eslint/js": "^9.39.4", + "@eslint/plugin-kit": ">=0.6.1", + "@hey-api/openapi-ts": "^0.94.0", "@testing-library/jest-dom": "^6.9.1", "@types/big.js": "^6.2.2", - "@types/date-fns": "^2.5.3", - "@types/node": "^22.13.10", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^5.1.3", + "@types/node": "^25.3.5", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "^4.0.18", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "jsdom": "^26.0.0", - "msw": "^2.7.3", - "prettier": "^3.5.3", - "typescript": "~5.8.2", - "typescript-eslint": "^8.54.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^28.1.0", + "msw": "^2.12.10", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", "vite": "7.3.1", "vitest": "^4.0.18" + }, + "engines": { + "node": ">=25 <26" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -86,25 +95,62 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" }, "node_modules/@babel/code-frame": { "version": "7.29.0", @@ -173,9 +219,9 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { @@ -418,10 +464,23 @@ "node": ">=18" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -435,13 +494,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, "funding": [ { @@ -455,17 +514,17 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -479,21 +538,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -507,16 +566,33 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", + "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -530,7 +606,7 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@date-fns/tz": { @@ -540,9 +616,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -556,9 +632,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -572,9 +648,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -588,9 +664,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -604,9 +680,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -620,9 +696,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -636,9 +712,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -652,9 +728,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -668,9 +744,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -684,9 +760,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -700,9 +776,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -716,9 +792,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -732,9 +808,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -748,9 +824,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -764,9 +840,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -780,9 +856,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -796,9 +872,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -812,9 +888,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -828,9 +904,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -844,9 +920,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -860,9 +936,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -876,9 +952,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -892,9 +968,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -908,9 +984,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -924,9 +1000,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -940,9 +1016,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -998,15 +1074,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1025,7 +1101,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -1038,21 +1114,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1062,23 +1151,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1092,17 +1164,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1123,45 +1188,63 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.5" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -1169,9 +1252,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@formatjs/ecma402-abstract": { @@ -1248,9 +1331,9 @@ } }, "node_modules/@hey-api/codegen-core": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.6.1.tgz", - "integrity": "sha512-khTIpxhKEAqmRmeLUnAFJQs4Sbg9RPokovJk9rRcC8B5MWH1j3/BRSqfpAIiJUBDU1+nbVg2RVCV+eQ174cdvw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.1.tgz", + "integrity": "sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -1270,38 +1353,37 @@ } }, "node_modules/@hey-api/json-schema-ref-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.3.tgz", - "integrity": "sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.3.1.tgz", + "integrity": "sha512-7atnpUkT8TyUPHYPLk91j/GyaqMuwTEHanLOe50Dlx0EEvNuQqFD52Yjg8x4KU0UFL1mWlyhE+sUE/wAtQ1N2A==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.1", - "lodash": "^4.17.21" + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "js-yaml": "4.1.1" }, "engines": { - "node": ">= 16" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/hey-api" } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.91.1.tgz", - "integrity": "sha512-d16WR35UtthK/ihAIwJaKxrj/zvb5LbYwtVJCyZFFMin2qzDU8Y3Lpk78ensAykrLoaDLzpd0iIyt9JCP5Qmww==", + "version": "0.94.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.0.tgz", + "integrity": "sha512-dbg3GG+v7sg9/Ahb7yFzwzQIJwm151JAtsnh9KtFyqiN0rGkMGA3/VqogEUq1kJB9XWrlMQwigwzhiEQ33VCSg==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.6.1", - "@hey-api/json-schema-ref-parser": "1.2.3", - "@hey-api/shared": "0.1.1", + "@hey-api/codegen-core": "0.7.1", + "@hey-api/json-schema-ref-parser": "1.3.1", + "@hey-api/shared": "0.2.2", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", - "commander": "14.0.2" + "commander": "14.0.3" }, "bin": { "openapi-ts": "bin/run.js" @@ -1317,14 +1399,14 @@ } }, "node_modules/@hey-api/shared": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.1.1.tgz", - "integrity": "sha512-/irgNGXw9TL5aKB3S7jCLgh07vgDFkYjSjz7vEWO9xEe6MUhx76zSFzkPspk2UrLghYayvmaKPf1ky4XjNI9ZQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.2.tgz", + "integrity": "sha512-vMqCS+j7F9xpWoXC7TBbqZkaelwrdeuSB+s/3elu54V5iq++S59xhkSq5rOgDIpI1trpE59zZQa6dpyUxItOgw==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.6.1", - "@hey-api/json-schema-ref-parser": "1.2.3", + "@hey-api/codegen-core": "0.7.1", + "@hey-api/json-schema-ref-parser": "1.3.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", @@ -1463,66 +1545,6 @@ } } }, - "node_modules/@inquirer/core/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@inquirer/core/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@inquirer/figures": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", @@ -1604,9 +1626,9 @@ "license": "MIT" }, "node_modules/@mswjs/interceptors": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", "dev": true, "license": "MIT", "dependencies": { @@ -3892,9 +3914,9 @@ } }, "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", - "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", "license": "MIT", "funding": { "type": "opencollective", @@ -3902,16 +3924,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -3922,9 +3944,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -3935,9 +3957,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -3948,9 +3970,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -3961,9 +3983,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -3974,9 +3996,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -3987,9 +4009,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -4000,9 +4022,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -4013,9 +4035,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -4026,9 +4048,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -4039,9 +4061,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -4052,9 +4074,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -4065,9 +4087,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -4078,9 +4100,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -4091,9 +4113,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -4104,9 +4126,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -4117,9 +4139,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -4130,9 +4152,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -4143,9 +4165,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -4156,9 +4178,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -4169,9 +4191,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -4182,9 +4204,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -4195,9 +4217,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -4208,9 +4230,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -4221,9 +4243,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4245,48 +4267,57 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -4296,13 +4327,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -4312,13 +4343,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -4328,13 +4359,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -4344,13 +4375,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -4360,13 +4391,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], @@ -4376,13 +4407,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], @@ -4392,13 +4423,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], @@ -4408,13 +4439,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], @@ -4424,13 +4455,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4445,21 +4476,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -4469,13 +4500,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -4485,18 +4516,18 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -4513,9 +4544,9 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", - "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.90.20" @@ -4674,13 +4705,6 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, - "node_modules/@types/date-fns": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz", - "integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4714,13 +4738,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/qrcode": { @@ -4734,9 +4758,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", - "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4766,17 +4790,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4789,7 +4813,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4805,16 +4829,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -4830,14 +4854,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -4852,14 +4876,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4870,9 +4894,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -4887,15 +4911,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -4912,9 +4936,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -4926,18 +4950,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -4953,43 +4977,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5004,13 +5041,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5022,9 +5059,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5035,16 +5072,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", - "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.2", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -5198,9 +5235,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -5230,6 +5267,23 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -5240,6 +5294,15 @@ "node": ">=6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5433,9 +5496,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { @@ -5485,19 +5548,32 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" } }, "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-7.0.1.tgz", + "integrity": "sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==", "license": "MIT", "engines": { "node": "*" @@ -5507,10 +5583,21 @@ "url": "https://opencollective.com/bigjs" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -5656,9 +5743,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -5766,51 +5853,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5867,9 +5909,9 @@ } }, "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { @@ -5884,9 +5926,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "dev": true, "license": "MIT" }, @@ -5907,6 +5949,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5922,6 +5977,20 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5930,17 +5999,29 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/csstype": { @@ -6071,17 +6152,17 @@ } }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/data-view-buffer": { @@ -6335,9 +6416,9 @@ "license": "MIT" }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6363,20 +6444,26 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.283", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", - "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -6580,9 +6667,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", "workspaces": [ "docs", @@ -6590,9 +6677,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -6602,32 +6689,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -6654,25 +6741,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -6691,7 +6778,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -6763,26 +6850,33 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^9 || ^10" } }, "node_modules/eslint-plugin-react/node_modules/semver": { @@ -6825,29 +6919,32 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@types/json-schema": "^7.0.15" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { + "node_modules/eslint/node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, "node_modules/espree": { "version": "10.4.0", @@ -7029,9 +7126,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.0.tgz", + "integrity": "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==", "dev": true, "license": "ISC" }, @@ -7233,9 +7330,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -7282,9 +7379,9 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", "dev": true, "license": "MIT", "engines": { @@ -7392,6 +7489,23 @@ "dev": true, "license": "MIT" }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -7408,16 +7522,16 @@ "license": "MIT" }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-escaper": { @@ -8002,9 +8116,9 @@ } }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -8118,35 +8232,36 @@ } }, "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", + "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -8177,6 +8292,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -8214,9 +8336,9 @@ } }, "node_modules/keycloak-js": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.2.tgz", - "integrity": "sha512-ug7pNZ1xNkd7PPkerOJCEU2VnUhS7CYStDOCFJgqCNQ64h53ppxaKrh4iXH0xM8hFu5b1W6e6lsyYWqBMvaQFg==", + "version": "26.2.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz", + "integrity": "sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==", "license": "Apache-2.0", "workspaces": [ "test" @@ -8247,9 +8369,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -8262,23 +8384,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -8296,9 +8418,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -8316,9 +8438,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -8336,9 +8458,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -8356,9 +8478,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -8376,9 +8498,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -8396,9 +8518,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -8416,9 +8538,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -8436,9 +8558,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -8456,9 +8578,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -8476,9 +8598,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -8511,13 +8633,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8549,9 +8664,9 @@ } }, "node_modules/lucide-react": { - "version": "0.479.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.479.0.tgz", - "integrity": "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==", + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8567,14 +8682,14 @@ } }, "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, @@ -8604,6 +8719,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8615,9 +8737,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8627,17 +8749,6 @@ "node": "*" } }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8646,15 +8757,15 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.7.tgz", - "integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==", + "version": "2.12.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", + "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.40.0", + "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", @@ -8664,7 +8775,7 @@ "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", - "rettime": "^0.7.0", + "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", @@ -8690,53 +8801,6 @@ } } }, - "node_modules/msw/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/msw/node_modules/tldts": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.21.tgz", - "integrity": "sha512-Plu6V8fF/XU6d2k8jPtlQf5F4Xx2hAin4r2C2ca7wR8NK5MbRTo9huLUWRe28f3Uk8bYZfg74tit/dSjc18xnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.21" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/msw/node_modules/tldts-core": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.21.tgz", - "integrity": "sha512-oVOMdHvgjqyzUZH1rOESgJP1uNe2bVrfK0jUHHmiM2rpEiRbf3j4BrsIc6JigJRbHGanQwuZv/R+LTcHsw+bLA==", - "dev": true, - "license": "MIT" - }, - "node_modules/msw/node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -8782,6 +8846,35 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -8790,23 +8883,16 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", - "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8822,9 +8908,9 @@ } }, "node_modules/nypm/node_modules/citty": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", - "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", "dev": true, "license": "MIT" }, @@ -9073,9 +9159,9 @@ } }, "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "dev": true, "license": "MIT", "dependencies": { @@ -9182,9 +9268,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -9303,15 +9389,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/qrcode/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -9323,12 +9400,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/qrcode/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/qrcode/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -9381,46 +9452,6 @@ "node": ">=8" } }, - "node_modules/qrcode/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -9483,14 +9514,15 @@ } }, "node_modules/react-day-picker": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz", - "integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", - "date-fns-jalali": "^4.1.0-0" + "date-fns-jalali": "4.1.0-0" }, "engines": { "node": ">=18" @@ -9516,9 +9548,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", - "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -9644,9 +9676,9 @@ } }, "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -9665,19 +9697,6 @@ } } }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -9715,15 +9734,15 @@ } }, "node_modules/recharts": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", "license": "MIT", "workspaces": [ "www" ], "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", @@ -9826,6 +9845,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -9839,19 +9868,25 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9867,16 +9902,16 @@ } }, "node_modules/rettime": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", "dev": true, "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -9889,41 +9924,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9992,13 +10020,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -10275,6 +10296,20 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -10373,6 +10408,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -10446,9 +10493,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -10456,9 +10503,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "license": "MIT" }, "node_modules/tailwindcss-animate": { @@ -10533,49 +10580,49 @@ } }, "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.86" + "tldts-core": "^7.0.25" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", "dev": true, "license": "MIT" }, "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/ts-api-utils": { @@ -10611,9 +10658,9 @@ } }, "node_modules/type-fest": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.3.tgz", - "integrity": "sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA==", + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -10705,9 +10752,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -10719,16 +10766,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10761,10 +10808,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, @@ -11072,64 +11129,38 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=20" } }, "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { @@ -11270,26 +11301,18 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "engines": { + "node": ">=8" } }, "node_modules/wsl-utils": { @@ -11372,51 +11395,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -11442,6 +11420,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index 87ac384..7b89f8f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "private": true, "version": "0.1.1", "type": "module", + "engines": { + "node": ">=25 <26" + }, "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -19,72 +22,71 @@ "crowdin:status": "crowdin status" }, "dependencies": { - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-hover-card": "^1.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query": "^5.67.3", - "big.js": "^6.2.2", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", + "big.js": "^7.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "keycloak-js": "^26.2.0", - "lucide-react": "^0.479.0", + "keycloak-js": "^26.2.3", + "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "qrcode": "^1.5.4", "qrcode.react": "^4.2.0", - "react": "^19.0.0", - "react-day-picker": "^9.6.4", - "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2", - "react-intl": "^8.0.11", - "react-router": "7.13.0", - "recharts": "^3.7.0", - "sonner": "^2.0.1", - "tailwind-merge": "^3.0.2", - "tailwindcss": "^4.1.18", + "react": "^19.2.4", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.4", + "react-hook-form": "^7.71.2", + "react-intl": "^8.1.3", + "react-router": "7.13.1", + "recharts": "^3.8.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.1", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2" }, "devDependencies": { - "@eslint/js": "^9.22.0", - "@eslint/plugin-kit": ">=0.3.4", - "@hey-api/openapi-ts": "^0.91.1", + "@eslint/js": "^9.39.4", + "@eslint/plugin-kit": ">=0.6.1", + "@hey-api/openapi-ts": "^0.94.0", "@testing-library/jest-dom": "^6.9.1", "@types/big.js": "^6.2.2", - "@types/date-fns": "^2.5.3", - "@types/node": "^22.13.10", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^5.1.3", + "@types/node": "^25.3.5", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "^4.0.18", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "jsdom": "^26.0.0", - "msw": "^2.7.3", - "prettier": "^3.5.3", - "typescript": "~5.8.2", - "typescript-eslint": "^8.54.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^28.1.0", + "msw": "^2.12.10", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", "vite": "7.3.1", "vitest": "^4.0.18" }, diff --git a/public/config.js b/public/config.js index 68a8529..913de41 100644 --- a/public/config.js +++ b/public/config.js @@ -1 +1 @@ -window.__ENV__ = window.__ENV__ || {} +window.__ENV__ = window.__ENV__ || {}; diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 461e260..daa58d0 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.7' +const PACKAGE_VERSION = '2.12.10' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/src/__tests__/sample.test.ts b/src/__tests__/sample.test.ts index a5c1202..7865688 100644 --- a/src/__tests__/sample.test.ts +++ b/src/__tests__/sample.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect } from "vitest" +import { describe, it, expect } from "vitest"; describe("Sample Test", () => { it("should pass", () => { - expect(true).toBe(true) - }) -}) + expect(true).toBe(true); + }); +}); diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 131af4b..efd2e89 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -1,12 +1,24 @@ -import { Bitcoin, Home, Inbox, Key } from "lucide-react" -import { useContext } from "react" -import { Sidebar, SidebarContent, SidebarFooter, SidebarRail, SidebarSeparator } from "@/components/ui/sidebar" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Bitcoin, Home, Inbox, Key } from "lucide-react"; +import { useContext } from "react"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarRail, + SidebarSeparator, +} from "@/components/ui/sidebar"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; // import { NavUser } from "./nav/NavUser" -import { NavMain } from "./nav/NavMain" +import { NavMain } from "./nav/NavMain"; // import { useKeycloak } from "../lib/keycloak-user" -import { LanguageContext } from "@/context/language/LanguageContext" -import { useIntl } from "react-intl" +import { LanguageContext } from "@/context/language/LanguageContext"; +import { useIntl } from "react-intl"; const data = { navMain: [ @@ -72,12 +84,12 @@ const data = { icon: Key, }, ], -} +}; function LanguageSelector() { - const intl = useIntl() - const { locale, setLocale, availableLocales } = useContext(LanguageContext) - const locales = availableLocales() + const intl = useIntl(); + const { locale, setLocale, availableLocales } = useContext(LanguageContext); + const locales = availableLocales(); return (
@@ -87,7 +99,10 @@ function LanguageSelector() { defaultMessage: "Language", })} - {locales.map((loc) => ( - + {intl.formatMessage({ id: `locale.${loc}`, defaultMessage: loc, @@ -108,7 +126,7 @@ function LanguageSelector() {
- ) + ); } export function AppSidebar() { @@ -128,5 +146,5 @@ export function AppSidebar() { */} - ) + ); } diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 2a89c3b..8fbd5f3 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from "react" +import { PropsWithChildren } from "react"; import { Breadcrumb, BreadcrumbItem, @@ -6,12 +6,15 @@ import { BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import { Link } from "react-router" -import { useIntl } from "react-intl" +} from "@/components/ui/breadcrumb"; +import { Link } from "react-router"; +import { useIntl } from "react-intl"; -export function Breadcrumbs({ parents, children }: PropsWithChildren<{ parents?: React.ReactNode[] }>) { - const intl = useIntl() +export function Breadcrumbs({ + parents, + children, +}: PropsWithChildren<{ parents?: React.ReactNode[] }>) { + const intl = useIntl(); return ( @@ -30,7 +33,10 @@ export function Breadcrumbs({ parents, children }: PropsWithChildren<{ parents?: {parents && ( <> {parents.map((it, index) => ( -
+
<>{it} @@ -44,5 +50,5 @@ export function Breadcrumbs({ parents, children }: PropsWithChildren<{ parents?: - ) + ); } diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index bf9944f..827fcd0 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -1,16 +1,16 @@ -import { Button } from "@/components/ui/button" -import { Copy, Check } from "lucide-react" -import { toast } from "sonner" -import { useState } from "react" -import { useIntl } from "react-intl" +import { Button } from "@/components/ui/button"; +import { Copy, Check } from "lucide-react"; +import { toast } from "sonner"; +import { useState } from "react"; +import { useIntl } from "react-intl"; interface CopyButtonProps { - value: string - label?: string - variant?: "ghost" | "outline" | "default" - size?: "sm" | "default" | "lg" | "icon" - className?: string - showCheckmark?: boolean + value: string; + label?: string; + variant?: "ghost" | "outline" | "default"; + size?: "sm" | "default" | "lg" | "icon"; + className?: string; + showCheckmark?: boolean; } export function CopyButton({ @@ -21,42 +21,57 @@ export function CopyButton({ className = "h-6 px-2", showCheckmark = false, }: CopyButtonProps) { - const intl = useIntl() - const [copied, setCopied] = useState(false) + const intl = useIntl(); + const [copied, setCopied] = useState(false); const fallbackLabel = intl.formatMessage({ id: "copyButton.defaultLabel", defaultMessage: "Value", - }) - const labelValue = label ?? fallbackLabel + }); + const labelValue = label ?? fallbackLabel; const handleCopy = () => { navigator.clipboard .writeText(value) .then(() => { if (showCheckmark) { - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setCopied(true); + setTimeout(() => setCopied(false), 2000); } toast.success( intl.formatMessage( - { id: "copyButton.copied", defaultMessage: "{label} copied to clipboard" }, + { + id: "copyButton.copied", + defaultMessage: "{label} copied to clipboard", + }, { label: labelValue }, ), - ) + ); }) .catch(() => { toast.error( intl.formatMessage( - { id: "copyButton.failed", defaultMessage: "Failed to copy {label}" }, + { + id: "copyButton.failed", + defaultMessage: "Failed to copy {label}", + }, { label: labelValue }, ), - ) - }) - } + ); + }); + }; return ( - - ) + ); } diff --git a/src/components/DatePicker/calendar.tsx b/src/components/DatePicker/calendar.tsx index 49b4fb2..16a2063 100644 --- a/src/components/DatePicker/calendar.tsx +++ b/src/components/DatePicker/calendar.tsx @@ -1,34 +1,43 @@ -import React, { useCallback, useEffect, useState } from "react" -import { DateRange, DayPicker, DayPickerProps, OnSelectHandler } from "react-day-picker" -import { isSameDay } from "date-fns" -import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react" -import { useIntl } from "react-intl" +import React, { useCallback, useEffect, useState } from "react"; +import { + DateRange, + DayPicker, + DayPickerProps, + OnSelectHandler, +} from "react-day-picker"; +import { isSameDay } from "date-fns"; +import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; +import { useIntl } from "react-intl"; -import { cn } from "@/lib/utils" -import { YearPicker } from "./yearPicker" -import { MonthPicker } from "./monthPicker" -import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters" +import { cn } from "@/lib/utils"; +import { YearPicker } from "./yearPicker"; +import { MonthPicker } from "./monthPicker"; +import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters"; -export type CalendarProps = Omit & { - mode: "single" | "range" - onSelect?: OnSelectHandler - selected: DateRange - onCaptionLabelClicked?: () => void - disableFutureNavigation?: boolean - rangeFocus?: "from" | "to" - className?: string - ISOWeek?: boolean - showOutsideDays?: boolean - month?: Date - minDate?: Date - initialFocus?: boolean - modifiers?: Record boolean> - modifiersClassNames?: Record -} +export type CalendarProps = Omit< + DayPickerProps, + "mode" | "onSelect" | "selected" +> & { + mode: "single" | "range"; + onSelect?: OnSelectHandler; + selected: DateRange; + onCaptionLabelClicked?: () => void; + disableFutureNavigation?: boolean; + rangeFocus?: "from" | "to"; + className?: string; + ISOWeek?: boolean; + showOutsideDays?: boolean; + month?: Date; + minDate?: Date; + initialFocus?: boolean; + modifiers?: Record boolean>; + modifiersClassNames?: Record; +}; const classNames = { root: "w-full", - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 w-full", + months: + "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 w-full", month: "space-y-4 w-full", month_caption: "flex justify-center relative items-center", // Hide default DayPicker caption label; we render our own header. @@ -47,28 +56,43 @@ const classNames = { range_start: "day-range-start", selected: "bg-elevation-200 hover:bg-elevation-200 border border-divider-100", today: "bg-accent text-accent-foreground", - outside: "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", + outside: + "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", disabled: "text-muted-foreground opacity-50", range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", hidden: "invisible", -} +}; function getNextDate(current: Date, offset: number): Date { - const year = current.getUTCFullYear() - const month = current.getUTCMonth() - const day = current.getUTCDate() + const year = current.getUTCFullYear(); + const month = current.getUTCMonth(); + const day = current.getUTCDate(); - const daysInCurrentMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate() - const isLastDay = day === daysInCurrentMonth + const daysInCurrentMonth = new Date( + Date.UTC(year, month + 1, 0), + ).getUTCDate(); + const isLastDay = day === daysInCurrentMonth; - const targetMonthDate = new Date(Date.UTC(year, month + offset, 1)) + const targetMonthDate = new Date(Date.UTC(year, month + offset, 1)); const daysInTargetMonth = new Date( - Date.UTC(targetMonthDate.getUTCFullYear(), targetMonthDate.getUTCMonth() + 1, 0), - ).getUTCDate() + Date.UTC( + targetMonthDate.getUTCFullYear(), + targetMonthDate.getUTCMonth() + 1, + 0, + ), + ).getUTCDate(); - const newDay = isLastDay ? daysInTargetMonth : Math.min(day, daysInTargetMonth) + const newDay = isLastDay + ? daysInTargetMonth + : Math.min(day, daysInTargetMonth); - return new Date(Date.UTC(targetMonthDate.getUTCFullYear(), targetMonthDate.getUTCMonth(), newDay)) + return new Date( + Date.UTC( + targetMonthDate.getUTCFullYear(), + targetMonthDate.getUTCMonth(), + newDay, + ), + ); } function Calendar({ @@ -88,91 +112,116 @@ function Calendar({ minDate, ...restProps }: CalendarProps) { - const intl = useIntl() - const [selectedDate, setSelectedDate] = useState(selected.from) - const [month, setMonth] = useState(selected.from ?? monthProp ?? new Date()) - const [showYearPicker, setShowYearPicker] = useState(false) - const [showMonthPicker, setShowMonthPicker] = useState(false) - const { formatDay2Digit, formatMonthShort, formatYearNumeric } = useUtcDateFormatters(intl.locale) + const intl = useIntl(); + const [selectedDate, setSelectedDate] = useState( + selected.from, + ); + const [month, setMonth] = useState( + selected.from ?? monthProp ?? new Date(), + ); + const [showYearPicker, setShowYearPicker] = useState(false); + const [showMonthPicker, setShowMonthPicker] = useState(false); + const { formatDay2Digit, formatMonthShort, formatYearNumeric } = + useUtcDateFormatters(intl.locale); useEffect(() => { if (mode === "single") { if (selected.from) { - setSelectedDate(selected.from) - setMonth(selected.from) + setSelectedDate(selected.from); + setMonth(selected.from); } else { - setSelectedDate(undefined) - setMonth(monthProp ?? new Date()) + setSelectedDate(undefined); + setMonth(monthProp ?? new Date()); } } else { if (!selected.from && !selected.to) { - setMonth(monthProp ?? new Date()) - setSelectedDate(undefined) + setMonth(monthProp ?? new Date()); + setSelectedDate(undefined); } } - }, [selected, mode, monthProp]) + }, [selected, mode, monthProp]); - const handleOnSelectRange: OnSelectHandler = (range, selectedDay, modifiers, e) => { + const handleOnSelectRange: OnSelectHandler = ( + range, + selectedDay, + modifiers, + e, + ) => { if (mode === "single") { - setSelectedDate(selectedDay) + setSelectedDate(selectedDay); } if (onSelect) { - onSelect(range, selectedDay, modifiers, e) + onSelect(range, selectedDay, modifiers, e); } - } + }; - const handleOnSelectSingle: OnSelectHandler = (day, selectedDay, modifiers, e) => { - handleOnSelectRange(day ? { from: day } : undefined, selectedDay, modifiers, e) - } + const handleOnSelectSingle: OnSelectHandler = ( + day, + selectedDay, + modifiers, + e, + ) => { + handleOnSelectRange( + day ? { from: day } : undefined, + selectedDay, + modifiers, + e, + ); + }; const goToOffsetMonth = useCallback( (offset: number) => { - setMonth(getNextDate(month, offset)) + setMonth(getNextDate(month, offset)); }, [month], - ) + ); - let touchStartX: number | null = null + let touchStartX: number | null = null; const handleTouchStart = (e: React.TouchEvent) => { - touchStartX = e.touches[0].clientX - } + touchStartX = e.touches[0].clientX; + }; const handleTouchEnd = (e: React.TouchEvent) => { if (touchStartX === null) { - return + return; } - const diffX = e.changedTouches[0].clientX - touchStartX + const diffX = e.changedTouches[0].clientX - touchStartX; if (diffX > 50) { - goToOffsetMonth(-1) + goToOffsetMonth(-1); } else if (diffX < -50) { if (canGoForward) { - goToOffsetMonth(1) + goToOffsetMonth(1); } } - touchStartX = null - } + touchStartX = null; + }; const isAtOrBeyondCurrentMonth = (d: Date) => { - const today = new Date() - const todayYear = today.getUTCFullYear() - const todayMonth = today.getUTCMonth() - const dateYear = d.getUTCFullYear() - const dateMonth = d.getUTCMonth() - return dateYear > todayYear || (dateYear === todayYear && dateMonth >= todayMonth) - } + const today = new Date(); + const todayYear = today.getUTCFullYear(); + const todayMonth = today.getUTCMonth(); + const dateYear = d.getUTCFullYear(); + const dateMonth = d.getUTCMonth(); + return ( + dateYear > todayYear || + (dateYear === todayYear && dateMonth >= todayMonth) + ); + }; - const canGoForward = disableFutureNavigation ? !isAtOrBeyondCurrentMonth(month) : true + const canGoForward = disableFutureNavigation + ? !isAtOrBeyondCurrentMonth(month) + : true; const NavigationHeader = () => (
@@ -191,10 +242,10 @@ function Calendar({ { - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} > @@ -203,11 +254,11 @@ function Calendar({ { - e.stopPropagation() - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + e.stopPropagation(); + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} > @@ -218,21 +269,21 @@ function Calendar({ { - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} > {formatMonthShort(month)} { - e.stopPropagation() - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + e.stopPropagation(); + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} > @@ -244,21 +295,21 @@ function Calendar({ { - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} > {formatMonthShort(month)} { - e.stopPropagation() - setShowYearPicker(!showYearPicker) - setShowMonthPicker(false) + e.stopPropagation(); + setShowYearPicker(!showYearPicker); + setShowMonthPicker(false); if (onCaptionLabelClicked) { - onCaptionLabelClicked() + onCaptionLabelClicked(); } }} className="hover:underline" @@ -267,12 +318,17 @@ function Calendar({ )} - {onCaptionLabelClicked && } + {onCaptionLabelClicked && ( + + )}
- ) + ); const pickerProps = { ...restProps, @@ -302,21 +358,25 @@ function Calendar({ ...(initialFocus && { initialFocus }), ...(modifiers && { modifiers }), ...(modifiersClassNames && { modifiersClassNames }), - } + }; return ( -
+
{!showYearPicker && !showMonthPicker && } {showYearPicker ? ( { - setMonth(newDate) - setShowYearPicker(false) - setShowMonthPicker(true) + setMonth(newDate); + setShowYearPicker(false); + setShowMonthPicker(true); }} onCaptionLabelClicked={() => { - setShowYearPicker(false) + setShowYearPicker(false); }} disableFutureNavigation={disableFutureNavigation} minDate={minDate} @@ -325,35 +385,46 @@ function Calendar({ { - setMonth(newDate) - setShowMonthPicker(false) + setMonth(newDate); + setShowMonthPicker(false); }} onCaptionLabelClicked={() => { - setShowMonthPicker(false) + setShowMonthPicker(false); }} disableFutureNavigation={disableFutureNavigation} minDate={minDate} /> ) : mode === "single" ? ( - + ) : ( { - const currentRange = selected - let nextRange: DateRange + const currentRange = selected; + let nextRange: DateRange; if (rangeFocus === "from") { - nextRange = { from: selectedDay, to: currentRange.to } + nextRange = { from: selectedDay, to: currentRange.to }; } else { - const from = currentRange.from ?? selectedDay - nextRange = selectedDay < from ? { from: selectedDay, to: from } : { from, to: selectedDay } + const from = currentRange.from ?? selectedDay; + nextRange = + selectedDay < from + ? { from: selectedDay, to: from } + : { from, to: selectedDay }; } - if (month.getMonth() !== selectedDay.getMonth() || month.getFullYear() !== selectedDay.getFullYear()) { - setMonth(selectedDay) + if ( + month.getMonth() !== selectedDay.getMonth() || + month.getFullYear() !== selectedDay.getFullYear() + ) { + setMonth(selectedDay); } - handleOnSelectRange(nextRange, selectedDay, modifiers, e) + handleOnSelectRange(nextRange, selectedDay, modifiers, e); }} modifiers={{ ...(selected.from && { @@ -364,7 +435,8 @@ function Calendar({ }), ...(selected.from && selected.to && { - range_middle: (d: Date) => d > selected.from! && d < selected.to!, + range_middle: (d: Date) => + d > selected.from! && d < selected.to!, }), ...(selected.from && rangeFocus === "from" && { @@ -387,9 +459,9 @@ function Calendar({ /> )}
- ) + ); } -Calendar.displayName = "Calendar" +Calendar.displayName = "Calendar"; -export { Calendar } +export { Calendar }; diff --git a/src/components/DatePicker/dataRangeDropdown.tsx b/src/components/DatePicker/dataRangeDropdown.tsx index 2771fbd..118fc71 100644 --- a/src/components/DatePicker/dataRangeDropdown.tsx +++ b/src/components/DatePicker/dataRangeDropdown.tsx @@ -1,7 +1,7 @@ -import { useIntl } from "react-intl" -import { CircleX } from "lucide-react" +import { useIntl } from "react-intl"; +import { CircleX } from "lucide-react"; -import { Button } from "@/components/ui/button" +import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -11,45 +11,52 @@ import { DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from "./dropdownMenu" +} from "./dropdownMenu"; interface DateRangeDropdownProps { - value?: number - onRangeChange: (range: number) => void - onClear?: () => void + value?: number; + onRangeChange: (range: number) => void; + onClear?: () => void; } -export function DateRangeDropdown({ value, onRangeChange, onClear }: DateRangeDropdownProps) { - const intl = useIntl() +export function DateRangeDropdown({ + value, + onRangeChange, + onClear, +}: DateRangeDropdownProps) { + const intl = useIntl(); const handleRangeChanged = (value: string) => { - const range = Number(value) - onRangeChange(range) - } + const range = Number(value); + onRangeChange(range); + }; const handleDisplayRange = (value: number | undefined): string => { switch (value) { case 30: case 60: case 90: - return intl.formatMessage({ id: "displayRange.days", defaultMessage: "{value} Days" }, { value }) + return intl.formatMessage( + { id: "displayRange.days", defaultMessage: "{value} Days" }, + { value }, + ); case 180: return intl.formatMessage({ id: "displayRange.sixMonths", defaultMessage: "6 Months", - }) + }); case 365: return intl.formatMessage({ id: "displayRange.oneYear", defaultMessage: "1 Year", - }) + }); default: return intl.formatMessage({ id: "displayRange.selectRange", defaultMessage: "Select range", - }) + }); } - } + }; return ( @@ -69,25 +76,28 @@ export function DateRangeDropdown({ value, onRangeChange, onClear }: DateRangeDr })} aria-pressed="false" onPointerDown={(e) => { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); }} onClick={(e) => { - e.stopPropagation() - onClear?.() + e.stopPropagation(); + onClear?.(); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { - e.preventDefault() - e.stopPropagation() - onClear?.() + e.preventDefault(); + e.stopPropagation(); + onClear?.(); } else if (e.key === "Escape") { - e.stopPropagation() + e.stopPropagation(); } }} className="p-1 rounded-sm hover:bg-elevation-250 focus:outline-hidden focus:ring-2 focus:ring-brand-200 focus:ring-offset-1 cursor-pointer transition-colors" > - +
)} @@ -100,7 +110,10 @@ export function DateRangeDropdown({ value, onRangeChange, onClear }: DateRangeDr })} - + {intl.formatMessage({ id: "dropdown.option.30days", @@ -134,5 +147,5 @@ export function DateRangeDropdown({ value, onRangeChange, onClear }: DateRangeDr - ) + ); } diff --git a/src/components/DatePicker/datePicker.tsx b/src/components/DatePicker/datePicker.tsx index ceefe1b..d664ab9 100644 --- a/src/components/DatePicker/datePicker.tsx +++ b/src/components/DatePicker/datePicker.tsx @@ -1,36 +1,36 @@ -import React, { useContext, useEffect, useMemo, useState } from "react" -import { DateRange, dateMatchModifiers, type Matcher } from "react-day-picker" -import { FormattedMessage } from "react-intl" -import { addDays, isSameDay } from "date-fns" -import { ArrowRight, CalendarIcon } from "lucide-react" - -import { Calendar } from "@/components/DatePicker/calendar" -import { Button } from "@/components/ui/button" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { LanguageContext } from "@/context/language/LanguageContext" -import { cn } from "@/lib/utils" -import { daysBetween, formatDateLong, formatDateShort } from "@/utils/dates" -import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters" - -import { DateRangeDropdown } from "./dataRangeDropdown" -import { MonthPicker } from "./monthPicker" -import { YearPicker } from "./yearPicker" +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { DateRange, dateMatchModifiers, type Matcher } from "react-day-picker"; +import { FormattedMessage } from "react-intl"; +import { addDays, isSameDay } from "date-fns"; +import { ArrowRight, CalendarIcon } from "lucide-react"; + +import { Calendar } from "@/components/DatePicker/calendar"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { LanguageContext } from "@/context/language/LanguageContext"; +import { cn } from "@/lib/utils"; +import { daysBetween, formatDateLong, formatDateShort } from "@/utils/dates"; +import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters"; + +import { DateRangeDropdown } from "./dataRangeDropdown"; +import { MonthPicker } from "./monthPicker"; +import { YearPicker } from "./yearPicker"; interface DatePickerProps { - className?: string - label?: string - mode: "single" | "range" - value?: DateRange - onChange: (dateRange: DateRange | undefined) => void - customComponent?: React.ReactElement - disabled?: Matcher | Matcher[] | undefined - displayIncrementButtons?: boolean - disableFutureNavigation?: boolean - disableAutoSelect?: boolean - currentYearPosition?: "start" | "center" | "end" - order?: "asc" | "desc" - dateFilterType?: "issue" | "maturity" - onDateFilterTypeChange?: (type: "issue" | "maturity") => void + className?: string; + label?: string; + mode: "single" | "range"; + value?: DateRange; + onChange: (dateRange: DateRange | undefined) => void; + customComponent?: React.ReactElement; + disabled?: Matcher | Matcher[] | undefined; + displayIncrementButtons?: boolean; + disableFutureNavigation?: boolean; + disableAutoSelect?: boolean; + currentYearPosition?: "start" | "center" | "end"; + order?: "asc" | "desc"; + dateFilterType?: "issue" | "maturity"; + onDateFilterTypeChange?: (type: "issue" | "maturity") => void; } export function DatePicker({ @@ -49,124 +49,135 @@ export function DatePicker({ dateFilterType = "issue", onDateFilterTypeChange, }: DatePickerProps) { - const lang = useContext(LanguageContext) - const { formatDateMmmDdYyyy } = useUtcDateFormatters(lang.locale) - const [canSelect, setCanSelect] = useState(false) - const [showCalendar, setShowCalendar] = useState(false) - const [showYearPicker, setShowYearPicker] = useState(false) - const [showMonthPicker, setShowMonthPicker] = useState(false) - const [selectedRange, setSelectedRange] = useState() - const allowRangeSelection = useMemo(() => mode === "range", [mode]) - const [hasBeenCleared, setHasBeenCleared] = useState(false) + const lang = useContext(LanguageContext); + const { formatDateMmmDdYyyy } = useUtcDateFormatters(lang.locale); + const [canSelect, setCanSelect] = useState(false); + const [showCalendar, setShowCalendar] = useState(false); + const [showYearPicker, setShowYearPicker] = useState(false); + const [showMonthPicker, setShowMonthPicker] = useState(false); + const [selectedRange, setSelectedRange] = useState(); + const allowRangeSelection = useMemo(() => mode === "range", [mode]); + const [hasBeenCleared, setHasBeenCleared] = useState(false); const getInitialDate = () => { if (value) { - return value + return value; } if (hasBeenCleared) { - return { from: undefined, to: undefined } + return { from: undefined, to: undefined }; } if (disableAutoSelect) { - return { from: undefined, to: undefined } + return { from: undefined, to: undefined }; } - return { from: new Date(), to: undefined } - } + return { from: new Date(), to: undefined }; + }; - const [current, setCurrent] = useState(getInitialDate()) - const [draft, setDraft] = useState(getInitialDate()) - const [rangeFocus, setRangeFocus] = useState<"from" | "to">("from") - const baseDate = useMemo(() => current.from ?? new Date(), [current]) + const [current, setCurrent] = useState(getInitialDate()); + const [draft, setDraft] = useState(getInitialDate()); + const [rangeFocus, setRangeFocus] = useState<"from" | "to">("from"); + const baseDate = useMemo(() => current.from ?? new Date(), [current]); useEffect(() => { if (value) { - setCurrent(value) - setDraft(value) - setHasBeenCleared(false) + setCurrent(value); + setDraft(value); + setHasBeenCleared(false); } - }, [value]) + }, [value]); const toggleCalendar = () => { setShowCalendar((prev) => { - const willOpen = !prev + const willOpen = !prev; if (willOpen) { - setDraft(current) + setDraft(current); } - return willOpen - }) - } + return willOpen; + }); + }; const toggleYearPicker = () => { - setShowYearPicker((prev) => !prev) - } + setShowYearPicker((prev) => !prev); + }; const clearSelection = () => { - const clearedRange: DateRange = { from: draft.from ?? current.from, to: undefined } - setSelectedRange(undefined) - setDraft(clearedRange) - setCurrent(clearedRange) - onChange(clearedRange) - setHasBeenCleared(true) - setRangeFocus("to") - setShowMonthPicker(false) - setShowYearPicker(false) - } + const clearedRange: DateRange = { + from: draft.from ?? current.from, + to: undefined, + }; + setSelectedRange(undefined); + setDraft(clearedRange); + setCurrent(clearedRange); + onChange(clearedRange); + setHasBeenCleared(true); + setRangeFocus("to"); + setShowMonthPicker(false); + setShowYearPicker(false); + }; useEffect(() => { if (selectedRange === undefined) { - return + return; } - const startDate = draft.from ?? current.from ?? new Date() + const startDate = draft.from ?? current.from ?? new Date(); const newRange = { from: startDate, to: addDays(startDate, selectedRange), - } + }; - setCurrent(newRange) - setDraft(newRange) + setCurrent(newRange); + setDraft(newRange); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedRange]) + }, [selectedRange]); useEffect(() => { setSelectedRange((val) => { if (current.from === undefined || current.to === undefined) { - return val + return val; } - const diffDays = daysBetween(current.from, current.to) - return diffDays !== val ? undefined : val - }) - }, [current]) + const diffDays = daysBetween(current.from, current.to); + return diffDays !== val ? undefined : val; + }); + }, [current]); useEffect(() => { if (!draft.from) { - setCanSelect(false) - return + setCanSelect(false); + return; } // single mode if (mode === "single") { - const isDisabled = disabled ? dateMatchModifiers(draft.from, [disabled as Matcher]) : false - setCanSelect(!isDisabled) + const isDisabled = disabled + ? dateMatchModifiers(draft.from, [disabled as Matcher]) + : false; + setCanSelect(!isDisabled); } // range mode if (mode === "range") { if (!draft.to) { - setCanSelect(false) - return + setCanSelect(false); + return; } - const isDisabledFrom = disabled ? dateMatchModifiers(draft.from, [disabled as Matcher]) : false - const isDisabledTo = disabled ? dateMatchModifiers(draft.to, [disabled as Matcher]) : false - setCanSelect(!isDisabledFrom && !isDisabledTo) + const isDisabledFrom = disabled + ? dateMatchModifiers(draft.from, [disabled as Matcher]) + : false; + const isDisabledTo = disabled + ? dateMatchModifiers(draft.to, [disabled as Matcher]) + : false; + setCanSelect(!isDisabledFrom && !isDisabledTo); } - }, [draft, disabled, mode]) + }, [draft, disabled, mode]); return ( <> {customComponent ? ( - React.cloneElement(customComponent, { onClick: toggleCalendar } as React.HTMLAttributes) + React.cloneElement(customComponent, { + onClick: toggleCalendar, + } as React.HTMLAttributes) ) : (
- +
@@ -329,13 +363,16 @@ export function DatePicker({ key={days} className="bg-elevation-200/70 p-1.5 rounded-sm text-text-300 text-[10px] font-medium" onClick={() => { - const base = current.from ?? new Date() // always start from confirmed date - const newDate = addDays(base, days) + const base = current.from ?? new Date(); // always start from confirmed date + const newDate = addDays(base, days); setDraft({ from: newDate, - to: mode === "range" ? addDays(newDate, days) : undefined, - }) + to: + mode === "range" + ? addDays(newDate, days) + : undefined, + }); }} > +{days} @@ -344,7 +381,9 @@ export function DatePicker({
)}
-
{current.from ? formatDateMmmDdYyyy(current.from) : "-"}
+
+ {current.from ? formatDateMmmDdYyyy(current.from) : "-"} +
)} @@ -357,13 +396,13 @@ export function DatePicker({ setDraft({ ...draft, from: date, - }) - setShowYearPicker(false) - setShowMonthPicker(true) + }); + setShowYearPicker(false); + setShowMonthPicker(true); }} onCaptionLabelClicked={() => { - setShowYearPicker(false) - setShowMonthPicker(false) + setShowYearPicker(false); + setShowMonthPicker(false); }} disableFutureNavigation={disableFutureNavigation} currentYearPosition={currentYearPosition} @@ -377,13 +416,13 @@ export function DatePicker({ setDraft({ ...draft, from: date, - }) - setShowYearPicker(false) - setShowMonthPicker(false) + }); + setShowYearPicker(false); + setShowMonthPicker(false); }} onCaptionLabelClicked={() => { - setShowYearPicker(true) - setShowMonthPicker(false) + setShowYearPicker(true); + setShowMonthPicker(false); }} disableFutureNavigation={disableFutureNavigation} /> @@ -397,13 +436,18 @@ export function DatePicker({ onSelect={(_ignored: DateRange | undefined, selectedDay) => { setDraft((prev) => { if (rangeFocus === "from") { - const newTo = prev.to && selectedDay <= prev.to ? prev.to : undefined - return { from: selectedDay, to: newTo } + const newTo = + prev.to && selectedDay <= prev.to ? prev.to : undefined; + return { from: selectedDay, to: newTo }; } - const from = prev.from ?? selectedDay - return selectedDay < from ? { from: selectedDay, to: from } : { from, to: selectedDay } - }) - setRangeFocus((prevFocus) => (prevFocus === "from" ? "to" : "from")) + const from = prev.from ?? selectedDay; + return selectedDay < from + ? { from: selectedDay, to: from } + : { from, to: selectedDay }; + }); + setRangeFocus((prevFocus) => + prevFocus === "from" ? "to" : "from", + ); }} initialFocus disabled={disabled} @@ -427,9 +471,9 @@ export function DatePicker({ size="sm" type="button" onClick={() => { - setShowMonthPicker(false) - setShowYearPicker(false) - setShowCalendar(false) + setShowMonthPicker(false); + setShowYearPicker(false); + setShowCalendar(false); }} > { - setCurrent(draft) - onChange(draft) - setShowMonthPicker(false) - setShowYearPicker(false) - setShowCalendar(false) + setCurrent(draft); + onChange(draft); + setShowMonthPicker(false); + setShowYearPicker(false); + setShowCalendar(false); }} > - ) + ); } diff --git a/src/components/DatePicker/dropdownMenu.tsx b/src/components/DatePicker/dropdownMenu.tsx index 033a75d..f76709b 100644 --- a/src/components/DatePicker/dropdownMenu.tsx +++ b/src/components/DatePicker/dropdownMenu.tsx @@ -1,25 +1,25 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( -)) -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -49,8 +50,9 @@ const DropdownMenuSubContent = React.forwardRef< )} {...props} /> -)) -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -67,13 +69,13 @@ const DropdownMenuContent = React.forwardRef< {...props} /> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -108,8 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< {children} -)) -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -130,35 +133,51 @@ const DropdownMenuRadioItem = React.forwardRef< {children} -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName - -const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { - return -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, @@ -176,4 +195,4 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, -} +}; diff --git a/src/components/DatePicker/monthPicker.tsx b/src/components/DatePicker/monthPicker.tsx index 467e731..bfc1f2a 100644 --- a/src/components/DatePicker/monthPicker.tsx +++ b/src/components/DatePicker/monthPicker.tsx @@ -1,18 +1,18 @@ -import { useContext, useEffect, useState } from "react" -import { ChevronLeft, ChevronRight, ChevronUp } from "lucide-react" +import { useContext, useEffect, useState } from "react"; +import { ChevronLeft, ChevronRight, ChevronUp } from "lucide-react"; -import { LanguageContext } from "@/context/language/LanguageContext" -import { cn } from "@/lib/utils" -import { formatMonthLong, formatMonthYear } from "@/utils/dates" +import { LanguageContext } from "@/context/language/LanguageContext"; +import { cn } from "@/lib/utils"; +import { formatMonthLong, formatMonthYear } from "@/utils/dates"; -import { buttonVariants } from "../ui/button" +import { buttonVariants } from "../ui/button"; interface MonthPickerProps { - value: Date - onChange: (date: Date) => void - onCaptionLabelClicked: () => void - disableFutureNavigation?: boolean - minDate?: Date + value: Date; + onChange: (date: Date) => void; + onCaptionLabelClicked: () => void; + disableFutureNavigation?: boolean; + minDate?: Date; } const MonthPicker = ({ @@ -22,64 +22,69 @@ const MonthPicker = ({ disableFutureNavigation = false, minDate, }: MonthPickerProps) => { - const lang = useContext(LanguageContext) - const now = new Date() - const currentYear = now.getUTCFullYear() - const currentMonth = now.getUTCMonth() - const minYear = minDate?.getUTCFullYear() - const minMonth = minDate?.getUTCMonth() + const lang = useContext(LanguageContext); + const now = new Date(); + const currentYear = now.getUTCFullYear(); + const currentMonth = now.getUTCMonth(); + const minYear = minDate?.getUTCFullYear(); + const minMonth = minDate?.getUTCMonth(); const [base, setBase] = useState(() => { - let initYear = value.getUTCFullYear() + let initYear = value.getUTCFullYear(); if (disableFutureNavigation) { - initYear = Math.min(initYear, currentYear) + initYear = Math.min(initYear, currentYear); } if (minYear !== undefined) { - initYear = Math.max(initYear, minYear) + initYear = Math.max(initYear, minYear); } - return new Date(Date.UTC(initYear, value.getUTCMonth(), 1)) - }) + return new Date(Date.UTC(initYear, value.getUTCMonth(), 1)); + }); useEffect(() => { - let nextYear = value.getUTCFullYear() + let nextYear = value.getUTCFullYear(); if (disableFutureNavigation) { - nextYear = Math.min(nextYear, currentYear) + nextYear = Math.min(nextYear, currentYear); } if (minYear !== undefined) { - nextYear = Math.max(nextYear, minYear) + nextYear = Math.max(nextYear, minYear); } if (nextYear !== base.getUTCFullYear()) { - setBase(() => new Date(Date.UTC(nextYear, value.getUTCMonth(), 1))) + setBase(() => new Date(Date.UTC(nextYear, value.getUTCMonth(), 1))); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, disableFutureNavigation, currentYear]) + }, [value, disableFutureNavigation, currentYear]); const handleOnChange = (monthIndex: number) => { - const newDate = new Date(base) - newDate.setUTCMonth(monthIndex) - onChange(newDate) - } + const newDate = new Date(base); + newDate.setUTCMonth(monthIndex); + onChange(newDate); + }; const addYears = (years: number) => { - setBase((val) => new Date(Date.UTC(val.getUTCFullYear() + years, val.getUTCMonth(), 1))) - } + setBase( + (val) => + new Date(Date.UTC(val.getUTCFullYear() + years, val.getUTCMonth(), 1)), + ); + }; - const canGoBackward = minYear === undefined || base.getUTCFullYear() > minYear - const canGoForward = !disableFutureNavigation || base.getUTCFullYear() < currentYear + const canGoBackward = + minYear === undefined || base.getUTCFullYear() > minYear; + const canGoForward = + !disableFutureNavigation || base.getUTCFullYear() < currentYear; const nextYear = () => { if (!canGoForward) { - return + return; } - addYears(1) - } + addYears(1); + }; const prevYear = () => { if (!canGoBackward) { - return + return; } - addYears(-1) - } + addYears(-1); + }; return (
@@ -91,9 +96,15 @@ const MonthPicker = ({ })} onClick={prevYear} /> -
+
{formatMonthYear(base, lang.locale)} - +
{Array.from({ length: 12 }, (_, index) => { - const date = new Date(Date.UTC(base.getUTCFullYear(), index, 1)) + const date = new Date(Date.UTC(base.getUTCFullYear(), index, 1)); const isFutureMonth = disableFutureNavigation && - (date.getUTCFullYear() > currentYear || (date.getUTCFullYear() === currentYear && index > currentMonth)) + (date.getUTCFullYear() > currentYear || + (date.getUTCFullYear() === currentYear && index > currentMonth)); const isPastMonth = minYear !== undefined && (date.getUTCFullYear() < minYear || - (date.getUTCFullYear() === minYear && minMonth !== undefined && index < minMonth)) + (date.getUTCFullYear() === minYear && + minMonth !== undefined && + index < minMonth)); const isSelected = - date.getUTCFullYear() === value.getUTCFullYear() && date.getUTCMonth() === value.getUTCMonth() + date.getUTCFullYear() === value.getUTCFullYear() && + date.getUTCMonth() === value.getUTCMonth(); return (
{ - if (!isFutureMonth && !isPastMonth) handleOnChange(index) + if (!isFutureMonth && !isPastMonth) handleOnChange(index); }} - className={cn("h-[42px] flex justify-center items-center", buttonVariants({ variant: "ghost" }), { - "cursor-pointer": !isFutureMonth && !isPastMonth, - "opacity-40 text-text-200 pointer-events-none": isFutureMonth || isPastMonth, - "bg-elevation-200 hover:bg-elevation-200 border border-divider-100": isSelected, - })} + className={cn( + "h-[42px] flex justify-center items-center", + buttonVariants({ variant: "ghost" }), + { + "cursor-pointer": !isFutureMonth && !isPastMonth, + "opacity-40 text-text-200 pointer-events-none": + isFutureMonth || isPastMonth, + "bg-elevation-200 hover:bg-elevation-200 border border-divider-100": + isSelected, + }, + )} > {formatMonthLong(date, lang.locale)}
- ) + ); })}
- ) -} + ); +}; -export { MonthPicker } +export { MonthPicker }; diff --git a/src/components/DatePicker/yearPicker.tsx b/src/components/DatePicker/yearPicker.tsx index 2450840..e3027c2 100644 --- a/src/components/DatePicker/yearPicker.tsx +++ b/src/components/DatePicker/yearPicker.tsx @@ -1,21 +1,21 @@ -import { useContext, useEffect, useRef, useState } from "react" -import { ChevronLeft, ChevronRight, ChevronUp } from "lucide-react" +import { useContext, useEffect, useRef, useState } from "react"; +import { ChevronLeft, ChevronRight, ChevronUp } from "lucide-react"; -import { LanguageContext } from "@/context/language/LanguageContext" -import { cn } from "@/lib/utils" -import { formatYearNumeric } from "@/utils/dates" +import { LanguageContext } from "@/context/language/LanguageContext"; +import { cn } from "@/lib/utils"; +import { formatYearNumeric } from "@/utils/dates"; -import { buttonVariants } from "../ui/button" +import { buttonVariants } from "../ui/button"; interface YearPickerProps { - value: Date - onChange: (date: Date) => void - onCaptionLabelClicked: () => void - numberYears?: number - disableFutureNavigation?: boolean - minDate?: Date - currentYearPosition?: "start" | "center" | "end" - order?: "asc" | "desc" + value: Date; + onChange: (date: Date) => void; + onCaptionLabelClicked: () => void; + numberYears?: number; + disableFutureNavigation?: boolean; + minDate?: Date; + currentYearPosition?: "start" | "center" | "end"; + order?: "asc" | "desc"; } const YearPicker = ({ @@ -28,89 +28,107 @@ const YearPicker = ({ currentYearPosition = "start", order = "asc", }: YearPickerProps) => { - const lang = useContext(LanguageContext) - const currentYear = new Date().getUTCFullYear() - const minYear = minDate?.getUTCFullYear() - const total = numberYears - const half = Math.floor(total / 2) - const positionIndex = currentYearPosition === "center" ? half : currentYearPosition === "end" ? total - 1 : 0 - const maxBaseYear = currentYear - (total - 1 - positionIndex) + const lang = useContext(LanguageContext); + const currentYear = new Date().getUTCFullYear(); + const minYear = minDate?.getUTCFullYear(); + const total = numberYears; + const half = Math.floor(total / 2); + const positionIndex = + currentYearPosition === "center" + ? half + : currentYearPosition === "end" + ? total - 1 + : 0; + const maxBaseYear = currentYear - (total - 1 - positionIndex); const [base, setBase] = useState(() => { - let initial = value.getUTCFullYear() + let initial = value.getUTCFullYear(); if (disableFutureNavigation) { - initial = Math.min(initial, maxBaseYear) + initial = Math.min(initial, maxBaseYear); } if (minYear !== undefined) { - initial = Math.max(initial, minYear) + initial = Math.max(initial, minYear); } - return new Date(Date.UTC(initial, 0, 1)) - }) + return new Date(Date.UTC(initial, 0, 1)); + }); const handleOnChange = (year: number) => { - const updateDate = new Date(value) - updateDate.setUTCFullYear(year) - onChange(updateDate) - } + const updateDate = new Date(value); + updateDate.setUTCFullYear(year); + onChange(updateDate); + }; - const startYear = base.getUTCFullYear() - positionIndex - const endYear = startYear + total - 1 - const prevSelectedRef = useRef(value.getUTCFullYear()) + const startYear = base.getUTCFullYear() - positionIndex; + const endYear = startYear + total - 1; + const prevSelectedRef = useRef(value.getUTCFullYear()); useEffect(() => { - const selected = value.getUTCFullYear() + const selected = value.getUTCFullYear(); if (selected === prevSelectedRef.current) { - return + return; } - prevSelectedRef.current = selected + prevSelectedRef.current = selected; - const maxBaseYear = currentYear - (total - 1 - positionIndex) - let clamped = disableFutureNavigation ? Math.min(selected, maxBaseYear) : selected + const maxBaseYear = currentYear - (total - 1 - positionIndex); + let clamped = disableFutureNavigation + ? Math.min(selected, maxBaseYear) + : selected; if (minYear !== undefined) { - clamped = Math.max(clamped, minYear) + clamped = Math.max(clamped, minYear); } - setBase(new Date(Date.UTC(clamped, 0, 1))) - }, [value, disableFutureNavigation, currentYear, total, positionIndex, minYear]) - - const canGoForward = !disableFutureNavigation || endYear < currentYear - const canGoBackward = minYear === undefined || startYear > minYear + setBase(new Date(Date.UTC(clamped, 0, 1))); + }, [ + value, + disableFutureNavigation, + currentYear, + total, + positionIndex, + minYear, + ]); + + const canGoForward = !disableFutureNavigation || endYear < currentYear; + const canGoBackward = minYear === undefined || startYear > minYear; const nextYears = () => { if (canGoForward) { setBase((val) => { - const target = val.getUTCFullYear() + numberYears - const maxBaseYear = currentYear - (total - 1 - positionIndex) - const clamped = disableFutureNavigation ? Math.min(target, maxBaseYear) : target - return new Date(Date.UTC(clamped, 0, 1)) - }) + const target = val.getUTCFullYear() + numberYears; + const maxBaseYear = currentYear - (total - 1 - positionIndex); + const clamped = disableFutureNavigation + ? Math.min(target, maxBaseYear) + : target; + return new Date(Date.UTC(clamped, 0, 1)); + }); } - } + }; const prevYears = () => { if (!canGoBackward) { - return + return; } - setBase((val) => new Date(Date.UTC(val.getUTCFullYear() - numberYears, 0, 1))) - } + setBase( + (val) => new Date(Date.UTC(val.getUTCFullYear() - numberYears, 0, 1)), + ); + }; - let touchStartX: number | null = null + let touchStartX: number | null = null; const handleTouchStart = (e: React.TouchEvent) => { - touchStartX = e.touches[0].clientX - } + touchStartX = e.touches[0].clientX; + }; const handleTouchEnd = (e: React.TouchEvent) => { - if (touchStartX === null) return - const diffX = e.changedTouches[0].clientX - touchStartX + if (touchStartX === null) return; + const diffX = e.changedTouches[0].clientX - touchStartX; if (diffX > 50) { - prevYears() + prevYears(); } else if (diffX < -50) { - nextYears() + nextYears(); } - touchStartX = null - } + touchStartX = null; + }; - const years = Array.from({ length: numberYears }, (_, i) => startYear + i) - const displayYears = order === "desc" ? [...years].reverse() : years + const years = Array.from({ length: numberYears }, (_, i) => startYear + i); + const displayYears = order === "desc" ? [...years].reverse() : years; return (
-
+
{formatYearNumeric(value, lang.locale)} - +
{displayYears.map((year, index) => { - const isSelected = year === value.getUTCFullYear() + const isSelected = year === value.getUTCFullYear(); const isDisabled = - (disableFutureNavigation && year > currentYear) || (minYear !== undefined && year < minYear) + (disableFutureNavigation && year > currentYear) || + (minYear !== undefined && year < minYear); return (
{ - if (!isDisabled) handleOnChange(year) + if (!isDisabled) handleOnChange(year); }} - className={cn("h-[42px] flex justify-center items-center", buttonVariants({ variant: "ghost" }), { - "cursor-pointer": !isDisabled, - "bg-elevation-200 hover:bg-elevation-200 border border-divider-100": isSelected, - "opacity-40 text-text-200 pointer-events-none": isDisabled, - })} + className={cn( + "h-[42px] flex justify-center items-center", + buttonVariants({ variant: "ghost" }), + { + "cursor-pointer": !isDisabled, + "bg-elevation-200 hover:bg-elevation-200 border border-divider-100": + isSelected, + "opacity-40 text-text-200 pointer-events-none": isDisabled, + }, + )} > {year}
- ) + ); })}
- ) -} + ); +}; -export { YearPicker } +export { YearPicker }; diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index 96e4409..c1f008d 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -1,4 +1,4 @@ -import { Button, buttonVariants } from "@/components/ui/button" +import { Button, buttonVariants } from "@/components/ui/button"; import { Drawer, DrawerClose, @@ -8,18 +8,24 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, -} from "@/components/ui/drawer" -import { VariantProps } from "class-variance-authority" -import { useIntl } from "react-intl" +} from "@/components/ui/drawer"; +import { VariantProps } from "class-variance-authority"; +import { useIntl } from "react-intl"; -type DrawerProps = Parameters[0] +type DrawerProps = Parameters[0]; type BaseDrawerProps = DrawerProps & { - title: string - description?: string - trigger?: React.ReactNode - children?: React.ReactNode -} -export function BaseDrawer({ title, description = "", trigger, children, ...drawerProps }: BaseDrawerProps) { + title: string; + description?: string; + trigger?: React.ReactNode; + children?: React.ReactNode; +}; +export function BaseDrawer({ + title, + description = "", + trigger, + children, + ...drawerProps +}: BaseDrawerProps) { return ( {trigger && {trigger}} @@ -27,22 +33,24 @@ export function BaseDrawer({ title, description = "", trigger, children, ...draw
{title} - {description && {description}} + {description && ( + {description} + )} {children}
- ) + ); } type ConfirmDrawerProps = BaseDrawerProps & { - cancelButtonText?: string - submitButtonText?: string - submitButtonVariant?: VariantProps["variant"] - submitButtonDisabled?: boolean - onSubmit: () => void -} + cancelButtonText?: string; + submitButtonText?: string; + submitButtonVariant?: VariantProps["variant"]; + submitButtonDisabled?: boolean; + onSubmit: () => void; +}; export function ConfirmDrawer({ cancelButtonText, @@ -53,19 +61,19 @@ export function ConfirmDrawer({ children, ...drawerProps }: ConfirmDrawerProps) { - const intl = useIntl() + const intl = useIntl(); const resolvedCancelText = cancelButtonText ?? intl.formatMessage({ id: "Cancel", defaultMessage: "Cancel", - }) + }); const resolvedSubmitText = submitButtonText ?? intl.formatMessage({ id: "Confirm", defaultMessage: "Confirm", - }) + }); return ( {children} @@ -81,12 +89,16 @@ export function ConfirmDrawer({ {resolvedSubmitText} -
- ) + ); } diff --git a/src/components/EndorsementChain.tsx b/src/components/EndorsementChain.tsx index b79f8d2..b3e6ac6 100644 --- a/src/components/EndorsementChain.tsx +++ b/src/components/EndorsementChain.tsx @@ -1,5 +1,5 @@ -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChevronDown, ChevronUp, @@ -11,30 +11,40 @@ import { Coins, DollarSign, ArrowUpDown, -} from "lucide-react" -import type { Endorsement, LightBillParticipant } from "@/generated/client/types.gen" -import { Skeleton } from "@/components/ui/skeleton" -import { Button } from "@/components/ui/button" -import { Separator } from "@/components/ui/separator" -import { TruncatedTextPopover } from "@/components/TruncatedTextPopover" -import { useIntl } from "react-intl" +} from "lucide-react"; +import type { + Endorsement, + LightBillParticipant, +} from "@/generated/client/types.gen"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { TruncatedTextPopover } from "@/components/TruncatedTextPopover"; +import { useIntl } from "react-intl"; interface EndorsementChainProps { - endorsements?: Endorsement[] - isLoading?: boolean - issueDate?: string - maturityDate?: string - mintingEnabled?: boolean - quoteOffered?: boolean + endorsements?: Endorsement[]; + isLoading?: boolean; + issueDate?: string; + maturityDate?: string; + mintingEnabled?: boolean; + quoteOffered?: boolean; } -function LightParticipantInfo({ participant }: { participant: LightBillParticipant }) { - const intl = useIntl() +function LightParticipantInfo({ + participant, +}: { + participant: LightBillParticipant; +}) { + const intl = useIntl(); if ("Anon" in participant) { return (
- {intl.formatMessage({ id: "participants.role.bearer", defaultMessage: "Bearer" })} + {intl.formatMessage({ + id: "participants.role.bearer", + defaultMessage: "Bearer", + })}
- ) + ); } else if ("Ident" in participant) { return (
- + {participant.Ident.city && participant.Ident.country && ( )}
- ) + ); } - return null + return null; } interface HistoryEvent { @@ -70,9 +84,9 @@ interface HistoryEvent { | "acceptance" | "rejection" | "minting" - | "rejectedToPay" - timestamp?: number - data: Endorsement | null + | "rejectedToPay"; + timestamp?: number; + data: Endorsement | null; } const EVENT_CONFIG = { @@ -130,7 +144,7 @@ const EVENT_CONFIG = { labelId: "endorsement.event.payment", defaultLabel: "Payment received", }, -} as const +} as const; export function EndorsementChain({ endorsements, @@ -147,21 +161,21 @@ export function EndorsementChain({ quoteOffered, offeredTimestamp, }: EndorsementChainProps & { - requestToPayTimestamp?: number - rejectedToPayTimestamp?: number - paymentTimestamp?: number - acceptanceTimestamp?: number - rejectionTimestamp?: number - mintingTimestamp?: number - offeredTimestamp?: number + requestToPayTimestamp?: number; + rejectedToPayTimestamp?: number; + paymentTimestamp?: number; + acceptanceTimestamp?: number; + rejectionTimestamp?: number; + mintingTimestamp?: number; + offeredTimestamp?: number; }) { - const intl = useIntl() - const [isExpanded, setIsExpanded] = useState(false) - const [sortByTimestamp, setSortByTimestamp] = useState(false) + const intl = useIntl(); + const [isExpanded, setIsExpanded] = useState(false); + const [sortByTimestamp, setSortByTimestamp] = useState(false); const titleLabel = intl.formatMessage({ id: "endorsement.history.title", defaultMessage: "Bill history", - }) + }); if (isLoading) { return ( @@ -173,18 +187,18 @@ export function EndorsementChain({ - ) + ); } - const events: HistoryEvent[] = [] + const events: HistoryEvent[] = []; if (issueDate) { - const issueTimestamp = new Date(issueDate).getTime() / 1000 + const issueTimestamp = new Date(issueDate).getTime() / 1000; events.push({ type: "issue", timestamp: issueTimestamp, data: null, - }) + }); } if (quoteOffered) { @@ -192,7 +206,7 @@ export function EndorsementChain({ type: "offered", timestamp: offeredTimestamp ?? undefined, data: null, - }) + }); } if (endorsements) { @@ -201,8 +215,8 @@ export function EndorsementChain({ type: "endorsement", timestamp: endorsement.signing_timestamp, data: endorsement, - }) - }) + }); + }); } if (requestToPayTimestamp) { @@ -210,7 +224,7 @@ export function EndorsementChain({ type: "requestToPay", timestamp: requestToPayTimestamp, data: null, - }) + }); } if (rejectedToPayTimestamp) { @@ -218,7 +232,7 @@ export function EndorsementChain({ type: "rejectedToPay", timestamp: rejectedToPayTimestamp, data: null, - }) + }); } if (paymentTimestamp) { @@ -226,7 +240,7 @@ export function EndorsementChain({ type: "payment", timestamp: paymentTimestamp, data: null, - }) + }); } if (acceptanceTimestamp) { @@ -234,7 +248,7 @@ export function EndorsementChain({ type: "acceptance", timestamp: acceptanceTimestamp, data: null, - }) + }); } if (rejectionTimestamp) { @@ -242,7 +256,7 @@ export function EndorsementChain({ type: "rejection", timestamp: rejectionTimestamp, data: null, - }) + }); } if (mintingTimestamp !== undefined) { @@ -250,13 +264,13 @@ export function EndorsementChain({ type: "minting", timestamp: mintingTimestamp, data: null, - }) + }); } else if (mintingEnabled) { events.push({ type: "minting", timestamp: undefined, data: null, - }) + }); } const typePriority: Record = { @@ -269,20 +283,23 @@ export function EndorsementChain({ requestToPay: 5, rejectedToPay: 6, payment: 7, - } + }; events.sort((a, b) => { if (!sortByTimestamp) { - return typePriority[a.type] - typePriority[b.type] + return typePriority[a.type] - typePriority[b.type]; } - return typePriority[b.type] - typePriority[a.type] - }) + return typePriority[b.type] - typePriority[a.type]; + }); - const eventCount = events.length + const eventCount = events.length; return ( - setIsExpanded(!isExpanded)}> + setIsExpanded(!isExpanded)} + >
{titleLabel} @@ -290,19 +307,34 @@ export function EndorsementChain({ {intl.formatMessage( { id: "endorsement.history.eventCount", - defaultMessage: "({count, plural, one {# event} other {# events}})", + defaultMessage: + "({count, plural, one {# event} other {# events}})", }, { count: eventCount }, )}
-
@@ -314,16 +346,22 @@ export function EndorsementChain({ variant="outline" size="sm" onClick={(e) => { - e.stopPropagation() - setSortByTimestamp(!sortByTimestamp) + e.stopPropagation(); + setSortByTimestamp(!sortByTimestamp); }} className="gap-2" > {sortByTimestamp - ? intl.formatMessage({ id: "endorsement.history.descending", defaultMessage: "Descending" }) - : intl.formatMessage({ id: "endorsement.history.ascending", defaultMessage: "Ascending" })} + ? intl.formatMessage({ + id: "endorsement.history.descending", + defaultMessage: "Descending", + }) + : intl.formatMessage({ + id: "endorsement.history.ascending", + defaultMessage: "Ascending", + })}
@@ -338,12 +376,12 @@ export function EndorsementChain({ ) : (
{events.map((event, index) => { - const config = EVENT_CONFIG[event.type] - const Icon = config.icon + const config = EVENT_CONFIG[event.type]; + const Icon = config.icon; const displayLabel = intl.formatMessage({ id: config.labelId, defaultMessage: config.defaultLabel, - }) + }); return (
@@ -351,14 +389,21 @@ export function EndorsementChain({ {/* Event Header */}
- {displayLabel} + + {displayLabel} +
{/* Timestamp */} {event.timestamp !== undefined && (
- {new Date(event.timestamp * 1000).toLocaleString(undefined, { timeZone: "UTC" })} + + {new Date(event.timestamp * 1000).toLocaleString( + undefined, + { timeZone: "UTC" }, + )} +
)} @@ -385,7 +430,9 @@ export function EndorsementChain({ defaultMessage: "Signed by", })}
- + {event.data.signed.signatory && (
@@ -410,7 +457,9 @@ export function EndorsementChain({ defaultMessage: "Endorsed to", })}
- +
@@ -439,14 +488,16 @@ export function EndorsementChain({ )} - {index < events.length - 1 && } + {index < events.length - 1 && ( + + )} - ) + ); })} )} )} - ) + ); } diff --git a/src/components/GrossToNetDiscountForm.tsx b/src/components/GrossToNetDiscountForm.tsx index c7bb341..c3d66c5 100644 --- a/src/components/GrossToNetDiscountForm.tsx +++ b/src/components/GrossToNetDiscountForm.tsx @@ -1,51 +1,51 @@ -import React, { useEffect, useMemo, useRef, useState } from "react" -import { useForm } from "react-hook-form" -import Big from "big.js" -import { parseFloatSafe, parseIntSafe } from "@/utils/numbers" -import { daysBetween } from "@/utils/dates" -import { Act360 } from "@/utils/discount-util" -import { Button } from "./ui/button" -import { DrawerFooter, DrawerClose } from "./ui/drawer" -import { setItem, getItem } from "@/utils/local-storage" // , removeItem -import { useIntl } from "react-intl" +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import Big from "big.js"; +import { parseFloatSafe, parseIntSafe } from "@/utils/numbers"; +import { daysBetween } from "@/utils/dates"; +import { Act360 } from "@/utils/discount-util"; +import { Button } from "./ui/button"; +import { DrawerFooter, DrawerClose } from "./ui/drawer"; +import { setItem, getItem } from "@/utils/local-storage"; // , removeItem +import { useIntl } from "react-intl"; interface CurrencyAmount { - value: Big - currency: string + value: Big; + currency: string; } interface CommonDiscountFormProps { - startDate?: Date - endDate: Date - onSubmit: (values: FormResult) => void + startDate?: Date; + endDate: Date; + onSubmit: (values: FormResult) => void; } type GrossToNetProps = CommonDiscountFormProps & { - gross: CurrencyAmount - submitButtonText?: string - onConfirm?: () => void - quoteId?: string -} + gross: CurrencyAmount; + submitButtonText?: string; + onConfirm?: () => void; + quoteId?: string; +}; interface FormResult { - days: number - discountRate: Big - net: CurrencyAmount - gross: CurrencyAmount + days: number; + discountRate: Big; + net: CurrencyAmount; + gross: CurrencyAmount; } interface FormValues { - daysInput?: string - discountRateInput?: string - netInput?: string + daysInput?: string; + discountRateInput?: string; + netInput?: string; } -const INPUT_DAYS_MIN_VALUE = 1 -const INPUT_DAYS_MAX_VALUE = 360 -const LOCAL_STORAGE_KEY_PREFIX = "offer-form-" -const NET_INPUT_DECIMALS = 2 +const INPUT_DAYS_MIN_VALUE = 1; +const INPUT_DAYS_MAX_VALUE = 360; +const LOCAL_STORAGE_KEY_PREFIX = "offer-form-"; +const NET_INPUT_DECIMALS = 2; -type GrossToNetFormValues = FormValues +type GrossToNetFormValues = FormValues; const GrossToNetDiscountForm = ({ startDate, @@ -55,123 +55,129 @@ const GrossToNetDiscountForm = ({ submitButtonText, quoteId, }: GrossToNetProps) => { - const intl = useIntl() - const [hasSetInitialDays, setHasSetInitialDays] = useState(false) - const [lastEdited, setLastEdited] = useState<"rate" | "net" | null>(null) - const isSat = gross.currency === "sat" + const intl = useIntl(); + const [hasSetInitialDays, setHasSetInitialDays] = useState(false); + const [lastEdited, setLastEdited] = useState<"rate" | "net" | null>(null); + const isSat = gross.currency === "sat"; const daysLabel = intl.formatMessage({ id: "discountForm.days", defaultMessage: "Days", - }) + }); const discountRateLabel = intl.formatMessage({ id: "discountForm.discountRate", defaultMessage: "Fee rate", - }) + }); const netAmountLabel = intl.formatMessage({ id: "discountForm.netAmount", defaultMessage: "Net amount", - }) + }); const annualDiscountLabel = intl.formatMessage({ id: "discountForm.annualDiscount", defaultMessage: "Annual fee", - }) + }); const grossAmountLabel = intl.formatMessage({ id: "discountForm.grossAmount", defaultMessage: "Gross amount", - }) + }); const parseDigitsToInt = (value: unknown) => { - let str = "" + let str = ""; if (typeof value === "string" || typeof value === "number") { - str = String(value) + str = String(value); } - return str.replace(/\D/g, "") - } + return str.replace(/\D/g, ""); + }; const validateNetAmount = (value?: string) => { if (value == null || value === "") { return intl.formatMessage({ id: "discountForm.validation.net.required", defaultMessage: "Net amount is required", - }) + }); } - const parsed = isSat ? parseIntSafe(value) : parseFloatSafe(value) + const parsed = isSat ? parseIntSafe(value) : parseFloatSafe(value); if (parsed === undefined || Number.isNaN(parsed)) { return intl.formatMessage({ id: "discountForm.validation.net.invalid", defaultMessage: "Net amount must be a valid number", - }) + }); } if (parsed < 1) { return intl.formatMessage( - { id: "discountForm.validation.net.min", defaultMessage: "Net amount must be at least {min}" }, + { + id: "discountForm.validation.net.min", + defaultMessage: "Net amount must be at least {min}", + }, { min: 1 }, - ) + ); } if (new Big(parsed).gt(gross.value)) { return intl.formatMessage({ id: "discountForm.validation.net.maxGross", defaultMessage: "Net amount cannot exceed gross amount", - }) - } - - return true - } - - const validateMinInteger = (min: number, label: string) => (value?: string) => { - if (value == null || value === "") { - return intl.formatMessage( - { - id: "discountForm.validation.required", - defaultMessage: "{label} is required", - }, - { label }, - ) - } - if (!/^\d+$/.test(value)) { - return intl.formatMessage( - { - id: "discountForm.validation.wholeNumber", - defaultMessage: "{label} must be a whole number", - }, - { label }, - ) - } + }); + } + + return true; + }; + + const validateMinInteger = + (min: number, label: string) => (value?: string) => { + if (value == null || value === "") { + return intl.formatMessage( + { + id: "discountForm.validation.required", + defaultMessage: "{label} is required", + }, + { label }, + ); + } + if (!/^\d+$/.test(value)) { + return intl.formatMessage( + { + id: "discountForm.validation.wholeNumber", + defaultMessage: "{label} must be a whole number", + }, + { label }, + ); + } - const n = parseInt(value, 10) - if (Number.isNaN(n)) { - return intl.formatMessage( - { - id: "discountForm.validation.invalid", - defaultMessage: "{label} is invalid", - }, - { label }, - ) - } - if (n < min) { - return intl.formatMessage( - { - id: "discountForm.validation.min", - defaultMessage: "{label} must be at least {min}", - }, - { label, min }, - ) - } - if (n > INPUT_DAYS_MAX_VALUE) { - return intl.formatMessage( - { - id: "discountForm.validation.max", - defaultMessage: "{label} must be at most {max}", - }, - { label, max: INPUT_DAYS_MAX_VALUE }, - ) - } + const n = parseInt(value, 10); + if (Number.isNaN(n)) { + return intl.formatMessage( + { + id: "discountForm.validation.invalid", + defaultMessage: "{label} is invalid", + }, + { label }, + ); + } + if (n < min) { + return intl.formatMessage( + { + id: "discountForm.validation.min", + defaultMessage: "{label} must be at least {min}", + }, + { label, min }, + ); + } + if (n > INPUT_DAYS_MAX_VALUE) { + return intl.formatMessage( + { + id: "discountForm.validation.max", + defaultMessage: "{label} must be at most {max}", + }, + { label, max: INPUT_DAYS_MAX_VALUE }, + ); + } - return true - } + return true; + }; - const localStorageKey = quoteId ? `${LOCAL_STORAGE_KEY_PREFIX}${quoteId}` : null + const localStorageKey = quoteId + ? `${LOCAL_STORAGE_KEY_PREFIX}${quoteId}` + : null; const { watch, register, @@ -180,24 +186,24 @@ const GrossToNetDiscountForm = ({ formState: { isValid, errors }, } = useForm({ mode: "all", - }) + }); const discountRateRegister = register("discountRateInput", { required: true, min: 0, max: 99.9999, - }) + }); const netInputRegister = register("netInput", { required: true, setValueAs: isSat ? parseDigitsToInt : undefined, validate: validateNetAmount, - }) + }); const blockDecimalInput = (e: React.KeyboardEvent) => { if ([".", ",", "e", "E", "+", "-", "^"].includes(e.key)) { - e.preventDefault() - return + e.preventDefault(); + return; } - } + }; const handleKeyDown = (e: React.KeyboardEvent) => { const allowed = new Set([ "Backspace", @@ -209,86 +215,91 @@ const GrossToNetDiscountForm = ({ "Tab", "Home", "End", - ]) + ]); if (e.key === "Enter") { - e.preventDefault() - e.currentTarget.blur() - return + e.preventDefault(); + e.currentTarget.blur(); + return; } if (allowed.has(e.key)) { - return + return; } if (e.key >= "0" && e.key <= "9") { - return + return; } - e.preventDefault() - } + e.preventDefault(); + }; const blockNonDigitInput = (e: React.SyntheticEvent) => { - const native = e.nativeEvent as InputEvent - const data = native.data + const native = e.nativeEvent as InputEvent; + const data = native.data; if (native.type === "beforeinput") { if ( - (native.inputType === "insertText" || native.inputType === "insertCompositionText") && + (native.inputType === "insertText" || + native.inputType === "insertCompositionText") && data && /\D/.test(data) ) { - e.preventDefault() + e.preventDefault(); } } - } - - const handlePasteDigitsFor = (field: "daysInput" | "netInput") => (e: React.ClipboardEvent) => { - e.preventDefault() - const text = e.clipboardData.getData("text") || "" - const digits = text.replace(/\D/g, "") - const input = e.currentTarget - const start = input.selectionStart ?? input.value.length - const end = input.selectionEnd ?? input.value.length - const before = input.value.slice(0, start) - const after = input.value.slice(end) - const next = (before + digits + after).replace(/\D/g, "") - input.value = next - setValue(field, next, { shouldValidate: true, shouldDirty: true }) - if (field === "netInput") { - setLastEdited("net") - } - const caret = (before + digits).length - try { - input.setSelectionRange(caret, caret) - } catch { - // ignore - } - } + }; + + const handlePasteDigitsFor = + (field: "daysInput" | "netInput") => + (e: React.ClipboardEvent) => { + e.preventDefault(); + const text = e.clipboardData.getData("text") || ""; + const digits = text.replace(/\D/g, ""); + const input = e.currentTarget; + const start = input.selectionStart ?? input.value.length; + const end = input.selectionEnd ?? input.value.length; + const before = input.value.slice(0, start); + const after = input.value.slice(end); + const next = (before + digits + after).replace(/\D/g, ""); + input.value = next; + setValue(field, next, { shouldValidate: true, shouldDirty: true }); + if (field === "netInput") { + setLastEdited("net"); + } + const caret = (before + digits).length; + try { + input.setSelectionRange(caret, caret); + } catch { + // ignore + } + }; const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - } + e.preventDefault(); + }; - const { daysInput, discountRateInput, netInput } = watch() + const { daysInput, discountRateInput, netInput } = watch(); const days = useMemo(() => { - return parseIntSafe(daysInput) - }, [daysInput]) + return parseIntSafe(daysInput); + }, [daysInput]); const discountRate = useMemo(() => { - const parsed = parseFloatSafe(discountRateInput) - return parsed === undefined ? undefined : new Big(parsed).div(new Big("100")) - }, [discountRateInput]) + const parsed = parseFloatSafe(discountRateInput); + return parsed === undefined + ? undefined + : new Big(parsed).div(new Big("100")); + }, [discountRateInput]); - const [net, setNet] = useState() - const skipNetToRateRef = useRef(false) + const [net, setNet] = useState(); + const skipNetToRateRef = useRef(false); const netInputValue = useMemo(() => { if (netInput == null || netInput === "") { - return undefined + return undefined; } if (isSat) { - const parsed = parseIntSafe(netInput) - return parsed === undefined ? undefined : new Big(parsed) + const parsed = parseIntSafe(netInput); + return parsed === undefined ? undefined : new Big(parsed); } - const parsed = parseFloatSafe(netInput) - return parsed === undefined ? undefined : new Big(parsed) - }, [netInput, isSat]) + const parsed = parseFloatSafe(netInput); + return parsed === undefined ? undefined : new Big(parsed); + }, [netInput, isSat]); const discount = useMemo(() => { return net === undefined @@ -296,55 +307,70 @@ const GrossToNetDiscountForm = ({ : { value: net.value.sub(gross.value), currency: net.currency, - } - }, [gross, net]) + }; + }, [gross, net]); - const prevNetInputRef = useRef(undefined) + const prevNetInputRef = useRef(undefined); const formatAmount = (value: Big, currency: string) => { if (currency === "sat") { - return value.round(0, Big.roundDown).toFixed(0) + return value.round(0, Big.roundDown).toFixed(0); } - return value.toFixed(NET_INPUT_DECIMALS) - } + return value.toFixed(NET_INPUT_DECIMALS); + }; useEffect(() => { if (hasSetInitialDays) { - return + return; } if (localStorageKey) { - const savedData = getItem<{ daysInput: string; discountRateInput: string; netInput?: string }>(localStorageKey) + const savedData = getItem<{ + daysInput: string; + discountRateInput: string; + netInput?: string; + }>(localStorageKey); if (savedData) { if (savedData.daysInput) { - setValue("daysInput", savedData.daysInput, { shouldValidate: true }) + setValue("daysInput", savedData.daysInput, { shouldValidate: true }); } if (savedData.discountRateInput) { - setValue("discountRateInput", savedData.discountRateInput, { shouldValidate: true }) + setValue("discountRateInput", savedData.discountRateInput, { + shouldValidate: true, + }); } if (savedData.netInput) { - setValue("netInput", savedData.netInput, { shouldValidate: true }) - setLastEdited("net") + setValue("netInput", savedData.netInput, { shouldValidate: true }); + setLastEdited("net"); } - setHasSetInitialDays(true) - return + setHasSetInitialDays(true); + return; } } if (startDate !== undefined) { - setValue("daysInput", String(Math.min(Math.max(1, daysBetween(startDate, endDate)), INPUT_DAYS_MAX_VALUE)), { - shouldValidate: true, - shouldDirty: true, - shouldTouch: true, - }) + setValue( + "daysInput", + String( + Math.min( + Math.max(1, daysBetween(startDate, endDate)), + INPUT_DAYS_MAX_VALUE, + ), + ), + { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }, + ); } - setHasSetInitialDays(true) - }, [startDate, endDate, setValue, localStorageKey, hasSetInitialDays]) + setHasSetInitialDays(true); + }, [startDate, endDate, setValue, localStorageKey, hasSetInitialDays]); useEffect(() => { if (!localStorageKey || !hasSetInitialDays) { - return + return; } if (daysInput || discountRateInput || netInput) { @@ -352,86 +378,94 @@ const GrossToNetDiscountForm = ({ daysInput: daysInput ?? "", discountRateInput: discountRateInput ?? "", netInput: netInput ?? "", - }) + }); } - }, [localStorageKey, daysInput, discountRateInput, netInput, hasSetInitialDays]) + }, [ + localStorageKey, + daysInput, + discountRateInput, + netInput, + hasSetInitialDays, + ]); useEffect(() => { if (netInput === prevNetInputRef.current) { - return + return; } - prevNetInputRef.current = netInput + prevNetInputRef.current = netInput; if (skipNetToRateRef.current) { - return + return; } if (netInput !== undefined) { - setLastEdited("net") + setLastEdited("net"); } - }, [netInput]) + }, [netInput]); useEffect(() => { if (discountRate === undefined || days === undefined) { - setNet(undefined) - return + setNet(undefined); + return; } if (lastEdited === "net") { - return + return; } - const netValue = Act360.grossToNet(gross.value, discountRate, days) - const roundedNetValue = isSat ? netValue.round(0, Big.roundDown) : netValue + const netValue = Act360.grossToNet(gross.value, discountRate, days); + const roundedNetValue = isSat ? netValue.round(0, Big.roundDown) : netValue; setNet({ value: roundedNetValue, currency: gross.currency, - }) - const formattedNet = formatAmount(roundedNetValue, gross.currency) + }); + const formattedNet = formatAmount(roundedNetValue, gross.currency); if (formattedNet !== netInput) { - skipNetToRateRef.current = true - setValue("netInput", formattedNet, { shouldValidate: true }) + skipNetToRateRef.current = true; + setValue("netInput", formattedNet, { shouldValidate: true }); } - }, [gross, days, discountRate, lastEdited, setValue, isSat, netInput]) + }, [gross, days, discountRate, lastEdited, setValue, isSat, netInput]); useEffect(() => { if (skipNetToRateRef.current) { - skipNetToRateRef.current = false - return + skipNetToRateRef.current = false; + return; } if (days === undefined || netInputValue === undefined) { - setNet(undefined) - return + setNet(undefined); + return; } if (netInputValue.lt(0)) { - setNet(undefined) - return + setNet(undefined); + return; } if (netInputValue.gt(gross.value)) { - setNet(undefined) - return + setNet(undefined); + return; } setNet({ value: netInputValue, currency: gross.currency, - }) + }); - const grossValue = gross.value + const grossValue = gross.value; if (grossValue.eq(0)) { - return + return; } - const ratio = new Big(1).minus(netInputValue.div(grossValue)) - const rate = ratio.times(360).div(days) + const ratio = new Big(1).minus(netInputValue.div(grossValue)); + const rate = ratio.times(360).div(days); if (rate.lt(0) || rate.gt(1)) { - return + return; } - const ratePercent = rate.times(100) - setValue("discountRateInput", ratePercent.toFixed(4), { shouldValidate: true }) - }, [days, netInputValue, gross, setValue, netInput]) + const ratePercent = rate.times(100); + setValue("discountRateInput", ratePercent.toFixed(4), { + shouldValidate: true, + }); + }, [days, netInputValue, gross, setValue, netInput]); const handleFormSubmit = () => { if (net === undefined || discountRate === undefined || days === undefined) { - return + return; } onSubmit({ @@ -439,38 +473,42 @@ const GrossToNetDiscountForm = ({ discountRate, net, gross, - }) - } - - const handleIntegerInputFor = (field: "daysInput" | "netInput") => (e: React.SyntheticEvent) => { - const input = e.currentTarget - const cleaned = input.value.replace(/[^\d]/g, "") - if (input.value !== cleaned) { - const caret = input.selectionStart ?? cleaned.length - input.value = cleaned - setValue(field, cleaned, { shouldValidate: true, shouldDirty: true }) - const pos = Math.min(caret, cleaned.length) - try { - input.setSelectionRange(pos, pos) - } catch { - // ignore unsupported setSelectionRange + }); + }; + + const handleIntegerInputFor = + (field: "daysInput" | "netInput") => + (e: React.SyntheticEvent) => { + const input = e.currentTarget; + const cleaned = input.value.replace(/[^\d]/g, ""); + if (input.value !== cleaned) { + const caret = input.selectionStart ?? cleaned.length; + input.value = cleaned; + setValue(field, cleaned, { shouldValidate: true, shouldDirty: true }); + const pos = Math.min(caret, cleaned.length); + try { + input.setSelectionRange(pos, pos); + } catch { + // ignore unsupported setSelectionRange + } } - } - if (field === "netInput") { - setLastEdited("net") - } - if (input.value === cleaned) { - setValue(field, cleaned, { shouldValidate: true, shouldDirty: true }) - } - } + if (field === "netInput") { + setLastEdited("net"); + } + if (input.value === cleaned) { + setValue(field, cleaned, { shouldValidate: true, shouldDirty: true }); + } + }; - const handleConfirmClick: React.MouseEventHandler = (e) => { - e.preventDefault() - e.stopPropagation() + const handleConfirmClick: React.MouseEventHandler = ( + e, + ) => { + e.preventDefault(); + e.stopPropagation(); void handleSubmit(handleFormSubmit)().catch((err) => { - console.error("Submit failed:", err) - }) - } + console.error("Submit failed:", err); + }); + }; return ( <> @@ -478,14 +516,17 @@ const GrossToNetDiscountForm = ({ className="flex flex-col gap-6 min-w-[8rem] px-4" onSubmit={(e) => { handleSubmit(handleFormSubmit)(e).catch((err) => { - console.error("Submit failed:", err) - }) + console.error("Submit failed:", err); + }); }} >
-
, - ) + ); - expect(page.querySelector("h1")?.textContent).toBe("Title One") - expect(page.querySelector("h2")?.textContent).toBe("Title Two") - expect(page.querySelector("h3")?.textContent).toBe("Title Three") - }) -}) + expect(page.querySelector("h1")?.textContent).toBe("Title One"); + expect(page.querySelector("h2")?.textContent).toBe("Title Two"); + expect(page.querySelector("h3")?.textContent).toBe("Title Three"); + }); +}); diff --git a/src/components/Headings.tsx b/src/components/Headings.tsx index 752edc8..d364eeb 100644 --- a/src/components/Headings.tsx +++ b/src/components/Headings.tsx @@ -1,13 +1,25 @@ -import { PropsWithChildren } from "react" +import { PropsWithChildren } from "react"; export function H1({ children }: PropsWithChildren) { - return

{children}

+ return ( +

+ {children} +

+ ); } export function H2({ children }: PropsWithChildren) { - return

{children}

+ return ( +

+ {children} +

+ ); } export function H3({ children }: PropsWithChildren) { - return

{children}

+ return ( +

+ {children} +

+ ); } diff --git a/src/components/InputContainer.tsx b/src/components/InputContainer.tsx index 4134289..5cf6e7b 100644 --- a/src/components/InputContainer.tsx +++ b/src/components/InputContainer.tsx @@ -1,10 +1,10 @@ -import React, { LabelHTMLAttributes, PropsWithChildren } from "react" -import { cn } from "@/lib/utils" +import React, { LabelHTMLAttributes, PropsWithChildren } from "react"; +import { cn } from "@/lib/utils"; type InputContainerProps = PropsWithChildren<{ - htmlFor: LabelHTMLAttributes["htmlFor"] - label: React.ReactNode -}> + htmlFor: LabelHTMLAttributes["htmlFor"]; + label: React.ReactNode; +}>; const InputContainer = ({ children, htmlFor, label }: InputContainerProps) => { return ( @@ -18,7 +18,7 @@ const InputContainer = ({ children, htmlFor, label }: InputContainerProps) => { {children}
- ) -} + ); +}; -export { InputContainer } +export { InputContainer }; diff --git a/src/components/PageTitle.tsx b/src/components/PageTitle.tsx index 812c32f..6776af2 100644 --- a/src/components/PageTitle.tsx +++ b/src/components/PageTitle.tsx @@ -1,6 +1,6 @@ -import { PropsWithChildren } from "react" -import { H1 } from "./Headings" +import { PropsWithChildren } from "react"; +import { H1 } from "./Headings"; export function PageTitle({ children }: PropsWithChildren) { - return

{children}

+ return

{children}

; } diff --git a/src/components/ParticipantsOverview.test.tsx b/src/components/ParticipantsOverview.test.tsx index b91586a..5d1265f 100644 --- a/src/components/ParticipantsOverview.test.tsx +++ b/src/components/ParticipantsOverview.test.tsx @@ -1,62 +1,85 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { ParticipantDetail, ParticipantsOverviewCard } from "./ParticipantsOverview" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { + ParticipantDetail, + ParticipantsOverviewCard, +} from "./ParticipantsOverview"; vi.mock("@/components/ui/avatar", () => ({ - Avatar: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
{children}
- ), - AvatarFallback: ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
{children}
- ), -})) + Avatar: ({ + children, + className, + }: { + children: React.ReactNode; + className?: string; + }) =>
{children}
, + AvatarFallback: ({ + children, + className, + }: { + children: React.ReactNode; + className?: string; + }) =>
{children}
, +})); vi.mock("@/components/ui/tooltip", () => ({ - TooltipProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, - Tooltip: ({ children }: { children: React.ReactNode }) =>
{children}
, - TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, - TooltipContent: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) + TooltipProvider: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Tooltip: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + TooltipTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + TooltipContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); vi.mock("@/components/TruncatedTextPopover", () => ({ - TruncatedTextPopover: ({ text }: { text: React.ReactNode }) => {text}, -})) + TruncatedTextPopover: ({ text }: { text: React.ReactNode }) => ( + {text} + ), +})); vi.mock("@/components/icons/UserAnonymous", () => ({ - UserAnonymousIcon: ({ className }: { className?: string }) => AnonIcon, -})) + UserAnonymousIcon: ({ className }: { className?: string }) => ( + AnonIcon + ), +})); -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderWithIntl(element: ReactElement): HTMLDivElement { - return renderIntoDom({element}) + return renderIntoDom({element}); } beforeEach(() => { if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } -}) +}); describe("ParticipantsOverview", () => { it("renders participants overview with ident and anon entries", () => { @@ -103,16 +126,16 @@ describe("ParticipantsOverview", () => { }, ]} />, - ) + ); - expect(page.textContent).toContain("Drawee") - expect(page.textContent).toContain("Drawer") - expect(page.textContent).toContain("Payee") - expect(page.textContent).toContain("Holder") - expect(page.textContent).toContain("payee@example.com") - expect(page.textContent).toContain("holder-anon-node") - expect(page.textContent).toContain("Bearer") - }) + expect(page.textContent).toContain("Drawee"); + expect(page.textContent).toContain("Drawer"); + expect(page.textContent).toContain("Payee"); + expect(page.textContent).toContain("Holder"); + expect(page.textContent).toContain("payee@example.com"); + expect(page.textContent).toContain("holder-anon-node"); + expect(page.textContent).toContain("Bearer"); + }); it("renders anonymous participant detail", () => { const page = renderWithIntl( @@ -124,12 +147,12 @@ describe("ParticipantsOverview", () => { }, }} />, - ) + ); - expect(page.textContent).toContain("AnonIcon") - expect(page.textContent).toContain("Bearer") - expect(page.textContent).toContain("anon-node-123") - }) + expect(page.textContent).toContain("AnonIcon"); + expect(page.textContent).toContain("Bearer"); + expect(page.textContent).toContain("anon-node-123"); + }); it("renders identified participant detail with contact data", () => { const page = renderWithIntl( @@ -145,16 +168,18 @@ describe("ParticipantsOverview", () => { nostr_relays: [], }} />, - ) + ); - expect(page.textContent).toContain("Ident Name") - expect(page.textContent).toContain("Paris, FR") - expect(page.textContent).toContain("ident-node-123") - expect(page.querySelector('a[href="mailto:ident@example.com"]')).not.toBeNull() - }) + expect(page.textContent).toContain("Ident Name"); + expect(page.textContent).toContain("Paris, FR"); + expect(page.textContent).toContain("ident-node-123"); + expect( + page.querySelector('a[href="mailto:ident@example.com"]'), + ).not.toBeNull(); + }); it("returns nothing when participant detail input is missing", () => { - const page = renderWithIntl() - expect(page.textContent).toBe("") - }) -}) + const page = renderWithIntl(); + expect(page.textContent).toBe(""); + }); +}); diff --git a/src/components/ParticipantsOverview.tsx b/src/components/ParticipantsOverview.tsx index 9db06a9..9bb4d40 100644 --- a/src/components/ParticipantsOverview.tsx +++ b/src/components/ParticipantsOverview.tsx @@ -1,31 +1,49 @@ -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { getDeterministicColor, getInitials } from "@/utils/strings" -import type { BillIdentParticipant, BillParticipant, BillAnonParticipant } from "@/generated/client/types.gen" -import { cn } from "@/lib/utils" -import { TruncatedTextPopover } from "@/components/TruncatedTextPopover" -import { UserAnonymousIcon } from "@/components/icons/UserAnonymous" -import type React from "react" -import { useIntl } from "react-intl" +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { getDeterministicColor, getInitials } from "@/utils/strings"; +import type { + BillIdentParticipant, + BillParticipant, + BillAnonParticipant, +} from "@/generated/client/types.gen"; +import { cn } from "@/lib/utils"; +import { TruncatedTextPopover } from "@/components/TruncatedTextPopover"; +import { UserAnonymousIcon } from "@/components/icons/UserAnonymous"; +import type React from "react"; +import { useIntl } from "react-intl"; -type IdentityPublicData = BillIdentParticipant -type AnonPublicData = BillAnonParticipant -type IdentOrAnonParticipant = BillParticipant +type IdentityPublicData = BillIdentParticipant; +type AnonPublicData = BillAnonParticipant; +type IdentOrAnonParticipant = BillParticipant; -function AnonPublicAvatar({ value, tooltip }: { value?: AnonPublicData; tooltip?: React.ReactNode }) { - const initials = "?" - const backgroundColor = getDeterministicColor(value?.node_id) +function AnonPublicAvatar({ + value, + tooltip, +}: { + value?: AnonPublicData; + tooltip?: React.ReactNode; +}) { + const initials = "?"; + const backgroundColor = getDeterministicColor(value?.node_id); const avatar = ( - + {initials} - ) + ); if (!tooltip) { - return avatar + return avatar; } return ( @@ -35,28 +53,37 @@ function AnonPublicAvatar({ value, tooltip }: { value?: AnonPublicData; tooltip? {tooltip} - ) + ); } -function IdentityPublicAvatar({ value, tooltip }: { value?: IdentityPublicData; tooltip?: React.ReactNode }) { - const initials = getInitials(value?.name) - const backgroundColor = getDeterministicColor(value?.name ?? value?.node_id) - const isCompany = (value?.type as unknown as number) === 1 - const shapeClass = isCompany ? "rounded-lg" : "rounded-full" +function IdentityPublicAvatar({ + value, + tooltip, +}: { + value?: IdentityPublicData; + tooltip?: React.ReactNode; +}) { + const initials = getInitials(value?.name); + const backgroundColor = getDeterministicColor(value?.name ?? value?.node_id); + const isCompany = (value?.type as unknown as number) === 1; + const shapeClass = isCompany ? "rounded-lg" : "rounded-full"; const avatar = ( {initials} - ) + ); if (!tooltip) { - return avatar + return avatar; } return ( @@ -66,23 +93,39 @@ function IdentityPublicAvatar({ value, tooltip }: { value?: IdentityPublicData; {tooltip} - ) + ); } -function IdentOrAnonAvatar({ value, tooltip }: { value?: IdentOrAnonParticipant; tooltip?: React.ReactNode }) { +function IdentOrAnonAvatar({ + value, + tooltip, +}: { + value?: IdentOrAnonParticipant; + tooltip?: React.ReactNode; +}) { if (!value) { - return null + return null; } if ("Ident" in value) { - const identData = value.Ident - return + const identData = value.Ident; + return ( + + ); } else if ("Anon" in value) { - const anonData = value.Anon - return + const anonData = value.Anon; + return ( + + ); } - return null + return null; } export function ParticipantsOverviewCard({ @@ -92,36 +135,39 @@ export function ParticipantsOverviewCard({ holder, className, }: { - drawee?: IdentityPublicData - drawer?: IdentityPublicData - holder?: IdentOrAnonParticipant[] - payee?: IdentOrAnonParticipant - className?: string + drawee?: IdentityPublicData; + drawer?: IdentityPublicData; + holder?: IdentOrAnonParticipant[]; + payee?: IdentOrAnonParticipant; + className?: string; }) { - const intl = useIntl() + const intl = useIntl(); const getRoleLabel = (role: "drawee" | "drawer" | "payee" | "holder") => { const defaults = { drawee: "Drawee", drawer: "Drawer", payee: "Payee", holder: "Holder", - } + }; return intl.formatMessage( { id: `participants.role.${role}`, defaultMessage: defaults[role], }, {}, - ) - } + ); + }; const bearerLabel = intl.formatMessage({ id: "participants.role.bearer", defaultMessage: "Bearer", - }) + }); - const getIdentTooltip = (data: IdentityPublicData | undefined, role: string) => { + const getIdentTooltip = ( + data: IdentityPublicData | undefined, + role: string, + ) => { if (!data) { - return role + return role; } return (
@@ -135,21 +181,26 @@ export function ParticipantsOverviewCard({ )}
{data.node_id}
- ) - } + ); + }; - const getIdentOrAnonTooltip = (data: IdentOrAnonParticipant | undefined, role: string) => { + const getIdentOrAnonTooltip = ( + data: IdentOrAnonParticipant | undefined, + role: string, + ) => { if (!data) { - return role + return role; } if ("Ident" in data) { - const identData = data.Ident + const identData = data.Ident; return (
{role}
{identData.name}
- {identData.email &&
{identData.email}
} + {identData.email && ( +
{identData.email}
+ )} {identData.city && identData.country && (
{identData.city}, {identData.country} @@ -157,65 +208,81 @@ export function ParticipantsOverviewCard({ )}
{identData.node_id}
- ) + ); } else if ("Anon" in data) { - const anonData = data.Anon + const anonData = data.Anon; return (
{role}
{bearerLabel}
- {anonData?.node_id &&
{anonData.node_id}
} + {anonData?.node_id && ( +
+ {anonData.node_id} +
+ )}
- ) + ); } - return role - } + return role; + }; return ( {drawee && (
- +
)} {drawer && (
- +
)} {payee && (
- +
)} {holder && holder.length > 0 && (
)}
- ) + ); } export function ParticipantDetail({ participant, }: { - participant: BillIdentParticipant | BillParticipant | undefined + participant: BillIdentParticipant | BillParticipant | undefined; }) { - const intl = useIntl() + const intl = useIntl(); if (!participant) { - return null + return null; } - let data: BillIdentParticipant | undefined - let avatar: React.ReactNode + let data: BillIdentParticipant | undefined; + let avatar: React.ReactNode; if ("Anon" in participant) { - const anonData = participant.Anon + const anonData = participant.Anon; return (
@@ -239,32 +306,50 @@ export function ParticipantDetail({ )}
- ) + ); } else if ("Ident" in participant) { - data = participant.Ident - avatar = + data = participant.Ident; + avatar = ; } else { - data = participant - avatar = + data = participant; + avatar = ; } if (!data) { - return null + return null; } return (
{avatar}
- + {data.email && ( - - + + )} {"city" in data && data.city && data.country && (
- +
)}
@@ -278,5 +363,5 @@ export function ParticipantDetail({
- ) + ); } diff --git a/src/components/QRCodeWithErrorBoundary.test.tsx b/src/components/QRCodeWithErrorBoundary.test.tsx index b3dc416..dacd940 100644 --- a/src/components/QRCodeWithErrorBoundary.test.tsx +++ b/src/components/QRCodeWithErrorBoundary.test.tsx @@ -1,106 +1,133 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { FeeTokenQRCodeModal, QRCode, QRCodeModal } from "./QRCodeWithErrorBoundary" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { + FeeTokenQRCodeModal, + QRCode, + QRCodeModal, +} from "./QRCodeWithErrorBoundary"; -const mockCanGenerateQRCode = vi.fn<(value: string) => boolean>() -const mockCanGenerateQRCodeAsync = vi.fn<(value: string) => Promise>() -const mockQrSvg = vi.fn<(props: { value: string }) => React.ReactNode>() +const mockCanGenerateQRCode = vi.fn<(value: string) => boolean>(); +const mockCanGenerateQRCodeAsync = vi.fn<(value: string) => Promise>(); +const mockQrSvg = vi.fn<(props: { value: string }) => React.ReactNode>(); vi.mock("qrcode.react", () => ({ QRCodeSVG: ({ value }: { value: string }) => mockQrSvg({ value }), -})) +})); vi.mock("@/utils/qrCodeUtils.ts", () => ({ QR_CODE_MAX_LENGTH: 4296, canGenerateQRCode: (value: string) => mockCanGenerateQRCode(value), canGenerateQRCodeAsync: (value: string) => mockCanGenerateQRCodeAsync(value), -})) +})); vi.mock("@/components/ui/drawer", () => ({ - Drawer: ({ children }: { children: React.ReactNode }) =>
{children}
, - DrawerTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, - DrawerContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - DrawerHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, - DrawerTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) + Drawer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DrawerTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DrawerContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DrawerHeader: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DrawerTitle: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderWithIntl(element: ReactElement): HTMLDivElement { - return renderIntoDom({element}) + return renderIntoDom({element}); } beforeEach(() => { - vi.clearAllMocks() - mockCanGenerateQRCode.mockReturnValue(true) - mockCanGenerateQRCodeAsync.mockResolvedValue(true) - mockQrSvg.mockImplementation(({ value }: { value: string }) => ) + vi.clearAllMocks(); + mockCanGenerateQRCode.mockReturnValue(true); + mockCanGenerateQRCodeAsync.mockResolvedValue(true); + mockQrSvg.mockImplementation(({ value }: { value: string }) => ( + + )); if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } -}) +}); describe("QRCodeWithErrorBoundary", () => { it("shows too-large warning when QR cannot be generated", () => { - mockCanGenerateQRCode.mockReturnValue(false) - const page = renderWithIntl() - expect(page.textContent).toContain("Data too large for QR code") - }) + mockCanGenerateQRCode.mockReturnValue(false); + const page = renderWithIntl(); + expect(page.textContent).toContain("Data too large for QR code"); + }); it("renders QRCode with label when generation is allowed", () => { - const page = renderWithIntl() - expect(page.querySelector('svg[data-value="hello-qr"]')).not.toBeNull() - expect(page.textContent).toContain("Scan me") - }) + const page = renderWithIntl( + , + ); + expect(page.querySelector('svg[data-value="hello-qr"]')).not.toBeNull(); + expect(page.textContent).toContain("Scan me"); + }); it("renders fallback when QR renderer throws", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); mockQrSvg.mockImplementation(() => { - throw new Error("render error") - }) + throw new Error("render error"); + }); - const page = renderWithIntl() - expect(page.textContent).toContain("QR code cannot be generated") - errorSpy.mockRestore() - }) + const page = renderWithIntl(); + expect(page.textContent).toContain("QR code cannot be generated"); + errorSpy.mockRestore(); + }); it("returns null for modal when async capability check fails", async () => { - mockCanGenerateQRCodeAsync.mockResolvedValue(false) - const page = renderWithIntl() + mockCanGenerateQRCodeAsync.mockResolvedValue(false); + const page = renderWithIntl(); await act(async () => { - await Promise.resolve() - }) - expect(page.textContent).toBe("") - }) + await Promise.resolve(); + }); + expect(page.textContent).toBe(""); + }); it("renders modal trigger and fee-token wrapper labels", async () => { - const page = renderWithIntl() + const page = renderWithIntl( + , + ); await act(async () => { - await Promise.resolve() - }) - const triggerButton = page.querySelector('button[aria-label="Show QR code for fee token"]') - expect(triggerButton).not.toBeNull() - expect(page.textContent).toContain("Fee Token QR Code") - }) -}) + await Promise.resolve(); + }); + const triggerButton = page.querySelector( + 'button[aria-label="Show QR code for fee token"]', + ); + expect(triggerButton).not.toBeNull(); + expect(page.textContent).toContain("Fee Token QR Code"); + }); +}); diff --git a/src/components/QRCodeWithErrorBoundary.tsx b/src/components/QRCodeWithErrorBoundary.tsx index d500a00..c2265d2 100644 --- a/src/components/QRCodeWithErrorBoundary.tsx +++ b/src/components/QRCodeWithErrorBoundary.tsx @@ -1,10 +1,20 @@ -import { Component, ReactNode, ErrorInfo, useEffect, useState } from "react" -import { QRCodeSVG } from "qrcode.react" -import { AlertTriangle, QrCode } from "lucide-react" -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer" -import { Button } from "@/components/ui/button" -import { canGenerateQRCode, canGenerateQRCodeAsync, QR_CODE_MAX_LENGTH } from "@/utils/qrCodeUtils.ts" -import { useIntl } from "react-intl" +import { Component, ReactNode, ErrorInfo, useEffect, useState } from "react"; +import { QRCodeSVG } from "qrcode.react"; +import { AlertTriangle, QrCode } from "lucide-react"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Button } from "@/components/ui/button"; +import { + canGenerateQRCode, + canGenerateQRCodeAsync, + QR_CODE_MAX_LENGTH, +} from "@/utils/qrCodeUtils.ts"; +import { useIntl } from "react-intl"; /** * Generic QR Code Components @@ -26,26 +36,29 @@ import { useIntl } from "react-intl" */ interface QRCodeErrorBoundaryProps { - children: ReactNode - fallbackMessage: string + children: ReactNode; + fallbackMessage: string; } interface QRCodeErrorBoundaryState { - hasError: boolean + hasError: boolean; } -class QRCodeErrorBoundary extends Component { +class QRCodeErrorBoundary extends Component< + QRCodeErrorBoundaryProps, + QRCodeErrorBoundaryState +> { constructor(props: QRCodeErrorBoundaryProps) { - super(props) - this.state = { hasError: false } + super(props); + this.state = { hasError: false }; } static getDerivedStateFromError(): QRCodeErrorBoundaryState { - return { hasError: true } + return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("QR Code Error:", error, errorInfo) + console.error("QR Code Error:", error, errorInfo); } render() { @@ -55,31 +68,31 @@ class QRCodeErrorBoundary extends Component {this.props.fallbackMessage}
- ) + ); } - return this.props.children + return this.props.children; } } interface QRCodeProps { - value: string - size?: number - label?: string - className?: string + value: string; + size?: number; + label?: string; + className?: string; } interface QRCodeModalProps extends QRCodeProps { - title?: string - triggerLabel?: string + title?: string; + triggerLabel?: string; } export function QRCode({ value, size = 200, label, className }: QRCodeProps) { - const intl = useIntl() + const intl = useIntl(); const errorFallback = intl.formatMessage({ id: "qrCode.error.generic", defaultMessage: "QR code cannot be generated (data too large)", - }) + }); if (!canGenerateQRCode(value)) { return ( @@ -89,60 +102,91 @@ export function QRCode({ value, size = 200, label, className }: QRCodeProps) { {intl.formatMessage( { id: "qrCode.error.tooLarge", - defaultMessage: "Data too large for QR code ({length} characters, max {max})", + defaultMessage: + "Data too large for QR code ({length} characters, max {max})", }, { length: value.length, max: QR_CODE_MAX_LENGTH }, )}
- ) + ); } return ( -
- - {label && {label}} +
+ + {label && ( + + {label} + + )}
- ) + ); } -export function QRCodeModal({ value, size = 768, label, title, triggerLabel }: QRCodeModalProps) { - const intl = useIntl() - const [open, setOpen] = useState(false) - const [canRender, setCanRender] = useState(false) - const resolvedTitle = title ?? intl.formatMessage({ id: "qrCode.modal.title", defaultMessage: "QR Code" }) +export function QRCodeModal({ + value, + size = 768, + label, + title, + triggerLabel, +}: QRCodeModalProps) { + const intl = useIntl(); + const [open, setOpen] = useState(false); + const [canRender, setCanRender] = useState(false); + const resolvedTitle = + title ?? + intl.formatMessage({ id: "qrCode.modal.title", defaultMessage: "QR Code" }); const resolvedTriggerLabel = - triggerLabel ?? intl.formatMessage({ id: "qrCode.modal.triggerLabel", defaultMessage: "Show QR code" }) + triggerLabel ?? + intl.formatMessage({ + id: "qrCode.modal.triggerLabel", + defaultMessage: "Show QR code", + }); const errorFallback = intl.formatMessage({ id: "qrCode.error.generic", defaultMessage: "QR code cannot be generated (data too large)", - }) + }); useEffect(() => { - let isActive = true + let isActive = true; void (async () => { - const result = await canGenerateQRCodeAsync(value) + const result = await canGenerateQRCodeAsync(value); if (isActive) { - setCanRender(result) + setCanRender(result); } - })() + })(); return () => { - isActive = false - } - }, [value]) + isActive = false; + }; + }, [value]); if (!canGenerateQRCode(value) || !canRender) { - return null + return null; } return ( - + - @@ -153,18 +197,33 @@ export function QRCodeModal({ value, size = 768, label, title, triggerLabel }: Q
- - {label && {label}} + + {label && ( + + {label} + + )}
- ) + ); } -export function FeeTokenQRCodeModal({ feeToken, size = 512 }: { feeToken: string; size?: number }) { - const intl = useIntl() +export function FeeTokenQRCodeModal({ + feeToken, + size = 512, +}: { + feeToken: string; + size?: number; +}) { + const intl = useIntl(); return ( - ) + ); } diff --git a/src/components/SortButtons.tsx b/src/components/SortButtons.tsx index 64c9268..1444593 100644 --- a/src/components/SortButtons.tsx +++ b/src/components/SortButtons.tsx @@ -1,31 +1,39 @@ -import { Button } from "@/components/ui/button" -import { ArrowUp, ArrowDown } from "lucide-react" -import { useIntl } from "react-intl" +import { Button } from "@/components/ui/button"; +import { ArrowUp, ArrowDown } from "lucide-react"; +import { useIntl } from "react-intl"; export interface SortConfig { - field: T - direction: "asc" | "desc" + field: T; + direction: "asc" | "desc"; } interface SortOption { - field: T - label: string + field: T; + label: string; } interface SortButtonsProps { - sortBy: string - onSortChange: (field: T) => void - options: SortOption[] + sortBy: string; + onSortChange: (field: T) => void; + options: SortOption[]; } -export function SortButtons({ sortBy, onSortChange, options }: SortButtonsProps) { - const intl = useIntl() +export function SortButtons({ + sortBy, + onSortChange, + options, +}: SortButtonsProps) { + const intl = useIntl(); const getSortIcon = (field: T) => { if (!sortBy.startsWith(field)) { - return null + return null; } - return sortBy.endsWith("asc") ? : - } + return sortBy.endsWith("asc") ? ( + + ) : ( + + ); + }; const getTitle = (field: T, label: string) => { if (sortBy.startsWith(field)) { @@ -43,7 +51,7 @@ export function SortButtons({ sortBy, onSortChange, options }: defaultMessage: "{label} Descending", }, { label }, - ) + ); } return intl.formatMessage( { @@ -51,8 +59,8 @@ export function SortButtons({ sortBy, onSortChange, options }: defaultMessage: "Sort by {label}", }, { label }, - ) - } + ); + }; return (
@@ -75,5 +83,5 @@ export function SortButtons({ sortBy, onSortChange, options }: ))}
- ) + ); } diff --git a/src/components/TruncatedTextPopover.test.tsx b/src/components/TruncatedTextPopover.test.tsx index e90c301..19b3b62 100644 --- a/src/components/TruncatedTextPopover.test.tsx +++ b/src/components/TruncatedTextPopover.test.tsx @@ -1,86 +1,104 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { TruncatedTextPopover } from "./TruncatedTextPopover" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { TruncatedTextPopover } from "./TruncatedTextPopover"; -const toastSuccess = vi.fn<(msg: string) => void>() -const toastError = vi.fn<(msg: string) => void>() +const toastSuccess = vi.fn<(msg: string) => void>(); +const toastError = vi.fn<(msg: string) => void>(); vi.mock("sonner", () => ({ toast: { success: (msg: string) => toastSuccess(msg), error: (msg: string) => toastError(msg), }, -})) +})); vi.mock("@/components/ui/popover", () => ({ - Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, - PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, - PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) - -let root: Root | null = null -let container: HTMLDivElement | null = null + Popover: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + PopoverTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + PopoverContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderWithIntl(element: ReactElement): HTMLDivElement { - return renderIntoDom({element}) + return renderIntoDom({element}); } beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() + vi.clearAllMocks(); + vi.useFakeTimers(); Object.defineProperty(document.documentElement, "clientWidth", { configurable: true, value: 1200, - }) + }); Object.defineProperty(window.navigator, "maxTouchPoints", { configurable: true, value: 0, - }) - window.matchMedia = vi.fn().mockReturnValue({ matches: false }) as unknown as typeof window.matchMedia + }); + window.matchMedia = vi + .fn() + .mockReturnValue({ matches: false }) as unknown as typeof window.matchMedia; if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } -}) +}); describe("TruncatedTextPopover", () => { it("renders plain text when no truncation is needed", () => { - const page = renderWithIntl() - expect(page.textContent).toContain("short text") - expect(page.querySelector("button")).toBeNull() - }) + const page = renderWithIntl( + , + ); + expect(page.textContent).toContain("short text"); + expect(page.querySelector("button")).toBeNull(); + }); it("renders truncated trigger and full content for long text", () => { Object.defineProperty(document.documentElement, "clientWidth", { configurable: true, value: 320, - }) - const longText = "12345678901234567890" - const page = renderWithIntl() - expect(page.textContent).toContain("123") - expect(page.textContent).toContain("…") - expect(page.textContent).toContain("890") - expect(page.textContent).toContain(longText) - }) + }); + const longText = "12345678901234567890"; + const page = renderWithIntl( + , + ); + expect(page.textContent).toContain("123"); + expect(page.textContent).toContain("…"); + expect(page.textContent).toContain("890"); + expect(page.textContent).toContain(longText); + }); it("copies text and shows success toast", async () => { Object.defineProperty(window.navigator, "clipboard", { @@ -88,22 +106,28 @@ describe("TruncatedTextPopover", () => { value: { writeText: vi.fn().mockResolvedValue(undefined), }, - }) - - const page = renderWithIntl() - const button = page.querySelector('button[title="Copy to clipboard"]') - expect(button).not.toBeNull() + }); + + const page = renderWithIntl( + , + ); + const button = page.querySelector('button[title="Copy to clipboard"]'); + expect(button).not.toBeNull(); await act(async () => { - button?.dispatchEvent(new MouseEvent("click", { bubbles: true })) - await Promise.resolve() - }) + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + }); - expect(toastSuccess).toHaveBeenCalled() + expect(toastSuccess).toHaveBeenCalled(); act(() => { - vi.runAllTimers() - }) - }) + vi.runAllTimers(); + }); + }); it("shows error toast when copy fails", async () => { Object.defineProperty(window.navigator, "clipboard", { @@ -111,18 +135,26 @@ describe("TruncatedTextPopover", () => { value: { writeText: vi.fn().mockRejectedValue(new Error("copy failed")), }, - }) - - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) - const page = renderWithIntl() - const button = page.querySelector('button[title="Copy to clipboard"]') + }); + + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + const page = renderWithIntl( + , + ); + const button = page.querySelector('button[title="Copy to clipboard"]'); await act(async () => { - button?.dispatchEvent(new MouseEvent("click", { bubbles: true })) - await Promise.resolve() - }) - - expect(toastError).toHaveBeenCalled() - errorSpy.mockRestore() - }) -}) + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + }); + + expect(toastError).toHaveBeenCalled(); + errorSpy.mockRestore(); + }); +}); diff --git a/src/components/TruncatedTextPopover.tsx b/src/components/TruncatedTextPopover.tsx index d825c24..a2b5cf9 100644 --- a/src/components/TruncatedTextPopover.tsx +++ b/src/components/TruncatedTextPopover.tsx @@ -1,89 +1,98 @@ -import * as React from "react" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { cn } from "@/lib/utils" -import { truncateString } from "@/utils/strings" -import { Copy, Check } from "lucide-react" -import { Button } from "@/components/ui/button" -import { toast } from "sonner" -import { useIntl } from "react-intl" +import * as React from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { truncateString } from "@/utils/strings"; +import { Copy, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { useIntl } from "react-intl"; interface TruncatedTextPopoverProps { - text: React.ReactNode - maxLength?: number - showFullOnDesktop?: boolean - className?: string - contentClassName?: string - title?: string - as?: "button" | "span" - showCopyButton?: boolean + text: React.ReactNode; + maxLength?: number; + showFullOnDesktop?: boolean; + className?: string; + contentClassName?: string; + title?: string; + as?: "button" | "span"; + showCopyButton?: boolean; } -function useResponsiveMaxLength(maxLength: number, showFullOnDesktop: boolean): number { - const [effectiveMaxLength, setEffectiveMaxLength] = React.useState(maxLength) +function useResponsiveMaxLength( + maxLength: number, + showFullOnDesktop: boolean, +): number { + const [effectiveMaxLength, setEffectiveMaxLength] = React.useState(maxLength); React.useEffect(() => { const calculateMaxLength = () => { // Use document.documentElement.clientWidth for more accurate width // This excludes scrollbar and respects the actual viewport - const width = document.documentElement.clientWidth || window.innerWidth + const width = document.documentElement.clientWidth || window.innerWidth; // Modern touch device detection using multiple signals const isTouchDevice = - window.matchMedia("(pointer: coarse)").matches || navigator.maxTouchPoints > 0 || "ontouchstart" in window + window.matchMedia("(pointer: coarse)").matches || + navigator.maxTouchPoints > 0 || + "ontouchstart" in window; // If showFullOnDesktop is true and we're on a large screen without touch, skip truncation if (showFullOnDesktop && width >= 1024 && !isTouchDevice) { - setEffectiveMaxLength(Infinity) - return + setEffectiveMaxLength(Infinity); + return; } // Define breakpoints and scaling factors based on viewport width // Touch devices get more aggressive truncation at larger widths if (width < 480) { // Extra small mobile: reduce to 30% of maxLength - setEffectiveMaxLength(Math.max(12, Math.floor(maxLength * 0.3))) + setEffectiveMaxLength(Math.max(12, Math.floor(maxLength * 0.3))); } else if (width < 768) { // Small mobile/tablet: reduce to 50% of maxLength - setEffectiveMaxLength(Math.max(16, Math.floor(maxLength * 0.5))) + setEffectiveMaxLength(Math.max(16, Math.floor(maxLength * 0.5))); } else if (width < 1024) { // Tablet/small desktop: reduce to 70% of maxLength - setEffectiveMaxLength(Math.max(16, Math.floor(maxLength * 0.7))) + setEffectiveMaxLength(Math.max(16, Math.floor(maxLength * 0.7))); } else if (width < 1440) { // Medium desktop: reduce to 90% of maxLength - setEffectiveMaxLength(Math.max(24, Math.floor(maxLength * 0.9))) + setEffectiveMaxLength(Math.max(24, Math.floor(maxLength * 0.9))); } else { // Large desktop: use full maxLength - setEffectiveMaxLength(maxLength) + setEffectiveMaxLength(maxLength); } - } + }; - calculateMaxLength() - window.addEventListener("resize", calculateMaxLength) - return () => window.removeEventListener("resize", calculateMaxLength) - }, [maxLength, showFullOnDesktop]) + calculateMaxLength(); + window.addEventListener("resize", calculateMaxLength); + return () => window.removeEventListener("resize", calculateMaxLength); + }, [maxLength, showFullOnDesktop]); - return effectiveMaxLength + return effectiveMaxLength; } function extractTextFromNode(node: unknown): string { if (node == null) { - return "" + return ""; } if (typeof node === "string" || typeof node === "number") { - return String(node) + return String(node); } if (Array.isArray(node)) { - return node.map((n) => extractTextFromNode(n)).join("") + return node.map((n) => extractTextFromNode(n)).join(""); } if (React.isValidElement(node)) { - const el = node as React.ReactElement<{ children?: React.ReactNode }> - return extractTextFromNode(el.props.children) + const el = node as React.ReactElement<{ children?: React.ReactNode }>; + return extractTextFromNode(el.props.children); } - return "" + return ""; } export function TruncatedTextPopover({ @@ -96,48 +105,58 @@ export function TruncatedTextPopover({ as = "button", showCopyButton = false, }: TruncatedTextPopoverProps) { - const intl = useIntl() - const effectiveMaxLength = useResponsiveMaxLength(maxLength, showFullOnDesktop) - const [copied, setCopied] = React.useState(false) - - const textStr = extractTextFromNode(text) - const rawLines = textStr.split(/\r?\n/) - const lines = rawLines.map((l) => l.replace(/\s+$/g, "")) - const needsTruncation = lines.some((line) => line.length > effectiveMaxLength) + const intl = useIntl(); + const effectiveMaxLength = useResponsiveMaxLength( + maxLength, + showFullOnDesktop, + ); + const [copied, setCopied] = React.useState(false); + + const textStr = extractTextFromNode(text); + const rawLines = textStr.split(/\r?\n/); + const lines = rawLines.map((l) => l.replace(/\s+$/g, "")); + const needsTruncation = lines.some( + (line) => line.length > effectiveMaxLength, + ); const handleCopy = async () => { try { - await navigator.clipboard.writeText(textStr) - setCopied(true) + await navigator.clipboard.writeText(textStr); + setCopied(true); toast.success( intl.formatMessage({ id: "truncatedTextPopover.copied", defaultMessage: "Copied to clipboard", }), - ) - setTimeout(() => setCopied(false), 2000) + ); + setTimeout(() => setCopied(false), 2000); } catch (err) { - console.error("Failed to copy text:", err) + console.error("Failed to copy text:", err); toast.error( intl.formatMessage({ id: "truncatedTextPopover.copyFailed", defaultMessage: "Failed to copy to clipboard", }), - ) + ); } - } - const flatLabel = lines.join(", ") - const truncatedLines = needsTruncation ? lines.map((line) => truncateString(line, effectiveMaxLength)) : lines + }; + const flatLabel = lines.join(", "); + const truncatedLines = needsTruncation + ? lines.map((line) => truncateString(line, effectiveMaxLength)) + : lines; if (!needsTruncation && !showCopyButton) { return ( - + {text} - ) + ); } - const TriggerTag = as + const TriggerTag = as; const popoverContent = ( @@ -154,7 +173,10 @@ export function TruncatedTextPopover({ > {needsTruncation ? ( truncatedLines.map((line, i) => ( - + {line} )) @@ -176,7 +198,7 @@ export function TruncatedTextPopover({ {textStr} - ) + ); if (showCopyButton) { return ( @@ -211,11 +233,15 @@ export function TruncatedTextPopover({ }) } > - {copied ? : } + {copied ? ( + + ) : ( + + )}
- ) + ); } - return popoverContent + return popoverContent; } diff --git a/src/components/icons/UserAnonymous.tsx b/src/components/icons/UserAnonymous.tsx index 339dd84..2ea8e8c 100644 --- a/src/components/icons/UserAnonymous.tsx +++ b/src/components/icons/UserAnonymous.tsx @@ -1,7 +1,7 @@ -import { useIntl } from "react-intl" +import { useIntl } from "react-intl"; export function UserAnonymousIcon({ className }: { className?: string }) { - const intl = useIntl() + const intl = useIntl(); return ( - ) + ); } diff --git a/src/components/nav/NavMain.tsx b/src/components/nav/NavMain.tsx index b67460c..eea8781 100644 --- a/src/components/nav/NavMain.tsx +++ b/src/components/nav/NavMain.tsx @@ -1,7 +1,11 @@ -import { ChevronRight, type LucideIcon } from "lucide-react" -import { NavLink } from "react-router" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" -import { useIntl } from "react-intl" +import { ChevronRight, type LucideIcon } from "lucide-react"; +import { NavLink } from "react-router"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { useIntl } from "react-intl"; import { SidebarGroup, SidebarGroupLabel, @@ -12,40 +16,52 @@ import { SidebarMenuSubButton, SidebarMenuSubItem, useSidebar, -} from "@/components/ui/sidebar" -import { cn } from "@/lib/utils" +} from "@/components/ui/sidebar"; +import { cn } from "@/lib/utils"; export function NavMain({ items, }: { items: { - titleId: string - titleDefaultMessage: string - url: string - icon?: LucideIcon - isActive?: boolean - disabled?: boolean + titleId: string; + titleDefaultMessage: string; + url: string; + icon?: LucideIcon; + isActive?: boolean; + disabled?: boolean; items?: { - titleId: string - titleDefaultMessage: string - url: string - disabled?: boolean - }[] - }[] + titleId: string; + titleDefaultMessage: string; + url: string; + disabled?: boolean; + }[]; + }[]; }) { - const { state } = useSidebar() - const intl = useIntl() + const { state } = useSidebar(); + const intl = useIntl(); return ( - {intl.formatMessage({ id: "nav.dashboard", defaultMessage: "Dashboard" })} + + {intl.formatMessage({ + id: "nav.dashboard", + defaultMessage: "Dashboard", + })} + {items.map((item) => { - const title = intl.formatMessage({ id: item.titleId, defaultMessage: item.titleDefaultMessage }) + const title = intl.formatMessage({ + id: item.titleId, + defaultMessage: item.titleDefaultMessage, + }); return (item.items ?? []).length === 0 || state === "collapsed" ? ( - + {item.disabled === true ? ( <> {item.icon && } @@ -60,10 +76,19 @@ export function NavMain({ ) : ( - +
- + {item.icon && } {title} @@ -73,7 +98,10 @@ export function NavMain({
- ) + ); } const getMonthLabels = (intl: ReturnType) => [ @@ -39,18 +44,21 @@ const getMonthLabels = (intl: ReturnType) => [ intl.formatMessage({ id: "month.oct.short", defaultMessage: "Oct" }), intl.formatMessage({ id: "month.nov.short", defaultMessage: "Nov" }), intl.formatMessage({ id: "month.dec.short", defaultMessage: "Dec" }), -] +]; export function BitcoinBalanceChart() { - const intl = useIntl() + const intl = useIntl(); const config = { bitcoin: { - label: intl.formatMessage({ id: "balances.chart.bitcoin", defaultMessage: "Bitcoin" }), + label: intl.formatMessage({ + id: "balances.chart.bitcoin", + defaultMessage: "Bitcoin", + }), color: "#2563eb", }, - } satisfies ChartConfig + } satisfies ChartConfig; - const months = getMonthLabels(intl) + const months = getMonthLabels(intl); const data = [ { month: months[0], bitcoin: 186 }, @@ -65,11 +73,17 @@ export function BitcoinBalanceChart() { { month: months[9], bitcoin: 0 }, { month: months[10], bitcoin: 0 }, { month: months[11], bitcoin: 0 }, - ] + ]; return ( - - + + value} /> - - + + } /> - ) + ); } export function OtherBalanceChart() { - const intl = useIntl() + const intl = useIntl(); const config = { eIOU: { - label: intl.formatMessage({ id: "balances.chart.eiou", defaultMessage: "e-IOU" }), + label: intl.formatMessage({ + id: "balances.chart.eiou", + defaultMessage: "e-IOU", + }), color: "#911198", }, credit: { - label: intl.formatMessage({ id: "balances.chart.creditToken", defaultMessage: "Credit token" }), + label: intl.formatMessage({ + id: "balances.chart.creditToken", + defaultMessage: "Credit token", + }), color: "#e9d4ff", }, debit: { - label: intl.formatMessage({ id: "balances.chart.debitToken", defaultMessage: "Debit token" }), + label: intl.formatMessage({ + id: "balances.chart.debitToken", + defaultMessage: "Debit token", + }), color: "#c27aff", }, - } satisfies ChartConfig + } satisfies ChartConfig; - const months = getMonthLabels(intl) + const months = getMonthLabels(intl); const data = [ { month: months[0], credit: 121, debit: 0 }, @@ -118,11 +150,17 @@ export function OtherBalanceChart() { { month: months[9], credit: 1782, debit: 1232 }, { month: months[10], credit: 0, debit: 0 }, { month: months[11], credit: 0, debit: 0 }, - ] + ]; return ( - - + + value} /> - - - - + + + + } /> - ) + ); } interface BalanceDisplay { - amount: string - unit: string + amount: string; + unit: string; } -export function BalanceText({ amount, unit, children }: PropsWithChildren) { +export function BalanceText({ + amount, + unit, + children, +}: PropsWithChildren) { return ( <>

@@ -154,7 +214,7 @@ export function BalanceText({ amount, unit, children }: PropsWithChildren {children} - ) + ); } function useBalances() { @@ -167,9 +227,9 @@ function useBalances() { refetchInterval: 30_000, staleTime: 25_000, retry: 2, - }) + }); - const error = isError ? "Failed to load coverage data" : null + const error = isError ? "Failed to load coverage data" : null; const balances: Record = { bitcoin: { @@ -188,13 +248,13 @@ function useBalances() { amount: coverage?.debit_circulating_supply?.toString() ?? "0", unit: "sat", }, - } + }; - return { balances, error, refetch } + return { balances, error, refetch }; } function PageBodyWithDevSection() { - const { balances, error } = useBalances() + const { balances, error } = useBalances(); if (error) { return ( @@ -213,7 +273,7 @@ function PageBodyWithDevSection() { - ) + ); } return ( @@ -223,41 +283,65 @@ function PageBodyWithDevSection() { - + - + - + - + - + - + - + - + @@ -277,22 +361,28 @@ function PageBodyWithDevSection() { */} - ) + ); } export default function BalancesPage() { return ( <> - + - + }> - ) + ); } diff --git a/src/pages/balances/CashFlowPage.tsx b/src/pages/balances/CashFlowPage.tsx index 5dc342a..0ee74ce 100644 --- a/src/pages/balances/CashFlowPage.tsx +++ b/src/pages/balances/CashFlowPage.tsx @@ -1,11 +1,23 @@ -import { Suspense } from "react" -import { Link } from "react-router" -import { CartesianGrid, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts" -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Skeleton } from "@/components/ui/skeleton" -import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent } from "@/components/ui/chart" -import { FormattedMessage, useIntl } from "react-intl" +import { Suspense } from "react"; +import { Link } from "react-router"; +import { + CartesianGrid, + Line, + LineChart, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, +} from "@/components/ui/chart"; +import { FormattedMessage, useIntl } from "react-intl"; function Loader() { return ( @@ -15,17 +27,20 @@ function Loader() { - ) + ); } function CashFlowChart() { - const intl = useIntl() + const intl = useIntl(); const config = { bitcoin: { - label: intl.formatMessage({ id: "balances.chart.bitcoin", defaultMessage: "Bitcoin" }), + label: intl.formatMessage({ + id: "balances.chart.bitcoin", + defaultMessage: "Bitcoin", + }), color: "#2563eb", }, - } satisfies ChartConfig + } satisfies ChartConfig; const months = [ intl.formatMessage({ id: "month.jan.short", defaultMessage: "Jan" }), @@ -40,7 +55,7 @@ function CashFlowChart() { intl.formatMessage({ id: "month.oct.short", defaultMessage: "Oct" }), intl.formatMessage({ id: "month.nov.short", defaultMessage: "Nov" }), intl.formatMessage({ id: "month.dec.short", defaultMessage: "Dec" }), - ] + ]; const data = [ { month: months[0], bitcoin: 186 }, @@ -55,10 +70,13 @@ function CashFlowChart() { { month: months[9], bitcoin: 0 }, { month: months[10], bitcoin: 0 }, { month: months[11], bitcoin: 0 }, - ] + ]; return ( - + value} /> - - - + + + } /> - ) + ); } function CashFlow() { @@ -91,7 +122,7 @@ function CashFlow() {
- ) + ); } function PageBody() { @@ -99,7 +130,7 @@ function PageBody() {
- ) + ); } export default function CashFlowPage() { @@ -109,20 +140,29 @@ export default function CashFlowPage() { parents={[ <> - + , ]} > - + - + }> - ) + ); } diff --git a/src/pages/balances/EarningsPage.tsx b/src/pages/balances/EarningsPage.tsx index 71110ae..48e13c7 100644 --- a/src/pages/balances/EarningsPage.tsx +++ b/src/pages/balances/EarningsPage.tsx @@ -1,12 +1,12 @@ -import { Suspense, useState } from "react" -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Skeleton } from "@/components/ui/skeleton" -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" -import { Link } from "react-router" -import { Button } from "@/components/ui/button" -import { ChartColumnIncreasingIcon } from "lucide-react" -import { FormattedMessage } from "react-intl" +import { Suspense, useState } from "react"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { Link } from "react-router"; +import { Button } from "@/components/ui/button"; +import { ChartColumnIncreasingIcon } from "lucide-react"; +import { FormattedMessage } from "react-intl"; function Loader() { return ( @@ -16,21 +16,26 @@ function Loader() { - ) + ); } function Earnings() { - const [timeframe, setTimeframe] = useState("1d") + const [timeframe, setTimeframe] = useState("1d"); return (
-
0.00 000 000 BTC
+
+ 0.00 000 000 BTC +
- +
@@ -43,35 +48,74 @@ function Earnings() { value={timeframe} onValueChange={(val) => setTimeframe((curr) => val || curr)} > - - + + - - + + - - + + - - + + - - + + - - + +
- +
- ) + ); } function PageBody() { @@ -80,27 +124,36 @@ function PageBody() { - ) + ); } export default function EarningsPage() { return ( <> - + - + }> - ) + ); } diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index d007f4b..b18b5a6 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -1,30 +1,30 @@ -import { PageTitle } from "@/components/PageTitle" -import { Skeleton } from "@/components/ui/skeleton" -import { useQuery } from "@tanstack/react-query" -import { Suspense } from "react" -import { CopyButton } from "@/components/CopyButton" +import { PageTitle } from "@/components/PageTitle"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useQuery } from "@tanstack/react-query"; +import { Suspense } from "react"; +import { CopyButton } from "@/components/CopyButton"; import { getIdentityOptions, getClowderInfoOptions, getMintInfoOptions, -} from "@/generated/client/@tanstack/react-query.gen" -import { FormattedMessage, useIntl } from "react-intl" +} from "@/generated/client/@tanstack/react-query.gen"; +import { FormattedMessage, useIntl } from "react-intl"; function Loader() { return (
- ) + ); } function PageBody() { - const intl = useIntl() + const intl = useIntl(); const { data: identityData } = useQuery({ ...getIdentityOptions(), staleTime: Infinity, gcTime: Infinity, - }) + }); const { data: mintData, @@ -33,7 +33,7 @@ function PageBody() { } = useQuery({ ...getMintInfoOptions(), staleTime: 60_000, - }) + }); const { data: clowderData, @@ -42,45 +42,67 @@ function PageBody() { } = useQuery({ ...getClowderInfoOptions(), staleTime: 60_000, - }) + }); return (

- +

{identityData ? (
- + + + + {identityData.name} - {identityData.name}
{identityData.email && (
- + + + + {identityData.email} - {identityData.email}
)}

- +

- +
@@ -90,7 +112,10 @@ function PageBody() {
- +
- +

- +

-
{identityData.postal_address.address}
+
+ {identityData.postal_address.address} +
{identityData.postal_address.city} - {identityData.postal_address.zip && `, ${identityData.postal_address.zip}`} + {identityData.postal_address.zip && + `, ${identityData.postal_address.zip}`} +
+
+ {identityData.postal_address.country}
-
{identityData.postal_address.country}
@@ -144,33 +180,51 @@ function PageBody() {
) : (
- +
)}

- +

{clowderLoading ? (
- +
) : clowderError ? (
- +
) : clowderData ? (
- +
@@ -180,26 +234,40 @@ function PageBody() {
- + {clowderData.version}
- + + + + {clowderData.network} - {clowderData.network}
- +
@@ -210,38 +278,58 @@ function PageBody() { {clowderData.uptime_timestamp && (
- + - {new Date(clowderData.uptime_timestamp * 1000).toLocaleString(undefined, { timeZone: "UTC" })} + {new Date( + clowderData.uptime_timestamp * 1000, + ).toLocaleString(undefined, { timeZone: "UTC" })}
)}
) : (
- +
)}

- +

{mintLoading ? (
- +
) : mintError ? (
- +
) : mintData ? (
- + {mintData.network}
@@ -249,7 +337,10 @@ function PageBody() {
- +
- +

- +

- + + + + {mintData.versions.wildcat} - {mintData.versions.wildcat}
- + + + + {mintData.versions.bcr_ebill_core} - {mintData.versions.bcr_ebill_core}
- + + + + {mintData.versions.cdk_mintd} - {mintData.versions.cdk_mintd}
- + + + + {mintData.versions.clowder} - {mintData.versions.clowder}
@@ -318,19 +435,31 @@ function PageBody() {
- + - {new Date(mintData.uptime_timestamp).toLocaleString(undefined, { timeZone: "UTC" })} + {new Date(mintData.uptime_timestamp).toLocaleString( + undefined, + { timeZone: "UTC" }, + )}
{mintData.build_time && (
- + - {new Date(mintData.build_time).toLocaleString(undefined, { timeZone: "UTC" })} + {new Date(mintData.build_time).toLocaleString( + undefined, + { timeZone: "UTC" }, + )}
)} @@ -339,24 +468,30 @@ function PageBody() {
) : (
- +
)}
- ) + ); } export default function HomePage() { return ( <> - + }> - ) + ); } diff --git a/src/pages/info/InfoPage.tsx b/src/pages/info/InfoPage.tsx index e303c3b..0fa30b5 100644 --- a/src/pages/info/InfoPage.tsx +++ b/src/pages/info/InfoPage.tsx @@ -1,24 +1,24 @@ -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Skeleton } from "@/components/ui/skeleton" -import { fetchInfo } from "@/lib/api" -import { useSuspenseQuery } from "@tanstack/react-query" -import { Suspense } from "react" -import { FormattedMessage } from "react-intl" +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Skeleton } from "@/components/ui/skeleton"; +import { fetchInfo } from "@/lib/api"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { Suspense } from "react"; +import { FormattedMessage } from "react-intl"; function Loader() { return (
- ) + ); } function PageBody() { const { data } = useSuspenseQuery({ queryKey: ["info"], queryFn: fetchInfo, - }) + }); return ( <> @@ -26,21 +26,27 @@ function PageBody() { {JSON.stringify(data, null, 2)} - ) + ); } export default function InfoPage() { return ( <> - + - + }> - ) + ); } diff --git a/src/pages/keysets/KeysetDetailPage.test.tsx b/src/pages/keysets/KeysetDetailPage.test.tsx index 981ae92..0f3b934 100644 --- a/src/pages/keysets/KeysetDetailPage.test.tsx +++ b/src/pages/keysets/KeysetDetailPage.test.tsx @@ -1,82 +1,87 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { MemoryRouter, Route, Routes } from "react-router" -import KeysetDetailPage from "./KeysetDetailPage" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { MemoryRouter, Route, Routes } from "react-router"; +import KeysetDetailPage from "./KeysetDetailPage"; interface QueryKeyEntry { - _id: string + _id: string; } interface QueryOptions { - queryKey: QueryKeyEntry[] + queryKey: QueryKeyEntry[]; } interface QueryResult { - data: unknown - isLoading: boolean + data: unknown; + isLoading: boolean; } interface UseQueriesArgs { - queries: { queryKey?: { _id?: string }[] }[] + queries: { queryKey?: { _id?: string }[] }[]; } interface UseQueriesResultItem { - isLoading: boolean - data?: { bill?: { id?: string; maturity_date?: string }; complete?: boolean } + isLoading: boolean; + data?: { bill?: { id?: string; maturity_date?: string }; complete?: boolean }; } interface MutationResult { - mutate: (value: { body: { kid: string } }) => void - isPending: boolean + mutate: (value: { body: { kid: string } }) => void; + isPending: boolean; } -const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>() -const mockUseQueries = vi.fn<(args: UseQueriesArgs) => UseQueriesResultItem[]>() -const mockUseMutation = vi.fn<() => MutationResult>() -const mutateSpy = vi.fn<(value: { body: { kid: string } }) => void>() +const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>(); +const mockUseQueries = + vi.fn<(args: UseQueriesArgs) => UseQueriesResultItem[]>(); +const mockUseMutation = vi.fn<() => MutationResult>(); +const mutateSpy = vi.fn<(value: { body: { kid: string } }) => void>(); vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() }, -})) +})); vi.mock("@tanstack/react-query", async () => { - const actual = await vi.importActual("@tanstack/react-query") + const actual = await vi.importActual( + "@tanstack/react-query", + ); return { ...actual, useQuery: (options: QueryOptions) => mockUseQuery(options), useQueries: (args: UseQueriesArgs) => mockUseQueries(args), useMutation: () => mockUseMutation(), useQueryClient: () => ({ invalidateQueries: vi.fn() }), - } -}) + }; +}); vi.mock("@/generated/client/@tanstack/react-query.gen", () => ({ listKeysetInfosOptions: () => ({ queryKey: [{ _id: "listKeysetInfos" }] }), listQuotesOptions: () => ({ queryKey: [{ _id: "listQuotes" }] }), listEbillsOptions: () => ({ queryKey: [{ _id: "listEbills" }] }), - getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ queryKey: [{ _id: "getQuote", path }] }), + getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ + queryKey: [{ _id: "getQuote", path }], + }), getEbillMintCompleteOptions: ({ path }: { path: { bid: string } }) => ({ queryKey: [{ _id: "getEbillMintComplete", path }], }), postEnableRedemptionMutation: () => ({ mutationFn: vi.fn() }), listKeysetInfosQueryKey: () => [{ _id: "listKeysetInfos" }], -})) +})); -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderPage(route: string): HTMLDivElement { @@ -84,86 +89,112 @@ function renderPage(route: string): HTMLDivElement { - } /> - } /> + } + /> + } + /> , - ) + ); } beforeEach(() => { - vi.clearAllMocks() + vi.clearAllMocks(); if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } - mockUseMutation.mockReturnValue({ mutate: mutateSpy, isPending: false }) + mockUseMutation.mockReturnValue({ mutate: mutateSpy, isPending: false }); mockUseQueries.mockImplementation(({ queries }: UseQueriesArgs) => { - const id = queries[0]?.queryKey?.[0]?._id + const id = queries[0]?.queryKey?.[0]?._id; if (id === "getQuote") { - return [{ isLoading: false, data: { bill: { id: "bill-1", maturity_date: "2026-02-20" } } }] + return [ + { + isLoading: false, + data: { bill: { id: "bill-1", maturity_date: "2026-02-20" } }, + }, + ]; } if (id === "getEbillMintComplete") { - return [{ isLoading: false, data: { complete: true } }] + return [{ isLoading: false, data: { complete: true } }]; } - return [] - }) -}) + return []; + }); +}); describe("KeysetDetailPage", () => { it("shows invalid keyset id when route has no :keysetId", () => { - const page = renderPage("/keysets") - expect(page.textContent).toContain("Invalid keyset ID") - }) + const page = renderPage("/keysets"); + expect(page.textContent).toContain("Invalid keyset ID"); + }); it("shows not found when keyset does not exist", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { - const id = opts.queryKey[0]._id + const id = opts.queryKey[0]._id; if (id === "listKeysetInfos") { - return { data: [{ id: "other-keyset" }], isLoading: false } + return { data: [{ id: "other-keyset" }], isLoading: false }; } if (id === "listQuotes") { - return { data: { quotes: [] }, isLoading: false } + return { data: { quotes: [] }, isLoading: false }; } if (id === "listEbills") { - return { data: [], isLoading: false } + return { data: [], isLoading: false }; } - return { data: undefined, isLoading: false } - }) + return { data: undefined, isLoading: false }; + }); - const page = renderPage("/keysets/target-keyset") - expect(page.textContent).toContain("Keyset not found") - }) + const page = renderPage("/keysets/target-keyset"); + expect(page.textContent).toContain("Keyset not found"); + }); it("enables redemption and calls mutation when Redeem is clicked", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { - const id = opts.queryKey[0]._id + const id = opts.queryKey[0]._id; if (id === "listKeysetInfos") { return { - data: [{ id: "keyset-1", active: true, final_expiry: 1771545600, unit: "sat" }], + data: [ + { + id: "keyset-1", + active: true, + final_expiry: 1771545600, + unit: "sat", + }, + ], isLoading: false, - } + }; } if (id === "listQuotes") { - return { data: { quotes: [{ id: "quote-1", status: "Pending", sum: 100 }] }, isLoading: false } + return { + data: { quotes: [{ id: "quote-1", status: "Pending", sum: 100 }] }, + isLoading: false, + }; } if (id === "listEbills") { - return { data: [{ id: "bill-1", status: { payment: { paid: true } } }], isLoading: false } + return { + data: [{ id: "bill-1", status: { payment: { paid: true } } }], + isLoading: false, + }; } - return { data: undefined, isLoading: false } - }) - - const page = renderPage("/keysets/keyset-1") - const redeemButton = Array.from(page.querySelectorAll("button")).find((button) => button.textContent === "Redeem") - expect(redeemButton?.disabled).toBe(false) + return { data: undefined, isLoading: false }; + }); + + const page = renderPage("/keysets/keyset-1"); + const redeemButton = Array.from(page.querySelectorAll("button")).find( + (button) => button.textContent === "Redeem", + ); + expect(redeemButton?.disabled).toBe(false); act(() => { - redeemButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })) - }) - expect(mutateSpy).toHaveBeenCalledWith({ body: { kid: "keyset-1" } }) - }) -}) + redeemButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + expect(mutateSpy).toHaveBeenCalledWith({ body: { kid: "keyset-1" } }); + }); +}); diff --git a/src/pages/keysets/KeysetDetailPage.tsx b/src/pages/keysets/KeysetDetailPage.tsx index f61f1f4..9d0f03a 100644 --- a/src/pages/keysets/KeysetDetailPage.tsx +++ b/src/pages/keysets/KeysetDetailPage.tsx @@ -1,7 +1,12 @@ -import { PageTitle } from "@/components/PageTitle" -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { useParams, Link, useLocation } from "react-router" -import { useQuery, useQueries, useMutation, useQueryClient } from "@tanstack/react-query" +import { PageTitle } from "@/components/PageTitle"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { useParams, Link, useLocation } from "react-router"; +import { + useQuery, + useQueries, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; import { listKeysetInfosOptions, listKeysetInfosQueryKey, @@ -10,18 +15,24 @@ import { listEbillsOptions, postEnableRedemptionMutation, getEbillMintCompleteOptions, -} from "@/generated/client/@tanstack/react-query.gen" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { ArrowRight } from "lucide-react" -import { BreadcrumbLink } from "@/components/ui/breadcrumb" -import { truncateString, formatStatusLabel } from "@/utils/strings" -import { getQuoteStatusVariant } from "@/utils/quote-status" -import { toast } from "sonner" -import { useMemo } from "react" -import { FormattedMessage, useIntl } from "react-intl" +} from "@/generated/client/@tanstack/react-query.gen"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ArrowRight } from "lucide-react"; +import { BreadcrumbLink } from "@/components/ui/breadcrumb"; +import { truncateString, formatStatusLabel } from "@/utils/strings"; +import { getQuoteStatusVariant } from "@/utils/quote-status"; +import { toast } from "sonner"; +import { useMemo } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; /** * Check if a bill's maturity date matches a keyset's final expiry date @@ -29,19 +40,22 @@ import { FormattedMessage, useIntl } from "react-intl" * @param billMaturityDate - Bill maturity date string (YYYY-MM-DD) * @returns true if dates match (year, month, day) */ -function doesBillMatchKeysetMaturity(keysetFinalExpiry: number, billMaturityDate: string): boolean { - const keysetDate = new Date(keysetFinalExpiry * 1000) - const billDate = new Date(billMaturityDate) +function doesBillMatchKeysetMaturity( + keysetFinalExpiry: number, + billMaturityDate: string, +): boolean { + const keysetDate = new Date(keysetFinalExpiry * 1000); + const billDate = new Date(billMaturityDate); return ( keysetDate.getFullYear() === billDate.getFullYear() && keysetDate.getMonth() === billDate.getMonth() && keysetDate.getDate() === billDate.getDate() - ) + ); } interface LocationState { - from?: string + from?: string; } function Loader() { @@ -50,18 +64,24 @@ function Loader() {
- ) + ); } function PageBody({ keysetId }: { keysetId: string }) { - const intl = useIntl() - const queryClient = useQueryClient() - const { data: keysets, isLoading: keysetsLoading } = useQuery(listKeysetInfosOptions()) - const { data: allQuotesData, isLoading: quotesLoading } = useQuery(listQuotesOptions()) - const allQuotes = useMemo(() => allQuotesData?.quotes ?? [], [allQuotesData?.quotes]) - const { data: ebills } = useQuery(listEbillsOptions()) - - const keyset = keysets?.find((k) => k.id === keysetId) + const intl = useIntl(); + const queryClient = useQueryClient(); + const { data: keysets, isLoading: keysetsLoading } = useQuery( + listKeysetInfosOptions(), + ); + const { data: allQuotesData, isLoading: quotesLoading } = + useQuery(listQuotesOptions()); + const allQuotes = useMemo( + () => allQuotesData?.quotes ?? [], + [allQuotesData?.quotes], + ); + const { data: ebills } = useQuery(listEbillsOptions()); + + const keyset = keysets?.find((k) => k.id === keysetId); const redemptionMutation = useMutation({ ...postEnableRedemptionMutation(), @@ -71,14 +91,14 @@ function PageBody({ keysetId }: { keysetId: string }) { id: "keyset.detail.redeem.success", defaultMessage: "Redemption enabled successfully", }), - ) + ); void queryClient.invalidateQueries({ queryKey: listKeysetInfosQueryKey(), exact: false, - }) + }); }, onError: (error) => { - const message = error instanceof Error ? error.message : String(error) + const message = error instanceof Error ? error.message : String(error); toast.error( intl.formatMessage( { @@ -87,9 +107,9 @@ function PageBody({ keysetId }: { keysetId: string }) { }, { error: message }, ), - ) + ); }, - }) + }); const quoteDetailsQueries = useQueries({ queries: allQuotes.map((quote) => @@ -97,82 +117,94 @@ function PageBody({ keysetId }: { keysetId: string }) { path: { qid: quote.id }, }), ), - }) + }); - const quoteDetailsLoading = quoteDetailsQueries.some((q) => q.isLoading) + const quoteDetailsLoading = quoteDetailsQueries.some((q) => q.isLoading); const quoteDetailsDepsKey = useMemo(() => { const billKeys = quoteDetailsQueries.map((query) => { - const billId = query.data?.bill?.id ?? "" - const maturityDate = query.data?.bill?.maturity_date ?? "" - return `${billId}|${maturityDate}` - }) - return billKeys.join(",") - }, [quoteDetailsQueries]) + const billId = query.data?.bill?.id ?? ""; + const maturityDate = query.data?.bill?.maturity_date ?? ""; + return `${billId}|${maturityDate}`; + }); + return billKeys.join(","); + }, [quoteDetailsQueries]); const matchingBillIds = useMemo(() => { - const billIds: string[] = [] + const billIds: string[] = []; if (!keyset?.final_expiry || quoteDetailsLoading) { - return billIds + return billIds; } - const keysetFinalExpiry = keyset.final_expiry + const keysetFinalExpiry = keyset.final_expiry; allQuotes.forEach((_quote, index) => { - const quoteDetails = quoteDetailsQueries[index]?.data - const billMaturityDate = quoteDetails?.bill?.maturity_date - const billId = quoteDetails?.bill?.id + const quoteDetails = quoteDetailsQueries[index]?.data; + const billMaturityDate = quoteDetails?.bill?.maturity_date; + const billId = quoteDetails?.bill?.id; if (!billMaturityDate || !billId) { - return + return; } if (doesBillMatchKeysetMaturity(keysetFinalExpiry, billMaturityDate)) { - billIds.push(billId) + billIds.push(billId); } - }) + }); - return billIds + return billIds; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [keyset?.final_expiry, allQuotes, quoteDetailsDepsKey, quoteDetailsLoading]) + }, [ + keyset?.final_expiry, + allQuotes, + quoteDetailsDepsKey, + quoteDetailsLoading, + ]); - const MINT_COMPLETE_POLL_INTERVAL_MS = 60_000 - const MINT_COMPLETE_RETRY_COUNT = 3 - const MINT_COMPLETE_RETRY_DELAY_MS = 30_000 + const MINT_COMPLETE_POLL_INTERVAL_MS = 60_000; + const MINT_COMPLETE_RETRY_COUNT = 3; + const MINT_COMPLETE_RETRY_DELAY_MS = 30_000; const mintCompleteQueries = useQueries({ queries: matchingBillIds.map((billId) => ({ ...getEbillMintCompleteOptions({ path: { bid: billId }, }), - refetchInterval: (query: { state: { data?: { complete?: boolean }; error?: unknown } }) => { - if (query.state.error) return false - return query.state.data?.complete === false ? MINT_COMPLETE_POLL_INTERVAL_MS : false + refetchInterval: (query: { + state: { data?: { complete?: boolean }; error?: unknown }; + }) => { + if (query.state.error) return false; + return query.state.data?.complete === false + ? MINT_COMPLETE_POLL_INTERVAL_MS + : false; }, retry: MINT_COMPLETE_RETRY_COUNT, retryDelay: MINT_COMPLETE_RETRY_DELAY_MS, refetchOnWindowFocus: false, })), - }) + }); const allBillsPaid = matchingBillIds.length > 0 && matchingBillIds.every((billId) => { - const ebill = ebills?.find((e) => e.id === billId) - return ebill?.status?.payment?.paid === true - }) + const ebill = ebills?.find((e) => e.id === billId); + return ebill?.status?.payment?.paid === true; + }); const allMintComplete = - matchingBillIds.length > 0 && mintCompleteQueries.every((query) => query.data?.complete === true) + matchingBillIds.length > 0 && + mintCompleteQueries.every((query) => query.data?.complete === true); - const canEnableRedemption = allBillsPaid && allMintComplete - const anyMintCompleteLoading = mintCompleteQueries.some((query) => query.isLoading) + const canEnableRedemption = allBillsPaid && allMintComplete; + const anyMintCompleteLoading = mintCompleteQueries.some( + (query) => query.isLoading, + ); - const hasNoMatchingBills = matchingBillIds.length === 0 + const hasNoMatchingBills = matchingBillIds.length === 0; if (keysetsLoading) { - return + return ; } if (!keyset) { @@ -181,18 +213,21 @@ function PageBody({ keysetId }: { keysetId: string }) {

- +

- ) + ); } const noExpiryText = intl.formatMessage({ id: "keysets.noExpiry", defaultMessage: "No expiry", - }) + }); const finalExpiryDate = keyset.final_expiry ? new Date(keyset.final_expiry * 1000) @@ -203,27 +238,28 @@ function PageBody({ keysetId }: { keysetId: string }) { timeZone: "UTC", }) .replace(/(\d{2}) (\w{3}), (\d{4})/, "$1. $2. $3") - : noExpiryText - const currencyUnit = typeof keyset.unit === "string" ? keyset.unit : keyset.unit.Custom + : noExpiryText; + const currencyUnit = + typeof keyset.unit === "string" ? keyset.unit : keyset.unit.Custom; - type EbillType = NonNullable[number] - const billIdToEbillMap = new Map() + type EbillType = NonNullable[number]; + const billIdToEbillMap = new Map(); if (ebills) { for (const ebill of ebills) { - billIdToEbillMap.set(ebill.id, ebill) + billIdToEbillMap.set(ebill.id, ebill); } } const matchingQuotes = allQuotes.filter((_quote, index) => { - const quoteDetails = quoteDetailsQueries[index]?.data - const billMaturityDate = quoteDetails?.bill?.maturity_date + const quoteDetails = quoteDetailsQueries[index]?.data; + const billMaturityDate = quoteDetails?.bill?.maturity_date; if (!keyset.final_expiry || !billMaturityDate) { - return false + return false; } - return doesBillMatchKeysetMaturity(keyset.final_expiry, billMaturityDate) - }) + return doesBillMatchKeysetMaturity(keyset.final_expiry, billMaturityDate); + }); return (
@@ -246,9 +282,15 @@ function PageBody({ keysetId }: { keysetId: string }) {
{keyset.active ? ( - + ) : ( - + )}
@@ -260,13 +302,16 @@ function PageBody({ keysetId }: { keysetId: string }) { size="sm" variant="default" disabled={ - redemptionMutation.isPending || !canEnableRedemption || anyMintCompleteLoading || hasNoMatchingBills + redemptionMutation.isPending || + !canEnableRedemption || + anyMintCompleteLoading || + hasNoMatchingBills } onClick={() => { redemptionMutation.mutate({ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment body: { kid: keyset.id as any }, - }) + }); }} > {redemptionMutation.isPending @@ -320,56 +365,96 @@ function PageBody({ keysetId }: { keysetId: string }) { - + - + - + - + - + - + {matchingQuotes.map((quote) => { - const quoteIndex = allQuotes.findIndex((q) => q.id === quote.id) - const quoteDetails = quoteDetailsQueries[quoteIndex]?.data - const billId = quoteDetails?.bill?.id - const ebill = billId ? billIdToEbillMap.get(billId) : null - const paymentStatus = ebill?.status?.payment - const cws = ebill?.current_waiting_state - const isPaid = paymentStatus?.paid === true - const isInMempool = cws && "Payment" in cws && cws.Payment.payment_data?.in_mempool === true - const hasPaymentRequestInWaitingState = Boolean(cws && "Payment" in cws) + const quoteIndex = allQuotes.findIndex( + (q) => q.id === quote.id, + ); + const quoteDetails = + quoteDetailsQueries[quoteIndex]?.data; + const billId = quoteDetails?.bill?.id; + const ebill = billId + ? billIdToEbillMap.get(billId) + : null; + const paymentStatus = ebill?.status?.payment; + const cws = ebill?.current_waiting_state; + const isPaid = paymentStatus?.paid === true; + const isInMempool = + cws && + "Payment" in cws && + cws.Payment.payment_data?.in_mempool === true; + const hasPaymentRequestInWaitingState = Boolean( + cws && "Payment" in cws, + ); const requestedToPay = Boolean( paymentStatus?.requested_to_pay ?? ebill?.status?.has_requested_funds ?? hasPaymentRequestInWaitingState, - ) - const rejectedToPay = Boolean(paymentStatus?.rejected_to_pay) - - const billIdIndex = billId ? matchingBillIds.indexOf(billId) : -1 - const mintCompleteQuery = billId && billIdIndex >= 0 ? mintCompleteQueries[billIdIndex] : null - const isMintComplete = mintCompleteQuery?.data?.complete === true - const isMintLoading = mintCompleteQuery?.isLoading - - let paymentAddress: string | undefined + ); + const rejectedToPay = Boolean( + paymentStatus?.rejected_to_pay, + ); + + const billIdIndex = billId + ? matchingBillIds.indexOf(billId) + : -1; + const mintCompleteQuery = + billId && billIdIndex >= 0 + ? mintCompleteQueries[billIdIndex] + : null; + const isMintComplete = + mintCompleteQuery?.data?.complete === true; + const isMintLoading = mintCompleteQuery?.isLoading; + + let paymentAddress: string | undefined; if (cws && "Payment" in cws) { - paymentAddress = cws.Payment.payment_data?.address_to_pay + paymentAddress = + cws.Payment.payment_data?.address_to_pay; } return ( - + - + {intl.formatMessage({ id: `quote.status.${quote.status}`, defaultMessage: formatStatusLabel(quote.status), @@ -390,55 +477,122 @@ function PageBody({ keysetId }: { keysetId: string }) { {ebill ? ( isPaid ? ( - - + + ) : rejectedToPay ? ( - - + + ) : isInMempool ? ( - - + + ) : !requestedToPay ? ( - - + + ) : ( - - + + ) ) : ( - - + + )} {!isPaid ? ( - - + + ) : isMintLoading || !mintCompleteQuery ? ( - - + + ) : ( - + {isMintComplete ? ( - + ) : ( - + )} )} {paymentAddress ?? ( - - + + )} @@ -453,7 +607,7 @@ function PageBody({ keysetId }: { keysetId: string }) { - ) + ); })} @@ -461,22 +615,25 @@ function PageBody({ keysetId }: { keysetId: string }) {
) : (

- +

)}
- ) + ); } export default function KeysetDetailPage() { - const { keysetId } = useParams<{ keysetId: string }>() - const location = useLocation() - const state = location.state as LocationState | null - const fromPath = state?.from - const fromQuote = fromPath?.startsWith("/quotes/") - const quoteId = fromQuote && fromPath ? fromPath.split("/quotes/")[1] : null + const { keysetId } = useParams<{ keysetId: string }>(); + const location = useLocation(); + const state = location.state as LocationState | null; + const fromPath = state?.from; + const fromQuote = fromPath?.startsWith("/quotes/"); + const quoteId = fromQuote && fromPath ? fromPath.split("/quotes/")[1] : null; if (!keysetId) { return ( @@ -484,21 +641,30 @@ export default function KeysetDetailPage() {

- +

- ) + ); } return ( <> + - + , ]} @@ -511,18 +677,33 @@ export default function KeysetDetailPage() { id="keyset.detail.title" defaultMessage="Keyset {id}" values={{ - id: {truncateString(keysetId, 16)}, + id: ( + + {truncateString(keysetId, 16)} + + ), }} /> {fromQuote && quoteId && ( -
- ) + ); } diff --git a/src/pages/keysets/KeysetsPage.test.tsx b/src/pages/keysets/KeysetsPage.test.tsx index ed8633a..3c6eb69 100644 --- a/src/pages/keysets/KeysetsPage.test.tsx +++ b/src/pages/keysets/KeysetsPage.test.tsx @@ -1,38 +1,46 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { MemoryRouter } from "react-router" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { MemoryRouter } from "react-router"; interface QueryOptions { - queryKey: { _id: string }[] + queryKey: { _id: string }[]; } interface QueryResult { - data: unknown - isLoading: boolean + data: unknown; + isLoading: boolean; } -const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>() -let nextSearchQuery = "" +const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>(); +let nextSearchQuery = ""; vi.mock("@tanstack/react-query", async () => { - const actual = await vi.importActual("@tanstack/react-query") + const actual = await vi.importActual( + "@tanstack/react-query", + ); return { ...actual, useQuery: (options: QueryOptions) => mockUseQuery(options), - } -}) + }; +}); vi.mock("@/generated/client/@tanstack/react-query.gen", () => ({ listKeysetInfosOptions: () => ({ queryKey: [{ _id: "listKeysetInfos" }] }), -})) +})); vi.mock("@/components/ui/search", () => ({ - default: ({ onChange, onSearch }: { onChange?: (value: string) => void; onSearch: (value: string) => void }) => ( + default: ({ + onChange, + onSearch, + }: { + onChange?: (value: string) => void; + onSearch: (value: string) => void; + }) => ( ), HighlightText: ({ text }: { text: string }) => <>{text}, -})) +})); vi.mock("@/components/SortButtons.tsx", () => ({ SortButtons: ({ options, onSortChange, }: { - options: { field: "maturity" | "status" | "currency"; label: string }[] - onSortChange: (field: "maturity" | "status" | "currency") => void + options: { field: "maturity" | "status" | "currency"; label: string }[]; + onSortChange: (field: "maturity" | "status" | "currency") => void; }) => (
{options.map((option) => ( - ))}
), -})) +})); -import KeysetsPage from "./KeysetsPage" +import KeysetsPage from "./KeysetsPage"; -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderPage(): HTMLDivElement { @@ -84,109 +96,145 @@ function renderPage(): HTMLDivElement { , - ) + ); } function clickButtonByText(page: HTMLDivElement, label: string) { - const button = Array.from(page.querySelectorAll("button")).find((node) => node.textContent === label) - expect(button).not.toBeUndefined() + const button = Array.from(page.querySelectorAll("button")).find( + (node) => node.textContent === label, + ); + expect(button).not.toBeUndefined(); act(() => { - button?.dispatchEvent(new MouseEvent("click", { bubbles: true })) - }) + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); } function orderedKeysetHrefs(page: HTMLDivElement): string[] { - const hrefs = Array.from(page.querySelectorAll('a[href^="/keysets/"]')).map((node) => node.getAttribute("href") ?? "") - const unique: string[] = [] + const hrefs = Array.from(page.querySelectorAll('a[href^="/keysets/"]')).map( + (node) => node.getAttribute("href") ?? "", + ); + const unique: string[] = []; for (const href of hrefs) { if (!unique.includes(href)) { - unique.push(href) + unique.push(href); } } - return unique + return unique; } beforeEach(() => { - vi.clearAllMocks() - vi.useFakeTimers() - vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z")) - nextSearchQuery = "" + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z")); + nextSearchQuery = ""; if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } -}) +}); describe("KeysetsPage", () => { it("shows empty state when no keysets are returned", () => { - mockUseQuery.mockReturnValue({ data: [], isLoading: false }) + mockUseQuery.mockReturnValue({ data: [], isLoading: false }); - const page = renderPage() - expect(page.textContent).toContain("No keysets found") - }) + const page = renderPage(); + expect(page.textContent).toContain("No keysets found"); + }); it("renders inactive keyset without expiry", () => { mockUseQuery.mockReturnValue({ - data: [{ id: "keyset-no-expiry", active: false, final_expiry: null, unit: { Custom: "usd" } }], + data: [ + { + id: "keyset-no-expiry", + active: false, + final_expiry: null, + unit: { Custom: "usd" }, + }, + ], isLoading: false, - }) + }); - const page = renderPage() - expect(page.textContent).toContain("Inactive") - expect(page.textContent).toContain("No expiry") - expect(page.textContent).toContain("usd") - }) + const page = renderPage(); + expect(page.textContent).toContain("Inactive"); + expect(page.textContent).toContain("No expiry"); + expect(page.textContent).toContain("usd"); + }); it("filters out all rows and shows no-match state from search", () => { mockUseQuery.mockReturnValue({ data: [ - { id: "keyset-aaa", active: true, final_expiry: 1771545600, unit: "sat" }, - { id: "keyset-bbb", active: false, final_expiry: 1771632000, unit: { Custom: "usd" } }, + { + id: "keyset-aaa", + active: true, + final_expiry: 1771545600, + unit: "sat", + }, + { + id: "keyset-bbb", + active: false, + final_expiry: 1771632000, + unit: { Custom: "usd" }, + }, ], isLoading: false, - }) - nextSearchQuery = "definitely-missing" + }); + nextSearchQuery = "definitely-missing"; - const page = renderPage() - clickButtonByText(page, "SearchMock") + const page = renderPage(); + clickButtonByText(page, "SearchMock"); - expect(page.textContent).toContain("No keysets match your search criteria") - }) + expect(page.textContent).toContain("No keysets match your search criteria"); + }); it("sorts by maturity, then currency, then status via sort controls", () => { mockUseQuery.mockReturnValue({ data: [ - { id: "keyset-expired", active: true, final_expiry: 1735689600, unit: "sat" }, - { id: "keyset-future", active: false, final_expiry: 1798761600, unit: { Custom: "usd" } }, - { id: "keyset-no-expiry", active: false, final_expiry: null, unit: { Custom: "eur" } }, + { + id: "keyset-expired", + active: true, + final_expiry: 1735689600, + unit: "sat", + }, + { + id: "keyset-future", + active: false, + final_expiry: 1798761600, + unit: { Custom: "usd" }, + }, + { + id: "keyset-no-expiry", + active: false, + final_expiry: null, + unit: { Custom: "eur" }, + }, ], isLoading: false, - }) + }); - const page = renderPage() + const page = renderPage(); // Default maturity-asc: expired first, no-expiry last. expect(orderedKeysetHrefs(page)).toEqual([ "/keysets/keyset-expired", "/keysets/keyset-future", "/keysets/keyset-no-expiry", - ]) + ]); // Currency-asc: eur, sat, usd. - clickButtonByText(page, "sort-currency") + clickButtonByText(page, "sort-currency"); expect(orderedKeysetHrefs(page)).toEqual([ "/keysets/keyset-no-expiry", "/keysets/keyset-expired", "/keysets/keyset-future", - ]) + ]); // Status-asc in this implementation sorts active first. - clickButtonByText(page, "sort-status") - expect(orderedKeysetHrefs(page)[0]).toBe("/keysets/keyset-expired") - }) -}) + clickButtonByText(page, "sort-status"); + expect(orderedKeysetHrefs(page)[0]).toBe("/keysets/keyset-expired"); + }); +}); diff --git a/src/pages/keysets/KeysetsPage.tsx b/src/pages/keysets/KeysetsPage.tsx index ef67dbb..48f0163 100644 --- a/src/pages/keysets/KeysetsPage.tsx +++ b/src/pages/keysets/KeysetsPage.tsx @@ -1,16 +1,22 @@ -import { PageTitle } from "@/components/PageTitle" -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { useQuery } from "@tanstack/react-query" -import { listKeysetInfosOptions } from "@/generated/client/@tanstack/react-query.gen" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Link } from "react-router" -import { useState } from "react" -import SearchComponent, { HighlightText } from "@/components/ui/search" -import { SortButtons } from "@/components/SortButtons.tsx" -import { FormattedMessage, useIntl } from "react-intl" +import { PageTitle } from "@/components/PageTitle"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { useQuery } from "@tanstack/react-query"; +import { listKeysetInfosOptions } from "@/generated/client/@tanstack/react-query.gen"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Link } from "react-router"; +import { useState } from "react"; +import SearchComponent, { HighlightText } from "@/components/ui/search"; +import { SortButtons } from "@/components/SortButtons.tsx"; +import { FormattedMessage, useIntl } from "react-intl"; function Loader() { return ( @@ -18,41 +24,54 @@ function Loader() {
- ) + ); } -type SortBy = "maturity-asc" | "maturity-desc" | "status-asc" | "status-desc" | "currency-asc" | "currency-desc" +type SortBy = + | "maturity-asc" + | "maturity-desc" + | "status-asc" + | "status-desc" + | "currency-asc" + | "currency-desc"; function PageBody() { - const { data: keysets, isLoading: keysetsLoading } = useQuery(listKeysetInfosOptions()) - const [searchQuery, setSearchQuery] = useState("") - const [sortBy, setSortBy] = useState("maturity-asc") - const intl = useIntl() + const { data: keysets, isLoading: keysetsLoading } = useQuery( + listKeysetInfosOptions(), + ); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState("maturity-asc"); + const intl = useIntl(); const noExpiryText = intl.formatMessage({ id: "keysets.noExpiry", defaultMessage: "No expiry", - }) + }); if (keysetsLoading) { - return + return ; } if (!keysets || keysets.length === 0) { return (
- +
- ) + ); } const filteredKeysets = keysets.filter((keyset) => { if (!searchQuery) { - return true + return true; } - const query = searchQuery.toLowerCase() - const keysetId = keyset.id.toLowerCase() - const currencyUnit = (typeof keyset.unit === "string" ? keyset.unit : keyset.unit.Custom).toLowerCase() + const query = searchQuery.toLowerCase(); + const keysetId = keyset.id.toLowerCase(); + const currencyUnit = ( + typeof keyset.unit === "string" ? keyset.unit : keyset.unit.Custom + ).toLowerCase(); const finalExpiryDate = keyset.final_expiry ? new Date(keyset.final_expiry * 1000) .toLocaleDateString("en-US", { @@ -63,84 +82,98 @@ function PageBody() { }) .replace(/(\d{2}) (\w{3}), (\d{4})/, "$1. $2. $3") .toLowerCase() - : noExpiryText.toLowerCase() + : noExpiryText.toLowerCase(); const status = keyset.active - ? intl.formatMessage({ id: "keysets.status.active", defaultMessage: "Active" }).toLowerCase() - : intl.formatMessage({ id: "keysets.status.inactive", defaultMessage: "Inactive" }).toLowerCase() + ? intl + .formatMessage({ + id: "keysets.status.active", + defaultMessage: "Active", + }) + .toLowerCase() + : intl + .formatMessage({ + id: "keysets.status.inactive", + defaultMessage: "Inactive", + }) + .toLowerCase(); return ( keysetId.includes(query) || currencyUnit.includes(query) || finalExpiryDate.includes(query) || status.includes(query) - ) - }) + ); + }); const sortedKeysets = [...filteredKeysets].sort((a, b) => { - let comparison = 0 + let comparison = 0; switch (sortBy) { case "maturity-asc": case "maturity-desc": { - const aExpiry = a.final_expiry ? new Date(a.final_expiry * 1000) : null - const bExpiry = b.final_expiry ? new Date(b.final_expiry * 1000) : null + const aExpiry = a.final_expiry ? new Date(a.final_expiry * 1000) : null; + const bExpiry = b.final_expiry ? new Date(b.final_expiry * 1000) : null; if (!aExpiry && !bExpiry) { - comparison = 0 + comparison = 0; } else if (!aExpiry) { - comparison = 1 + comparison = 1; } else if (!bExpiry) { - comparison = -1 + comparison = -1; } else { - const now = new Date() - const aIsExpired = aExpiry < now - const bIsExpired = bExpiry < now + const now = new Date(); + const aIsExpired = aExpiry < now; + const bIsExpired = bExpiry < now; if (aIsExpired && !bIsExpired) { - comparison = -1 + comparison = -1; } else if (!aIsExpired && bIsExpired) { - comparison = 1 + comparison = 1; } else { - comparison = aExpiry.getTime() - bExpiry.getTime() + comparison = aExpiry.getTime() - bExpiry.getTime(); } } if (sortBy === "maturity-desc") { - comparison = -comparison + comparison = -comparison; } - break + break; } case "status-asc": case "status-desc": { - const aStatus = a.active ? 1 : 0 - const bStatus = b.active ? 1 : 0 - comparison = bStatus - aStatus + const aStatus = a.active ? 1 : 0; + const bStatus = b.active ? 1 : 0; + comparison = bStatus - aStatus; if (sortBy === "status-desc") { - comparison = -comparison + comparison = -comparison; } - break + break; } case "currency-asc": case "currency-desc": { - const aCurrency = typeof a.unit === "string" ? a.unit : a.unit.Custom - const bCurrency = typeof b.unit === "string" ? b.unit : b.unit.Custom - comparison = aCurrency.localeCompare(bCurrency) + const aCurrency = typeof a.unit === "string" ? a.unit : a.unit.Custom; + const bCurrency = typeof b.unit === "string" ? b.unit : b.unit.Custom; + comparison = aCurrency.localeCompare(bCurrency); if (sortBy === "currency-desc") { - comparison = -comparison + comparison = -comparison; } - break + break; } } - return comparison - }) + return comparison; + }); const toggleSort = (field: "maturity" | "status" | "currency") => { if (sortBy.startsWith(field)) { - setSortBy(sortBy.endsWith("asc") ? (`${field}-desc` as SortBy) : (`${field}-asc` as SortBy)) + setSortBy( + sortBy.endsWith("asc") + ? (`${field}-desc` as SortBy) + : (`${field}-asc` as SortBy), + ); } else { - setSortBy(`${field}-asc` as SortBy) + setSortBy(`${field}-asc` as SortBy); } - } + }; const sortOptions = [ { @@ -164,7 +197,7 @@ function PageBody() { defaultMessage: "Status", }), }, - ] + ]; return (
@@ -174,18 +207,26 @@ function PageBody() { className="flex-1 max-w-md" placeholder={intl.formatMessage({ id: "keysets.search.placeholder", - defaultMessage: "Search by keyset ID, currency, maturity date, or status...", + defaultMessage: + "Search by keyset ID, currency, maturity date, or status...", })} onSearch={setSearchQuery} onChange={setSearchQuery} size="sm" /> - +
{sortedKeysets.length === 0 ? (
- +
) : ( <> @@ -199,8 +240,11 @@ function PageBody() { timeZone: "UTC", }) .replace(/(\d{2}) (\w{3}), (\d{4})/, "$1. $2. $3") - : noExpiryText - const currencyUnit = typeof keyset.unit === "string" ? keyset.unit : keyset.unit.Custom + : noExpiryText; + const currencyUnit = + typeof keyset.unit === "string" + ? keyset.unit + : keyset.unit.Custom; const statusText = keyset.active ? intl.formatMessage({ id: "keysets.status.active", @@ -209,16 +253,25 @@ function PageBody() { : intl.formatMessage({ id: "keysets.status.inactive", defaultMessage: "Inactive", - }) + }); return ( - +
- + - + @@ -226,46 +279,73 @@ function PageBody() { id="keysets.card.meta" defaultMessage="Currency: {currency} | Maturity date: {maturityDate}" values={{ - currency: , - maturityDate: , + currency: ( + + ), + maturityDate: ( + + ), }} />
- +
-
- ) + ); })} )}
- ) + ); } export default function KeysetsPage() { return ( <> - + - + - ) + ); } diff --git a/src/pages/quotes/DenyConfirmDrawer.tsx b/src/pages/quotes/DenyConfirmDrawer.tsx index dcd035e..6d323b1 100644 --- a/src/pages/quotes/DenyConfirmDrawer.tsx +++ b/src/pages/quotes/DenyConfirmDrawer.tsx @@ -1,23 +1,30 @@ -import { ConfirmDrawer } from "@/components/Drawers" -import type { ReactNode } from "react" -import { useIntl } from "react-intl" +import { ConfirmDrawer } from "@/components/Drawers"; +import type { ReactNode } from "react"; +import { useIntl } from "react-intl"; interface DenyConfirmDrawerProps { - title: string - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: () => void - children: ReactNode + title: string; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: () => void; + children: ReactNode; } -export function DenyConfirmDrawer({ title, open, onOpenChange, onSubmit, children }: DenyConfirmDrawerProps) { - const intl = useIntl() +export function DenyConfirmDrawer({ + title, + open, + onOpenChange, + onSubmit, + children, +}: DenyConfirmDrawerProps) { + const intl = useIntl(); return ( - ) + ); } diff --git a/src/pages/quotes/OfferFormDrawer.tsx b/src/pages/quotes/OfferFormDrawer.tsx index 086566c..3327ea2 100644 --- a/src/pages/quotes/OfferFormDrawer.tsx +++ b/src/pages/quotes/OfferFormDrawer.tsx @@ -1,35 +1,35 @@ -import Big from "big.js" -import { BaseDrawer } from "@/components/Drawers" -import { GrossToNetDiscountForm } from "@/components/GrossToNetDiscountForm" -import type { InfoReply } from "@/generated/client/types.gen" -import type { ReactNode } from "react" +import Big from "big.js"; +import { BaseDrawer } from "@/components/Drawers"; +import { GrossToNetDiscountForm } from "@/components/GrossToNetDiscountForm"; +import type { InfoReply } from "@/generated/client/types.gen"; +import type { ReactNode } from "react"; export interface OfferFormResult { discount: { - days: number - discountRate: Big + days: number; + discountRate: Big; net: { - value: Big - currency: string - } + value: Big; + currency: string; + }; gross: { - value: Big - currency: string - } - } + value: Big; + currency: string; + }; + }; ttl: { - ttl: Date - } + ttl: Date; + }; } interface OfferFormDrawerProps { - title: string - description: string - value: InfoReply - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (data: OfferFormResult) => void - children: ReactNode + title: string; + description: string; + value: InfoReply; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: OfferFormResult) => void; + children: ReactNode; } export function OfferFormDrawer({ @@ -42,28 +42,37 @@ export function OfferFormDrawer({ children, }: OfferFormDrawerProps) { const handleFormSubmit = (values: { - days: number - discountRate: Big - net: { value: Big; currency: string } - gross: { value: Big; currency: string } + days: number; + discountRate: Big; + net: { value: Big; currency: string }; + gross: { value: Big; currency: string }; }) => { - const now = new Date() - const ttl = new Date(now.getTime() + values.days * 24 * 60 * 60 * 1000) + const now = new Date(); + const ttl = new Date(now.getTime() + values.days * 24 * 60 * 60 * 1000); const result: OfferFormResult = { discount: values, ttl: { ttl }, - } + }; - onSubmit(result) - } + onSubmit(result); + }; - const startDate = value.status === "Pending" ? new Date(value.submitted) : new Date() + const startDate = + value.status === "Pending" ? new Date(value.submitted) : new Date(); const endDate = - value.status === "Pending" ? new Date(value.suggested_expiration) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + value.status === "Pending" + ? new Date(value.suggested_expiration) + : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); return ( - + - ) + ); } diff --git a/src/pages/quotes/QuoteActions.tsx b/src/pages/quotes/QuoteActions.tsx index 1edabd7..659b344 100644 --- a/src/pages/quotes/QuoteActions.tsx +++ b/src/pages/quotes/QuoteActions.tsx @@ -1,26 +1,32 @@ -import { useState } from "react" -import { useQuery } from "@tanstack/react-query" -import { LoaderIcon } from "lucide-react" -import { Button } from "@/components/ui/button" -import { getEbillOptions } from "@/generated/client/@tanstack/react-query.gen" -import type { InfoReply, BillWaitingStatePaymentData } from "@/generated/client/types.gen" -import { OfferFormDrawer, type OfferFormResult } from "./components/OfferFormDrawer.tsx" -import { DenyConfirmDrawer } from "./components/DenyConfirmDrawer.tsx" -import { removeItem } from "@/utils/local-storage" -import { PaymentRequestCard } from "./components/PaymentRequestCard.tsx" -import { OfferConfirmation } from "./components/OfferConfirmation.tsx" -import { RequestToPayConfirmation } from "./components/RequestToPayConfirmation.tsx" -import { useQuoteMutations } from "./components/useQuoteMutations.ts" -import { useIntl } from "react-intl" +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { LoaderIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { getEbillOptions } from "@/generated/client/@tanstack/react-query.gen"; +import type { + InfoReply, + BillWaitingStatePaymentData, +} from "@/generated/client/types.gen"; +import { + OfferFormDrawer, + type OfferFormResult, +} from "./components/OfferFormDrawer.tsx"; +import { DenyConfirmDrawer } from "./components/DenyConfirmDrawer.tsx"; +import { removeItem } from "@/utils/local-storage"; +import { PaymentRequestCard } from "./components/PaymentRequestCard.tsx"; +import { OfferConfirmation } from "./components/OfferConfirmation.tsx"; +import { RequestToPayConfirmation } from "./components/RequestToPayConfirmation.tsx"; +import { useQuoteMutations } from "./components/useQuoteMutations.ts"; +import { useIntl } from "react-intl"; interface QuoteActionsProps { - value: InfoReply - isFetching: boolean - ebillPaid: boolean - isMintComplete: boolean - requestedToPay: boolean - paymentDeadlineTs?: number | null - timeOfRequestToPay?: number | null + value: InfoReply; + isFetching: boolean; + ebillPaid: boolean; + isMintComplete: boolean; + requestedToPay: boolean; + paymentDeadlineTs?: number | null; + timeOfRequestToPay?: number | null; } export function QuoteActions({ @@ -32,61 +38,79 @@ export function QuoteActions({ paymentDeadlineTs, timeOfRequestToPay, }: QuoteActionsProps) { - const intl = useIntl() - const billId = value.bill.id + const intl = useIntl(); + const billId = value.bill.id; const ebillQuery = useQuery({ ...getEbillOptions({ path: { bid: billId } }), retry: 1, enabled: !!billId, - }) + }); - const ebill = ebillQuery.data - const quoteStatus = value.status as string - const paymentStatus = ebill?.status.payment - const cws = ebill?.current_waiting_state + const ebill = ebillQuery.data; + const quoteStatus = value.status as string; + const paymentStatus = ebill?.status.payment; + const cws = ebill?.current_waiting_state; - let waitingPaymentData: BillWaitingStatePaymentData | undefined + let waitingPaymentData: BillWaitingStatePaymentData | undefined; if (cws && "Payment" in cws) { - waitingPaymentData = cws.Payment.payment_data + waitingPaymentData = cws.Payment.payment_data; } - const requestedToPayEff = Boolean(requestedToPay || paymentStatus?.requested_to_pay) - const ebillPaidEff = Boolean(ebillPaid || (paymentStatus?.paid && isMintComplete)) + const requestedToPayEff = Boolean( + requestedToPay || paymentStatus?.requested_to_pay, + ); + const ebillPaidEff = Boolean( + ebillPaid || (paymentStatus?.paid && isMintComplete), + ); const effectiveRequestTime = - timeOfRequestToPay ?? paymentStatus?.time_of_request_to_pay ?? waitingPaymentData?.time_of_request ?? null + timeOfRequestToPay ?? + paymentStatus?.time_of_request_to_pay ?? + waitingPaymentData?.time_of_request ?? + null; const effectiveDeadlineTs = - paymentDeadlineTs ?? paymentStatus?.payment_deadline_timestamp ?? waitingPaymentData?.payment_deadline ?? null - const linkToPay: string | undefined = waitingPaymentData?.mempool_link_for_address_to_pay - const addressToPay: string | undefined = waitingPaymentData?.address_to_pay + paymentDeadlineTs ?? + paymentStatus?.payment_deadline_timestamp ?? + waitingPaymentData?.payment_deadline ?? + null; + const linkToPay: string | undefined = + waitingPaymentData?.mempool_link_for_address_to_pay; + const addressToPay: string | undefined = waitingPaymentData?.address_to_pay; - const [offerFormData, setOfferFormData] = useState() - const [offerFormDrawerOpen, setOfferFormDrawerOpen] = useState(false) - const [offerConfirmDrawerOpen, setOfferConfirmDrawerOpen] = useState(false) - const [denyConfirmDrawerOpen, setDenyConfirmDrawerOpen] = useState(false) - const [requestToPayConfirmDrawerOpen, setRequestToPayConfirmDrawerOpen] = useState(false) + const [offerFormData, setOfferFormData] = useState(); + const [offerFormDrawerOpen, setOfferFormDrawerOpen] = useState(false); + const [offerConfirmDrawerOpen, setOfferConfirmDrawerOpen] = useState(false); + const [denyConfirmDrawerOpen, setDenyConfirmDrawerOpen] = useState(false); + const [requestToPayConfirmDrawerOpen, setRequestToPayConfirmDrawerOpen] = + useState(false); const denyTitle = intl.formatMessage({ id: "quotes.actions.deny.title", defaultMessage: "Confirm denying quote", - }) + }); const denyButtonLabel = intl.formatMessage({ id: "quotes.actions.deny.button", defaultMessage: "Deny", - }) + }); const offerTitle = intl.formatMessage({ id: "quotes.actions.offer.title", defaultMessage: "Offer quote", - }) + }); const offerDescription = intl.formatMessage({ id: "quotes.actions.offer.description", defaultMessage: "Make an offer to the current holder of this bill", - }) + }); const offerButtonLabel = intl.formatMessage({ id: "quotes.actions.offer.button", defaultMessage: "Offer", - }) - const { denyQuote, offerQuote, requestToPayMutation, handleDenyQuote, handleOfferQuote, handleRequestToPay } = - useQuoteMutations(value.id, billId) + }); + const { + denyQuote, + offerQuote, + requestToPayMutation, + handleDenyQuote, + handleOfferQuote, + handleRequestToPay, + } = useQuoteMutations(value.id, billId); return ( <> @@ -97,12 +121,19 @@ export function QuoteActions({ open={denyConfirmDrawerOpen} onOpenChange={setDenyConfirmDrawerOpen} onSubmit={() => { - handleDenyQuote() - setDenyConfirmDrawerOpen(false) + handleDenyQuote(); + setDenyConfirmDrawerOpen(false); }} > - )} @@ -115,13 +146,19 @@ export function QuoteActions({ open={offerFormDrawerOpen} onOpenChange={setOfferFormDrawerOpen} onSubmit={(data) => { - setOfferFormData(data) - setOfferConfirmDrawerOpen(true) - setOfferFormDrawerOpen(false) + setOfferFormData(data); + setOfferConfirmDrawerOpen(true); + setOfferFormDrawerOpen(false); }} > - )} @@ -131,9 +168,9 @@ export function QuoteActions({ open={offerConfirmDrawerOpen} onOpenChange={setOfferConfirmDrawerOpen} onSubmit={(finalData) => { - removeItem(`offer-form-${value.id}`) - handleOfferQuote(finalData) - setOfferConfirmDrawerOpen(false) + removeItem(`offer-form-${value.id}`); + handleOfferQuote(finalData); + setOfferConfirmDrawerOpen(false); }} quoteId={value.id} /> @@ -147,8 +184,8 @@ export function QuoteActions({ open={requestToPayConfirmDrawerOpen} onOpenChange={setRequestToPayConfirmDrawerOpen} onSubmit={(deadline) => { - handleRequestToPay(value.bill.sum, deadline) - setRequestToPayConfirmDrawerOpen(false) + handleRequestToPay(value.bill.sum, deadline); + setRequestToPayConfirmDrawerOpen(false); }} isFetching={isFetching} isPending={requestToPayMutation.isPending} @@ -167,5 +204,5 @@ export function QuoteActions({ /> )} - ) + ); } diff --git a/src/pages/quotes/QuotePage.test.tsx b/src/pages/quotes/QuotePage.test.tsx index d49287f..13caa1f 100644 --- a/src/pages/quotes/QuotePage.test.tsx +++ b/src/pages/quotes/QuotePage.test.tsx @@ -1,62 +1,66 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { MemoryRouter, Route, Routes } from "react-router" -import QuotePage from "./QuotePage" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { MemoryRouter, Route, Routes } from "react-router"; +import QuotePage from "./QuotePage"; interface QueryKeyEntry { - _id: string - path?: { qid?: string; bid?: string } + _id: string; + path?: { qid?: string; bid?: string }; } interface QueryOptions { - queryKey: QueryKeyEntry[] + queryKey: QueryKeyEntry[]; } interface QueryResult { - data: unknown - isLoading: boolean - isFetching?: boolean - error: Error | null + data: unknown; + isLoading: boolean; + isFetching?: boolean; + error: Error | null; } interface MutationResult { - mutate: (value: { body: { token: string } }) => void - isPending: boolean - isSuccess: boolean - isError: boolean - data: unknown + mutate: (value: { body: { token: string } }) => void; + isPending: boolean; + isSuccess: boolean; + isError: boolean; + data: unknown; } -const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>() -const mockUseMutation = vi.fn<() => MutationResult>() +const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>(); +const mockUseMutation = vi.fn<() => MutationResult>(); vi.mock("sonner", () => ({ toast: { error: vi.fn() }, -})) +})); vi.mock("./QuoteActions.tsx", () => ({ QuoteActions: () =>
QuoteActionsMock
, -})) +})); vi.mock("@/components/EndorsementChain", () => ({ EndorsementChain: () =>
EndorsementChainMock
, -})) +})); vi.mock("@/components/ParticipantsOverview", () => ({ ParticipantsOverviewCard: () =>
ParticipantsOverviewMock
, ParticipantDetail: () =>
ParticipantDetailMock
, -})) +})); vi.mock("@tanstack/react-query", async () => { - const actual = await vi.importActual("@tanstack/react-query") + const actual = await vi.importActual( + "@tanstack/react-query", + ); return { ...actual, useQuery: (options: QueryOptions) => mockUseQuery(options), useMutation: () => mockUseMutation(), - } -}) + }; +}); vi.mock("@/generated/client/@tanstack/react-query.gen", () => ({ - getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ queryKey: [{ _id: "getQuote", path }] }), + getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ + queryKey: [{ _id: "getQuote", path }], + }), listEbillsOptions: () => ({ queryKey: [{ _id: "listEbills" }] }), getEbillEndorsementsOptions: ({ path }: { path: { bid: string } }) => ({ queryKey: [{ _id: "getEbillEndorsements", path }], @@ -65,44 +69,49 @@ vi.mock("@/generated/client/@tanstack/react-query.gen", () => ({ queryKey: [{ _id: "getEbillMintComplete", path }], }), postTokenStatusMutation: () => ({ mutationFn: vi.fn() }), -})) +})); -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } -function renderPage(entry: string | { pathname: string; state?: Record }): HTMLDivElement { +function renderPage( + entry: string | { pathname: string; state?: Record }, +): HTMLDivElement { return renderIntoDom( - } /> + } + /> , - ) + ); } beforeEach(() => { - vi.clearAllMocks() + vi.clearAllMocks(); if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } mockUseMutation.mockReturnValue({ @@ -111,10 +120,10 @@ beforeEach(() => { isSuccess: false, isError: false, data: undefined, - }) + }); mockUseQuery.mockImplementation((opts: QueryOptions) => { - const id = opts.queryKey[0]._id + const id = opts.queryKey[0]._id; if (id === "getQuote") { return { data: { @@ -134,37 +143,45 @@ beforeEach(() => { isLoading: false, isFetching: false, error: null, - } + }; } if (id === "listEbills") { - return { data: [], isLoading: false, error: null } + return { data: [], isLoading: false, error: null }; } if (id === "getEbillEndorsements") { - return { data: [], isLoading: false, error: null } + return { data: [], isLoading: false, error: null }; } if (id === "getEbillMintComplete") { - return { data: { complete: false }, isLoading: false, error: null } + return { data: { complete: false }, isLoading: false, error: null }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) -}) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); +}); describe("QuotePage", () => { it("shows back-to-keyset action when navigated from a keyset page", () => { - const page = renderPage({ pathname: "/quotes/quote-1", state: { from: "/keysets/keyset-1234" } }) - const link = page.querySelector('a[href="/keysets/keyset-1234"]') - expect(link?.textContent).toContain("Back to keyset") - }) + const page = renderPage({ + pathname: "/quotes/quote-1", + state: { from: "/keysets/keyset-1234" }, + }); + const link = page.querySelector('a[href="/keysets/keyset-1234"]'); + expect(link?.textContent).toContain("Back to keyset"); + }); it("shows go-to-keyset action from quote data when no navigation state is provided", () => { - const page = renderPage("/quotes/quote-1") - const link = page.querySelector('a[href="/keysets/keyset-from-quote"]') - expect(link?.textContent).toContain("Go to keyset") - }) + const page = renderPage("/quotes/quote-1"); + const link = page.querySelector('a[href="/keysets/keyset-from-quote"]'); + expect(link?.textContent).toContain("Go to keyset"); + }); it("shows quote load error state", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { @@ -174,15 +191,20 @@ describe("QuotePage", () => { isLoading: false, isFetching: false, error: new Error("boom"), - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); - const page = renderPage("/quotes/quote-error") - expect(page.textContent).toContain("Failed to load quote") - expect(page.textContent).toContain("boom") - }) + const page = renderPage("/quotes/quote-error"); + expect(page.textContent).toContain("Failed to load quote"); + expect(page.textContent).toContain("boom"); + }); it("shows empty quote state when bill data is missing", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { @@ -192,14 +214,19 @@ describe("QuotePage", () => { isLoading: false, isFetching: false, error: null, - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); - const page = renderPage("/quotes/quote-1") - expect(page.textContent).toContain("No quote data available") - }) + const page = renderPage("/quotes/quote-1"); + expect(page.textContent).toContain("No quote data available"); + }); it("does not render keyset action when quote does not expose keyset id", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { @@ -221,13 +248,18 @@ describe("QuotePage", () => { isLoading: false, isFetching: false, error: null, - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) - - const page = renderPage("/quotes/quote-2") - const keysetLink = page.querySelector('a[href^="/keysets/"]') - expect(keysetLink).toBeNull() - }) -}) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); + + const page = renderPage("/quotes/quote-2"); + const keysetLink = page.querySelector('a[href^="/keysets/"]'); + expect(keysetLink).toBeNull(); + }); +}); diff --git a/src/pages/quotes/QuotePage.tsx b/src/pages/quotes/QuotePage.tsx index b9322a8..5e92b8f 100644 --- a/src/pages/quotes/QuotePage.tsx +++ b/src/pages/quotes/QuotePage.tsx @@ -1,35 +1,38 @@ -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" -import { ParticipantsOverviewCard, ParticipantDetail } from "@/components/ParticipantsOverview" +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + ParticipantsOverviewCard, + ParticipantDetail, +} from "@/components/ParticipantsOverview"; import { getQuoteOptions, listEbillsOptions, getEbillEndorsementsOptions, getEbillMintCompleteOptions, postTokenStatusMutation, -} from "@/generated/client/@tanstack/react-query.gen" -import { useMutation, useQuery } from "@tanstack/react-query" -import { useParams, Link, useLocation } from "react-router" -import { humanReadableDurationDays } from "@/utils/dates" -import { BreadcrumbLink } from "@/components/ui/breadcrumb" -import { QuoteActions } from "./QuoteActions.tsx" -import { truncateString, formatStatusLabel } from "@/utils/strings.ts" -import { getQuoteStatusVariant } from "@/utils/quote-status" -import { TruncatedTextPopover } from "@/components/TruncatedTextPopover.tsx" -import { EndorsementChain } from "@/components/EndorsementChain" -import { FeeTokenQRCodeModal } from "@/components/QRCodeWithErrorBoundary" -import { serializeKeysetId } from "@/utils/keyset" -import { useIntl } from "react-intl" -import { useEffect, useRef } from "react" -import { toast } from "sonner" -import { getApiErrorMessage } from "@/lib/api-error" +} from "@/generated/client/@tanstack/react-query.gen"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useParams, Link, useLocation } from "react-router"; +import { humanReadableDurationDays } from "@/utils/dates"; +import { BreadcrumbLink } from "@/components/ui/breadcrumb"; +import { QuoteActions } from "./QuoteActions.tsx"; +import { truncateString, formatStatusLabel } from "@/utils/strings.ts"; +import { getQuoteStatusVariant } from "@/utils/quote-status"; +import { TruncatedTextPopover } from "@/components/TruncatedTextPopover.tsx"; +import { EndorsementChain } from "@/components/EndorsementChain"; +import { FeeTokenQRCodeModal } from "@/components/QRCodeWithErrorBoundary"; +import { serializeKeysetId } from "@/utils/keyset"; +import { useIntl } from "react-intl"; +import { useEffect, useRef } from "react"; +import { toast } from "sonner"; +import { getApiErrorMessage } from "@/lib/api-error"; interface LocationState { - from?: string + from?: string; } function Loader() { @@ -37,12 +40,12 @@ function Loader() {
- ) + ); } function PageBody({ id }: { id: string }) { - const intl = useIntl() - const EBILL_POLL_INTERVAL_MS = 30_000 + const intl = useIntl(); + const EBILL_POLL_INTERVAL_MS = 30_000; const { data: quoteData, isFetching, @@ -53,33 +56,34 @@ function PageBody({ id }: { id: string }) { path: { qid: id }, }), retry: 1, - }) + }); - const billId = quoteData?.bill?.id - const quoteStatus = quoteData?.status as string | undefined + const billId = quoteData?.bill?.id; + const quoteStatus = quoteData?.status as string | undefined; const ebillsQuery = useQuery({ ...listEbillsOptions(), retry: 1, enabled: !!billId, refetchInterval: (query) => { - if (query.state.error) return false - const ebill = (query.state.data ?? []).find((item) => item.id === billId) - return ebill?.status?.payment?.paid ? false : EBILL_POLL_INTERVAL_MS + if (query.state.error) return false; + const ebill = (query.state.data ?? []).find((item) => item.id === billId); + return ebill?.status?.payment?.paid ? false : EBILL_POLL_INTERVAL_MS; }, - }) + }); const endorsementsQuery = useQuery({ ...getEbillEndorsementsOptions({ path: { bid: billId ?? "" } }), retry: 1, enabled: !!billId, - }) + }); - const ebill = ebillsQuery.data?.find((item) => item.id === billId) - const isPaid = ebill?.status?.payment?.paid === true - const shouldCheckMintComplete = quoteStatus === "Accepted" || quoteStatus === "MintingEnabled" || isPaid + const ebill = ebillsQuery.data?.find((item) => item.id === billId); + const isPaid = ebill?.status?.payment?.paid === true; + const shouldCheckMintComplete = + quoteStatus === "Accepted" || quoteStatus === "MintingEnabled" || isPaid; - const feeTokenRequestRef = useRef(null) + const feeTokenRequestRef = useRef(null); const { mutate: requestFeeTokenStatus, @@ -91,7 +95,7 @@ function PageBody({ id }: { id: string }) { ...postTokenStatusMutation(), retry: 5, onError: (error) => { - const message = getApiErrorMessage(error) + const message = getApiErrorMessage(error); toast.error( intl.formatMessage( { @@ -100,10 +104,10 @@ function PageBody({ id }: { id: string }) { }, { error: message }, ), - ) - feeTokenRequestRef.current = null + ); + feeTokenRequestRef.current = null; }, - }) + }); const mintCompleteQuery = useQuery({ ...getEbillMintCompleteOptions({ path: { bid: billId ?? "" } }), @@ -111,38 +115,45 @@ function PageBody({ id }: { id: string }) { enabled: !!billId && shouldCheckMintComplete, refetchInterval: (query) => { if (!shouldCheckMintComplete) { - return false + return false; } - const data = query.state.data - return data?.complete === false ? 60000 : false + const data = query.state.data; + return data?.complete === false ? 60000 : false; }, - }) + }); - const feeTokenFromQuote = quoteData && "fee" in quoteData ? quoteData.fee : null - const quoteStatusForEffect = quoteData?.status + const feeTokenFromQuote = + quoteData && "fee" in quoteData ? quoteData.fee : null; + const quoteStatusForEffect = quoteData?.status; useEffect(() => { if (!feeTokenFromQuote || quoteStatusForEffect !== "MintingEnabled") { - return + return; } if (feeTokenRequestRef.current === feeTokenFromQuote) { - return + return; } if (isFeeTokenStatusPending || isFeeTokenStatusSuccess) { - return + return; } - feeTokenRequestRef.current = feeTokenFromQuote + feeTokenRequestRef.current = feeTokenFromQuote; requestFeeTokenStatus({ body: { token: feeTokenFromQuote }, - }) - }, [feeTokenFromQuote, isFeeTokenStatusPending, isFeeTokenStatusSuccess, quoteStatusForEffect, requestFeeTokenStatus]) + }); + }, [ + feeTokenFromQuote, + isFeeTokenStatusPending, + isFeeTokenStatusSuccess, + quoteStatusForEffect, + requestFeeTokenStatus, + ]); if (error) { - const errorMessage = getApiErrorMessage(error) + const errorMessage = getApiErrorMessage(error); return (
@@ -165,34 +176,39 @@ function PageBody({ id }: { id: string }) { })}
- ) + ); } if (isLoading) { - return + return ; } - const quote = quoteData! - const bill = quote?.bill - const quoteStatusValue = quote.status as string - const feeToken = "fee" in quote && typeof quote.fee === "string" ? quote.fee : null - - const billStatus = ebill?.status - const paymentStatus = billStatus?.payment - const cws = ebill?.current_waiting_state - const isMintComplete = mintCompleteQuery.data?.complete ?? false - const isMintCompleteLoading = mintCompleteQuery.isLoading - const ebillPaid = Boolean(paymentStatus?.paid) - const hasPaymentRequestInWaitingState = Boolean(cws && "Payment" in cws) + const quote = quoteData!; + const bill = quote?.bill; + const quoteStatusValue = quote.status as string; + const feeToken = + "fee" in quote && typeof quote.fee === "string" ? quote.fee : null; + + const billStatus = ebill?.status; + const paymentStatus = billStatus?.payment; + const cws = ebill?.current_waiting_state; + const isMintComplete = mintCompleteQuery.data?.complete ?? false; + const isMintCompleteLoading = mintCompleteQuery.isLoading; + const ebillPaid = Boolean(paymentStatus?.paid); + const hasPaymentRequestInWaitingState = Boolean(cws && "Payment" in cws); const requestedToPay = Boolean( - paymentStatus?.requested_to_pay ?? billStatus?.has_requested_funds ?? hasPaymentRequestInWaitingState, - ) - const rejectedToPay = Boolean(paymentStatus?.rejected_to_pay) - const paymentDeadlineTs = paymentStatus?.payment_deadline_timestamp ?? null - const timeOfRequestToPay = paymentStatus?.time_of_request_to_pay ?? null - - const isInMempool = cws && "Payment" in cws && cws.Payment.payment_data?.in_mempool === true - const showPayment = quoteStatus === "Accepted" || quoteStatus === "MintingEnabled" + paymentStatus?.requested_to_pay ?? + billStatus?.has_requested_funds ?? + hasPaymentRequestInWaitingState, + ); + const rejectedToPay = Boolean(paymentStatus?.rejected_to_pay); + const paymentDeadlineTs = paymentStatus?.payment_deadline_timestamp ?? null; + const timeOfRequestToPay = paymentStatus?.time_of_request_to_pay ?? null; + + const isInMempool = + cws && "Payment" in cws && cws.Payment.payment_data?.in_mempool === true; + const showPayment = + quoteStatus === "Accepted" || quoteStatus === "MintingEnabled"; if (!quote || !bill) { return ( @@ -202,16 +218,16 @@ function PageBody({ id }: { id: string }) { defaultMessage: "No quote data available", })}
- ) + ); } - const maturityDate = bill.maturity_date ? new Date(bill.maturity_date) : null + const maturityDate = bill.maturity_date ? new Date(bill.maturity_date) : null; const maturityLabel = maturityDate ? humanReadableDurationDays(intl.locale, maturityDate) : intl.formatMessage({ id: "quotes.common.unknown", defaultMessage: "Unknown", - }) + }); return (
@@ -260,14 +276,22 @@ function PageBody({ id }: { id: string }) { })} {isMintCompleteLoading ? ( - + {intl.formatMessage({ id: "quotes.redemption.pending", defaultMessage: "Pending", })} ) : ( - + {isMintComplete ? intl.formatMessage({ id: "quotes.redemption.complete", @@ -302,35 +326,50 @@ function PageBody({ id }: { id: string }) { })} {ebillPaid ? ( - + {intl.formatMessage({ id: "quotes.payment.paid", defaultMessage: "Paid", })} ) : rejectedToPay ? ( - + {intl.formatMessage({ id: "quotes.payment.rejected", defaultMessage: "Rejected to pay", })} ) : isInMempool ? ( - + {intl.formatMessage({ id: "quotes.payment.inMempool", defaultMessage: "In mempool", })} ) : !requestedToPay ? ( - + {intl.formatMessage({ id: "quotes.payment.notRequested", defaultMessage: "Not requested", })} ) : ( - + {intl.formatMessage({ id: "quotes.payment.requested", defaultMessage: "Requested", @@ -358,7 +397,9 @@ function PageBody({ id }: { id: string }) { defaultMessage: "Fee:", })} - {quote.discounted} sat + + {quote.discounted} sat +
@@ -367,7 +408,9 @@ function PageBody({ id }: { id: string }) { defaultMessage: "Effective fee (absolute):", })} - {bill.sum - quote.discounted} sat + + {bill.sum - quote.discounted} sat +
@@ -377,7 +420,10 @@ function PageBody({ id }: { id: string }) { })} - {(((bill.sum - quote.discounted) / bill.sum) * 100).toFixed(4)}% + {(((bill.sum - quote.discounted) / bill.sum) * 100).toFixed( + 4, + )} + %
@@ -398,35 +444,50 @@ function PageBody({ id }: { id: string }) { /> {isFeeTokenStatusPending ? ( - + {intl.formatMessage({ id: "quotes.feeToken.badge.checking", defaultMessage: "Checking...", })} ) : feeTokenStatusData?.state === "Spent" ? ( - + {intl.formatMessage({ id: "quotes.feeToken.badge.spent", defaultMessage: "Spent", })} ) : feeTokenStatusData?.state === "Unspent" ? ( - + {intl.formatMessage({ id: "quotes.feeToken.badge.active", defaultMessage: "Active", })} ) : isFeeTokenStatusError ? ( - + {intl.formatMessage({ id: "quotes.feeToken.badge.error", defaultMessage: "Error", })} ) : feeTokenStatusData?.state ? ( - + {intl.formatMessage({ id: "quotes.feeToken.badge.unknown", defaultMessage: "Unknown", @@ -500,7 +561,9 @@ function PageBody({ id }: { id: string }) { })} : - + )}
@@ -523,22 +586,35 @@ function PageBody({ id }: { id: string }) { isLoading={endorsementsQuery.isLoading} issueDate={ebill?.data?.issue_date} maturityDate={bill.maturity_date} - requestToPayTimestamp={ebill?.status?.payment?.time_of_request_to_pay ?? undefined} + requestToPayTimestamp={ + ebill?.status?.payment?.time_of_request_to_pay ?? undefined + } rejectedToPayTimestamp={ - ebill?.status?.payment?.rejected_to_pay ? (ebill?.status?.last_block_time ?? undefined) : undefined + ebill?.status?.payment?.rejected_to_pay + ? (ebill?.status?.last_block_time ?? undefined) + : undefined + } + paymentTimestamp={ + ebill?.status?.payment?.paid + ? (ebill?.status?.last_block_time ?? undefined) + : undefined } - paymentTimestamp={ebill?.status?.payment?.paid ? (ebill?.status?.last_block_time ?? undefined) : undefined} acceptanceTimestamp={ ebill?.status?.acceptance?.accepted - ? (ebill?.status?.acceptance?.time_of_request_to_accept ?? undefined) + ? (ebill?.status?.acceptance?.time_of_request_to_accept ?? + undefined) : undefined } rejectionTimestamp={ - ebill?.status?.acceptance?.rejected_to_accept ? (ebill?.status?.last_block_time ?? undefined) : undefined + ebill?.status?.acceptance?.rejected_to_accept + ? (ebill?.status?.last_block_time ?? undefined) + : undefined } mintingEnabled={quoteStatusValue === "MintingEnabled"} quoteOffered={ - quoteStatusValue === "Offered" || quoteStatusValue === "Accepted" || quoteStatusValue === "MintingEnabled" + quoteStatusValue === "Offered" || + quoteStatusValue === "Accepted" || + quoteStatusValue === "MintingEnabled" } offeredTimestamp={ "submitted" in quote @@ -549,35 +625,41 @@ function PageBody({ id }: { id: string }) { } />
- ) + ); } export default function QuotePage() { - const intl = useIntl() - const { id } = useParams() - const quoteId = id ?? "" - const location = useLocation() - const state = location.state as LocationState | null - const fromPath = state?.from - const fromKeyset = fromPath?.startsWith("/keysets/") - const keysetIdFromState = fromKeyset && fromPath ? fromPath.split("/keysets/")[1] : null + const intl = useIntl(); + const { id } = useParams(); + const quoteId = id ?? ""; + const location = useLocation(); + const state = location.state as LocationState | null; + const fromPath = state?.from; + const fromKeyset = fromPath?.startsWith("/keysets/"); + const keysetIdFromState = + fromKeyset && fromPath ? fromPath.split("/keysets/")[1] : null; const { data: quoteData } = useQuery({ ...getQuoteOptions({ path: { qid: quoteId }, }), retry: 1, - }) + }); - const quoteDataStatus = quoteData?.status as string | undefined + const quoteDataStatus = quoteData?.status as string | undefined; const hasKeysetId = - quoteData && (quoteDataStatus === "Accepted" || quoteDataStatus === "MintingEnabled") && "keyset_id" in quoteData + quoteData && + (quoteDataStatus === "Accepted" || quoteDataStatus === "MintingEnabled") && + "keyset_id" in quoteData; return ( <> + {intl.formatMessage({ id: "quotes.breadcrumb", @@ -599,28 +681,46 @@ export default function QuotePage() { {truncateString(quoteId, 16)} {fromKeyset && keysetIdFromState ? ( - ) : hasKeysetId ? ( - ) : null} - ) + ); } diff --git a/src/pages/quotes/StatusQuotePage.test.tsx b/src/pages/quotes/StatusQuotePage.test.tsx index 25e5b6d..e6783cf 100644 --- a/src/pages/quotes/StatusQuotePage.test.tsx +++ b/src/pages/quotes/StatusQuotePage.test.tsx @@ -1,65 +1,70 @@ -import { act, type ReactElement } from "react" -import { createRoot, type Root } from "react-dom/client" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { IntlProvider } from "react-intl" -import { MemoryRouter } from "react-router" -import StatusQuotePage from "./StatusQuotePage" +import { act, type ReactElement } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IntlProvider } from "react-intl"; +import { MemoryRouter } from "react-router"; +import StatusQuotePage from "./StatusQuotePage"; interface QueryKeyEntry { - _id: string - path?: { qid: string } + _id: string; + path?: { qid: string }; } interface QueryOptions { - queryKey: QueryKeyEntry[] + queryKey: QueryKeyEntry[]; } interface QueryResult { - data: unknown - isLoading: boolean - isFetching?: boolean - error: Error | null + data: unknown; + isLoading: boolean; + isFetching?: boolean; + error: Error | null; } interface UseQueriesArgs { - queries: { queryKey?: { path?: { qid: string } }[] }[] + queries: { queryKey?: { path?: { qid: string } }[] }[]; } interface UseQueriesResultItem { - data: unknown - isLoading: boolean + data: unknown; + isLoading: boolean; } -const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>() -const mockUseQueries = vi.fn<(args: UseQueriesArgs) => UseQueriesResultItem[]>() +const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>(); +const mockUseQueries = + vi.fn<(args: UseQueriesArgs) => UseQueriesResultItem[]>(); vi.mock("sonner", () => ({ toast: { error: vi.fn() }, -})) +})); vi.mock("@tanstack/react-query", async () => { - const actual = await vi.importActual("@tanstack/react-query") + const actual = await vi.importActual( + "@tanstack/react-query", + ); return { ...actual, useQuery: (options: QueryOptions) => mockUseQuery(options), useQueries: (args: UseQueriesArgs) => mockUseQueries(args), - } -}) + }; +}); vi.mock("@/generated/client/@tanstack/react-query.gen", () => ({ listQuotesOptions: () => ({ queryKey: [{ _id: "listQuotes" }] }), - getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ queryKey: [{ _id: "getQuote", path }] }), -})) + getQuoteOptions: ({ path }: { path: { qid: string } }) => ({ + queryKey: [{ _id: "getQuote", path }], + }), +})); -let root: Root | null = null -let container: HTMLDivElement | null = null +let root: Root | null = null; +let container: HTMLDivElement | null = null; function renderIntoDom(element: ReactElement): HTMLDivElement { - const mount = document.createElement("div") - document.body.appendChild(mount) - const mountRoot = createRoot(mount) + const mount = document.createElement("div"); + document.body.appendChild(mount); + const mountRoot = createRoot(mount); act(() => { - mountRoot.render(element) - }) - root = mountRoot - container = mount - return mount + mountRoot.render(element); + }); + root = mountRoot; + container = mount; + return mount; } function renderPage(status?: "Accepted" | "Pending"): HTMLDivElement { @@ -69,22 +74,22 @@ function renderPage(status?: "Accepted" | "Pending"): HTMLDivElement { , - ) + ); } beforeEach(() => { - vi.clearAllMocks() + vi.clearAllMocks(); if (root && container) { act(() => { - root?.unmount() - }) - container.remove() - root = null - container = null + root?.unmount(); + }); + container.remove(); + root = null; + container = null; } mockUseQuery.mockImplementation((opts: QueryOptions) => { - const id = opts.queryKey[0]._id + const id = opts.queryKey[0]._id; if (id === "listQuotes") { return { data: { @@ -96,7 +101,7 @@ beforeEach(() => { isLoading: false, isFetching: false, error: null, - } + }; } if (id === "getQuote") { @@ -113,11 +118,16 @@ beforeEach(() => { }, isLoading: false, error: null, - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); mockUseQueries.mockImplementation(({ queries }: UseQueriesArgs) => queries.map((query) => ({ @@ -129,21 +139,21 @@ beforeEach(() => { }, isLoading: false, })), - ) -}) + ); +}); describe("StatusQuotePage", () => { it("shows all quotes page title when no status filter is passed", () => { - const page = renderPage() - expect(page.textContent).toContain("All quotes") - }) + const page = renderPage(); + expect(page.textContent).toContain("All quotes"); + }); it("filters cards by status", () => { - const page = renderPage("Accepted") - expect(page.textContent).toContain("Accepted quotes") - expect(page.textContent).toContain("quote-accepted") - expect(page.textContent).not.toContain("quote-pending") - }) + const page = renderPage("Accepted"); + expect(page.textContent).toContain("Accepted quotes"); + expect(page.textContent).toContain("quote-accepted"); + expect(page.textContent).not.toContain("quote-pending"); + }); it("shows API error state when quotes query fails", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { @@ -153,16 +163,21 @@ describe("StatusQuotePage", () => { isLoading: false, isFetching: false, error: new Error("network down"), - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) - mockUseQueries.mockReturnValue([]) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); + mockUseQueries.mockReturnValue([]); - const page = renderPage() - expect(page.textContent).toContain("Failed to load quotes") - expect(page.textContent).toContain("network down") - }) + const page = renderPage(); + expect(page.textContent).toContain("Failed to load quotes"); + expect(page.textContent).toContain("network down"); + }); it("shows empty state when quotes list is empty", () => { mockUseQuery.mockImplementation((opts: QueryOptions) => { @@ -172,13 +187,18 @@ describe("StatusQuotePage", () => { isLoading: false, isFetching: false, error: null, - } + }; } - return { data: undefined, isLoading: false, isFetching: false, error: null } - }) - mockUseQueries.mockReturnValue([]) - - const page = renderPage() - expect(page.textContent).toContain("No quotes available.") - }) -}) + return { + data: undefined, + isLoading: false, + isFetching: false, + error: null, + }; + }); + mockUseQueries.mockReturnValue([]); + + const page = renderPage(); + expect(page.textContent).toContain("No quotes available."); + }); +}); diff --git a/src/pages/quotes/StatusQuotePage.tsx b/src/pages/quotes/StatusQuotePage.tsx index 6fb76eb..0ce3a5a 100644 --- a/src/pages/quotes/StatusQuotePage.tsx +++ b/src/pages/quotes/StatusQuotePage.tsx @@ -1,26 +1,33 @@ -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Button } from "@/components/ui/button" -import { Card, CardTitle } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" -import { listQuotesOptions, getQuoteOptions } from "@/generated/client/@tanstack/react-query.gen" -import { useQuery, useQueries } from "@tanstack/react-query" -import { LoaderIcon } from "lucide-react" -import { Link, useNavigate } from "react-router" -import { formatNumber, truncateString, formatStatusLabel } from "@/utils/strings" -import { getQuoteStatusVariant } from "@/utils/quote-status" -import { Badge } from "@/components/ui/badge" -import { cn } from "@/lib/utils" -import type { LightInfo } from "@/generated/client/types.gen" -import { ParticipantsOverviewCard } from "@/components/ParticipantsOverview" -import { toast } from "sonner" -import * as React from "react" -import SearchComponent, { HighlightText } from "@/components/ui/search" -import { useState } from "react" -import { BreadcrumbLink } from "@/components/ui/breadcrumb" -import { SortButtons } from "@/components/SortButtons" -import { useIntl } from "react-intl" -import { getApiErrorMessage } from "@/lib/api-error" +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Button } from "@/components/ui/button"; +import { Card, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + listQuotesOptions, + getQuoteOptions, +} from "@/generated/client/@tanstack/react-query.gen"; +import { useQuery, useQueries } from "@tanstack/react-query"; +import { LoaderIcon } from "lucide-react"; +import { Link, useNavigate } from "react-router"; +import { + formatNumber, + truncateString, + formatStatusLabel, +} from "@/utils/strings"; +import { getQuoteStatusVariant } from "@/utils/quote-status"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { LightInfo } from "@/generated/client/types.gen"; +import { ParticipantsOverviewCard } from "@/components/ParticipantsOverview"; +import { toast } from "sonner"; +import * as React from "react"; +import SearchComponent, { HighlightText } from "@/components/ui/search"; +import { useState } from "react"; +import { BreadcrumbLink } from "@/components/ui/breadcrumb"; +import { SortButtons } from "@/components/SortButtons"; +import { useIntl } from "react-intl"; +import { getApiErrorMessage } from "@/lib/api-error"; type QuoteStatus = | "Accepted" @@ -30,14 +37,20 @@ type QuoteStatus = | "Pending" | "Rejected" | "Canceled" - | "MintingEnabled" -type SortBy = "status-asc" | "status-desc" | "sum-asc" | "sum-desc" | "maturity-asc" | "maturity-desc" + | "MintingEnabled"; +type SortBy = + | "status-asc" + | "status-desc" + | "sum-asc" + | "sum-desc" + | "maturity-asc" + | "maturity-desc"; -const RETRY_COUNT = 2 -const retryDelay = (attempt: number) => Math.min(1000 * 2 ** attempt, 10_000) +const RETRY_COUNT = 2; +const retryDelay = (attempt: number) => Math.min(1000 * 2 ** attempt, 10_000); interface StatusQuotePageProps { - status?: QuoteStatus + status?: QuoteStatus; } function Loader() { @@ -51,12 +64,18 @@ function Loader() { - ) + ); } -function QuoteItemCard({ quote, searchQuery }: { quote: LightInfo; searchQuery: string }) { - const intl = useIntl() - const navigate = useNavigate() +function QuoteItemCard({ + quote, + searchQuery, +}: { + quote: LightInfo; + searchQuery: string; +}) { + const intl = useIntl(); + const navigate = useNavigate(); const queryResult = useQuery({ ...getQuoteOptions({ @@ -65,35 +84,48 @@ function QuoteItemCard({ quote, searchQuery }: { quote: LightInfo; searchQuery: retry: RETRY_COUNT, retryDelay, enabled: !!quote.id, - }) + }); - const { data: quoteDetails, isLoading: isLoadingDetails, error: detailsError } = queryResult - const bill = quoteDetails?.bill + const { + data: quoteDetails, + isLoading: isLoadingDetails, + error: detailsError, + } = queryResult; + const bill = quoteDetails?.bill; const handleQuoteClick = (e: React.MouseEvent) => { if (detailsError) { - e.preventDefault() - const errorMessage = getApiErrorMessage(detailsError) - toast.error(intl.formatMessage({ id: "quotes.card.error.title", defaultMessage: "Cannot load quote" }), { - description: intl.formatMessage( - { - id: "quotes.card.error.description", - defaultMessage: "Quote {id} is unavailable. {message}", - }, - { - id: truncateString(quote.id, 12), - message: - errorMessage || - intl.formatMessage({ id: "quotes.error.tryAgain", defaultMessage: "Please try again later." }), - }, - ), - id: `quote-error-${quote.id}`, - duration: 5000, - }) + e.preventDefault(); + const errorMessage = getApiErrorMessage(detailsError); + toast.error( + intl.formatMessage({ + id: "quotes.card.error.title", + defaultMessage: "Cannot load quote", + }), + { + description: intl.formatMessage( + { + id: "quotes.card.error.description", + defaultMessage: "Quote {id} is unavailable. {message}", + }, + { + id: truncateString(quote.id, 12), + message: + errorMessage || + intl.formatMessage({ + id: "quotes.error.tryAgain", + defaultMessage: "Please try again later.", + }), + }, + ), + id: `quote-error-${quote.id}`, + duration: 5000, + }, + ); } else { - void navigate(`/quotes/${quote.id}`) + void navigate(`/quotes/${quote.id}`); } - } + }; return ( @@ -101,8 +133,14 @@ function QuoteItemCard({ quote, searchQuery }: { quote: LightInfo; searchQuery:
- - + + @@ -110,7 +148,10 @@ function QuoteItemCard({ quote, searchQuery }: { quote: LightInfo; searchQuery:
- +
-
- ) + ); } function QuoteList({ status }: { status?: QuoteStatus }) { - const intl = useIntl() - const [searchQuery, setSearchQuery] = useState("") - const [sortBy, setSortBy] = useState("maturity-asc") + const intl = useIntl(); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState("maturity-asc"); const { data, isFetching, error, isLoading } = useQuery({ ...listQuotesOptions(), retry: RETRY_COUNT, retryDelay, - }) + }); /* TODO: optimize this with pagination or batch fetching if API supports it */ const quoteDetailsQueries = useQueries({ @@ -191,15 +236,16 @@ function QuoteList({ status }: { status?: QuoteStatus }) { retryDelay, enabled: !!quote.id, })), - }) + }); const noQuotesMessage = intl.formatMessage({ id: "quotes.list.empty", defaultMessage: "No quotes available.", - }) + }); if (error) { - const errorMessage = (error as { message?: string }).message ?? String(error) + const errorMessage = + (error as { message?: string }).message ?? String(error); return (
@@ -222,71 +268,85 @@ function QuoteList({ status }: { status?: QuoteStatus }) { })}
- ) + ); } if (isLoading) { - return + return ; } const filteredQuotes = data?.quotes.filter((quote) => { if (status && quote.status !== status) { - return false + return false; } if (!searchQuery) { - return true + return true; } - const query = searchQuery.toLowerCase() - const quoteId = quote.id.toLowerCase() - const quoteStatus = quote.status.toLowerCase() - const quoteSum = quote.sum.toString() + const query = searchQuery.toLowerCase(); + const quoteId = quote.id.toLowerCase(); + const quoteStatus = quote.status.toLowerCase(); + const quoteSum = quote.sum.toString(); - return quoteId.includes(query) || quoteStatus.includes(query) || quoteSum.includes(query) - }) ?? [] + return ( + quoteId.includes(query) || + quoteStatus.includes(query) || + quoteSum.includes(query) + ); + }) ?? []; const sortedQuotes = [...filteredQuotes].sort((a, b) => { - const aIndex = data?.quotes.findIndex((q) => q.id === a.id) ?? -1 - const bIndex = data?.quotes.findIndex((q) => q.id === b.id) ?? -1 + const aIndex = data?.quotes.findIndex((q) => q.id === a.id) ?? -1; + const bIndex = data?.quotes.findIndex((q) => q.id === b.id) ?? -1; - const aBill = aIndex >= 0 ? quoteDetailsQueries[aIndex]?.data?.bill : null - const bBill = bIndex >= 0 ? quoteDetailsQueries[bIndex]?.data?.bill : null + const aBill = aIndex >= 0 ? quoteDetailsQueries[aIndex]?.data?.bill : null; + const bBill = bIndex >= 0 ? quoteDetailsQueries[bIndex]?.data?.bill : null; switch (sortBy) { case "status-asc": - return a.status.localeCompare(b.status) + return a.status.localeCompare(b.status); case "status-desc": - return b.status.localeCompare(a.status) + return b.status.localeCompare(a.status); case "sum-asc": - return a.sum - b.sum + return a.sum - b.sum; case "sum-desc": - return b.sum - a.sum + return b.sum - a.sum; case "maturity-asc": { - if (!aBill?.maturity_date && !bBill?.maturity_date) return 0 - if (!aBill?.maturity_date) return 1 - if (!bBill?.maturity_date) return -1 - return new Date(aBill.maturity_date).getTime() - new Date(bBill.maturity_date).getTime() + if (!aBill?.maturity_date && !bBill?.maturity_date) return 0; + if (!aBill?.maturity_date) return 1; + if (!bBill?.maturity_date) return -1; + return ( + new Date(aBill.maturity_date).getTime() - + new Date(bBill.maturity_date).getTime() + ); } case "maturity-desc": { - if (!aBill?.maturity_date && !bBill?.maturity_date) return 0 - if (!aBill?.maturity_date) return 1 - if (!bBill?.maturity_date) return -1 - return new Date(bBill.maturity_date).getTime() - new Date(aBill.maturity_date).getTime() + if (!aBill?.maturity_date && !bBill?.maturity_date) return 0; + if (!aBill?.maturity_date) return 1; + if (!bBill?.maturity_date) return -1; + return ( + new Date(bBill.maturity_date).getTime() - + new Date(aBill.maturity_date).getTime() + ); } default: - return 0 + return 0; } - }) + }); const toggleSort = (field: "status" | "sum" | "maturity") => { if (sortBy.startsWith(field)) { - setSortBy(sortBy.endsWith("asc") ? (`${field}-desc` as SortBy) : (`${field}-asc` as SortBy)) + setSortBy( + sortBy.endsWith("asc") + ? (`${field}-desc` as SortBy) + : (`${field}-asc` as SortBy), + ); } else { - setSortBy(`${field}-asc` as SortBy) + setSortBy(`${field}-asc` as SortBy); } - } + }; const sortOptions = [ { @@ -310,7 +370,7 @@ function QuoteList({ status }: { status?: QuoteStatus }) { defaultMessage: "Status", }), }, - ] + ]; return ( <> @@ -326,7 +386,11 @@ function QuoteList({ status }: { status?: QuoteStatus }) { onChange={setSearchQuery} size="sm" /> - +
@@ -347,20 +411,25 @@ function QuoteList({ status }: { status?: QuoteStatus }) { })}
)} - {sortedQuotes.length === 0 && !searchQuery &&
{noQuotesMessage}
} + {sortedQuotes.length === 0 && !searchQuery && ( +
{noQuotesMessage}
+ )} {sortedQuotes.map((quote, index) => { if (!quote.id) { - console.warn(`Quote at index ${index} is missing an ID:`, quote) + console.warn(`Quote at index ${index} is missing an ID:`, quote); } return (
- +
- ) + ); })}
- ) + ); } function PageBody({ status }: { status?: QuoteStatus }) { @@ -370,17 +439,17 @@ function PageBody({ status }: { status?: QuoteStatus }) {
- ) + ); } export default function StatusQuotePage({ status }: StatusQuotePageProps) { - const intl = useIntl() + const intl = useIntl(); const statusLabel = status ? intl.formatMessage({ id: `quote.status.${status}`, defaultMessage: formatStatusLabel(status), }) - : undefined + : undefined; const pageTitle = status ? intl.formatMessage( { @@ -392,7 +461,7 @@ export default function StatusQuotePage({ status }: StatusQuotePageProps) { : intl.formatMessage({ id: "quotes.statusPage.titleAll", defaultMessage: "All quotes", - }) + }); return ( <> @@ -400,7 +469,10 @@ export default function StatusQuotePage({ status }: StatusQuotePageProps) { parents={ status ? [ - + {intl.formatMessage({ id: "quotes.breadcrumb", @@ -422,5 +494,5 @@ export default function StatusQuotePage({ status }: StatusQuotePageProps) { {pageTitle} - ) + ); } diff --git a/src/pages/quotes/components/CalendarModal.tsx b/src/pages/quotes/components/CalendarModal.tsx index 64e59cb..00b5a57 100644 --- a/src/pages/quotes/components/CalendarModal.tsx +++ b/src/pages/quotes/components/CalendarModal.tsx @@ -1,29 +1,49 @@ -import { Button } from "@/components/ui/button.tsx" -import { Calendar } from "@/components/DatePicker/calendar.tsx" -import { CalendarIcon } from "lucide-react" -import { addDays, isAfter, isBefore, isSameDay } from "date-fns" -import { cn } from "@/lib/utils.ts" -import { useIntl } from "react-intl" -import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters" +import { Button } from "@/components/ui/button.tsx"; +import { Calendar } from "@/components/DatePicker/calendar.tsx"; +import { CalendarIcon } from "lucide-react"; +import { addDays, isAfter, isBefore, isSameDay } from "date-fns"; +import { cn } from "@/lib/utils.ts"; +import { useIntl } from "react-intl"; +import { useUtcDateFormatters } from "@/hooks/use-utc-date-formatters"; interface CalendarModalProps { - isOpen: boolean - selectedDate?: Date - draftDate?: Date - title: string - minDate?: Date - maxDate?: Date - onClose: () => void - onDateChange: (date: Date) => void - onConfirm: () => void - onCancel: () => void + isOpen: boolean; + selectedDate?: Date; + draftDate?: Date; + title: string; + minDate?: Date; + maxDate?: Date; + onClose: () => void; + onDateChange: (date: Date) => void; + onConfirm: () => void; + onCancel: () => void; } const toUtcStartOfDay = (date: Date) => - new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0)) + new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + 0, + 0, + 0, + 0, + ), + ); const toUtcEndOfDay = (date: Date) => - new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999)) + new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + 23, + 59, + 59, + 999, + ), + ); export function CalendarModal({ isOpen, @@ -37,13 +57,14 @@ export function CalendarModal({ onConfirm, onCancel, }: CalendarModalProps) { - const intl = useIntl() - const { formatDateMmmDdYyyy } = useUtcDateFormatters(intl.locale) - const fallbackMin = addDays(new Date(Date.now()), 1) - const minDay = toUtcStartOfDay(minDate ?? fallbackMin) - const maxDay = maxDate ? toUtcEndOfDay(maxDate) : null - const disabled = (date: Date) => isBefore(date, minDay) || (maxDay ? isAfter(date, maxDay) : false) - const displayMonth = draftDate ?? selectedDate ?? minDate ?? new Date() + const intl = useIntl(); + const { formatDateMmmDdYyyy } = useUtcDateFormatters(intl.locale); + const fallbackMin = addDays(new Date(Date.now()), 1); + const minDay = toUtcStartOfDay(minDate ?? fallbackMin); + const maxDay = maxDate ? toUtcEndOfDay(maxDate) : null; + const disabled = (date: Date) => + isBefore(date, minDay) || (maxDay ? isAfter(date, maxDay) : false); + const displayMonth = draftDate ?? selectedDate ?? minDate ?? new Date(); return ( <> @@ -67,7 +88,9 @@ export function CalendarModal({ >
{title}
-
{draftDate ? formatDateMmmDdYyyy(draftDate) : "-"}
+
+ {draftDate ? formatDateMmmDdYyyy(draftDate) : "-"} +
{ if (range?.from) { - onDateChange(range.from) + onDateChange(range.from); } }} disabled={disabled} @@ -102,7 +125,13 @@ export function CalendarModal({ defaultMessage: "Cancel", })} -
- ) + ); } interface DatePickerButtonProps { - date?: Date - onClick: () => void + date?: Date; + onClick: () => void; } export function DatePickerButton({ date, onClick }: DatePickerButtonProps) { - const intl = useIntl() - const { formatDateMmmDdYyyy } = useUtcDateFormatters(intl.locale) + const intl = useIntl(); + const { formatDateMmmDdYyyy } = useUtcDateFormatters(intl.locale); return ( - ) + ); } diff --git a/src/pages/quotes/components/DenyConfirmDrawer.tsx b/src/pages/quotes/components/DenyConfirmDrawer.tsx index 8a46e7e..2c8dd54 100644 --- a/src/pages/quotes/components/DenyConfirmDrawer.tsx +++ b/src/pages/quotes/components/DenyConfirmDrawer.tsx @@ -1,23 +1,30 @@ -import { ConfirmDrawer } from "@/components/Drawers.tsx" -import type { ReactNode } from "react" -import { useIntl } from "react-intl" +import { ConfirmDrawer } from "@/components/Drawers.tsx"; +import type { ReactNode } from "react"; +import { useIntl } from "react-intl"; interface DenyConfirmDrawerProps { - title: string - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: () => void - children: ReactNode + title: string; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: () => void; + children: ReactNode; } -export function DenyConfirmDrawer({ title, open, onOpenChange, onSubmit, children }: DenyConfirmDrawerProps) { - const intl = useIntl() +export function DenyConfirmDrawer({ + title, + open, + onOpenChange, + onSubmit, + children, +}: DenyConfirmDrawerProps) { + const intl = useIntl(); return ( - ) + ); } diff --git a/src/pages/quotes/components/OfferConfirmation.tsx b/src/pages/quotes/components/OfferConfirmation.tsx index 5c5f45a..3880df1 100644 --- a/src/pages/quotes/components/OfferConfirmation.tsx +++ b/src/pages/quotes/components/OfferConfirmation.tsx @@ -1,58 +1,78 @@ -import { useEffect, useMemo, useState } from "react" -import { ConfirmDrawer } from "@/components/Drawers.tsx" -import { CalendarModal, DatePickerButton } from "./CalendarModal.tsx" -import Big from "big.js" -import type { OfferFormResult } from "./OfferFormDrawer.tsx" -import { addDays, addYears } from "date-fns" -import { getItem, removeItem, setItem } from "@/utils/local-storage" -import { useIntl } from "react-intl" +import { useEffect, useMemo, useState } from "react"; +import { ConfirmDrawer } from "@/components/Drawers.tsx"; +import { CalendarModal, DatePickerButton } from "./CalendarModal.tsx"; +import Big from "big.js"; +import type { OfferFormResult } from "./OfferFormDrawer.tsx"; +import { addDays, addYears } from "date-fns"; +import { getItem, removeItem, setItem } from "@/utils/local-storage"; +import { useIntl } from "react-intl"; interface OfferConfirmationProps { - offerFormData?: OfferFormResult - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (data: OfferFormResult) => void - quoteId?: string + offerFormData?: OfferFormResult; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: OfferFormResult) => void; + quoteId?: string; } -const OFFER_VALID_UNTIL_STORAGE_KEY_PREFIX = "offer-valid-until-" +const OFFER_VALID_UNTIL_STORAGE_KEY_PREFIX = "offer-valid-until-"; -export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, quoteId }: OfferConfirmationProps) { - const intl = useIntl() - const [validUntilDate, setValidUntilDate] = useState(undefined) - const [showValidUntilCalendar, setShowValidUntilCalendar] = useState(false) - const [draftValidUntilDate, setDraftValidUntilDate] = useState(undefined) +export function OfferConfirmation({ + offerFormData, + open, + onOpenChange, + onSubmit, + quoteId, +}: OfferConfirmationProps) { + const intl = useIntl(); + const [validUntilDate, setValidUntilDate] = useState( + undefined, + ); + const [showValidUntilCalendar, setShowValidUntilCalendar] = useState(false); + const [draftValidUntilDate, setDraftValidUntilDate] = useState< + Date | undefined + >(undefined); const minDate = useMemo(() => { - const date = addDays(new Date(), 1) - date.setHours(0, 0, 0, 0) - return date - }, []) + const date = addDays(new Date(), 1); + date.setHours(0, 0, 0, 0); + return date; + }, []); const maxDate = useMemo(() => { - const date = addYears(new Date(), 1) - date.setHours(23, 59, 59, 999) - return date - }, []) - const storageKey = quoteId ? `${OFFER_VALID_UNTIL_STORAGE_KEY_PREFIX}${quoteId}` : null + const date = addYears(new Date(), 1); + date.setHours(23, 59, 59, 999); + return date; + }, []); + const storageKey = quoteId + ? `${OFFER_VALID_UNTIL_STORAGE_KEY_PREFIX}${quoteId}` + : null; useEffect(() => { if (!open || validUntilDate || !storageKey) { - return + return; } - const stored = getItem(storageKey) + const stored = getItem(storageKey); if (!stored) { - return + return; } - const parsed = new Date(stored) - if (!Number.isNaN(parsed.getTime()) && parsed >= minDate && parsed <= maxDate) { - setValidUntilDate(parsed) + const parsed = new Date(stored); + if ( + !Number.isNaN(parsed.getTime()) && + parsed >= minDate && + parsed <= maxDate + ) { + setValidUntilDate(parsed); } else { - removeItem(storageKey) + removeItem(storageKey); } - }, [open, validUntilDate, storageKey, minDate, maxDate]) + }, [open, validUntilDate, storageKey, minDate, maxDate]); const effectiveDiscount = offerFormData - ? new Big(1).minus(offerFormData.discount.net.value.div(offerFormData.discount.gross.value)) - : undefined + ? new Big(1).minus( + offerFormData.discount.net.value.div( + offerFormData.discount.gross.value, + ), + ) + : undefined; return ( <> @@ -67,17 +87,20 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, })} open={open} onOpenChange={(isOpen) => { - onOpenChange(isOpen) + onOpenChange(isOpen); }} submitButtonDisabled={!validUntilDate} onSubmit={() => { if (!offerFormData || !validUntilDate) { - return + return; } - const finalOfferData = { ...offerFormData, ttl: { ttl: validUntilDate } } + const finalOfferData = { + ...offerFormData, + ttl: { ttl: validUntilDate }, + }; - onSubmit(finalOfferData) + onSubmit(finalOfferData); }} >
@@ -88,7 +111,9 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, defaultMessage: "Effective fee (relative):", })} - {effectiveDiscount?.mul(new Big("100")).toFixed(2)}% + + {effectiveDiscount?.mul(new Big("100")).toFixed(2)}% +
@@ -98,7 +123,9 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, })} - {offerFormData?.discount.gross.value.minus(offerFormData?.discount.net.value).toFixed(0)}{" "} + {offerFormData?.discount.gross.value + .minus(offerFormData?.discount.net.value) + .toFixed(0)}{" "} {offerFormData?.discount.net.currency}
@@ -110,7 +137,8 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, })} - {offerFormData?.discount.net.value.round(0).toFixed(0)} {offerFormData?.discount.net.currency} + {offerFormData?.discount.net.value.round(0).toFixed(0)}{" "} + {offerFormData?.discount.net.currency}
@@ -123,9 +151,9 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, { - setDraftValidUntilDate(validUntilDate) - onOpenChange(false) - setShowValidUntilCalendar(true) + setDraftValidUntilDate(validUntilDate); + onOpenChange(false); + setShowValidUntilCalendar(true); }} />
@@ -143,26 +171,26 @@ export function OfferConfirmation({ offerFormData, open, onOpenChange, onSubmit, minDate={minDate} maxDate={maxDate} onClose={() => { - setShowValidUntilCalendar(false) - onOpenChange(true) + setShowValidUntilCalendar(false); + onOpenChange(true); }} onDateChange={setDraftValidUntilDate} onConfirm={() => { if (draftValidUntilDate) { - setValidUntilDate(draftValidUntilDate) + setValidUntilDate(draftValidUntilDate); if (storageKey) { - setItem(storageKey, draftValidUntilDate.toISOString()) + setItem(storageKey, draftValidUntilDate.toISOString()); } } - setShowValidUntilCalendar(false) - onOpenChange(true) + setShowValidUntilCalendar(false); + onOpenChange(true); }} onCancel={() => { - setShowValidUntilCalendar(false) - setDraftValidUntilDate(undefined) - onOpenChange(true) + setShowValidUntilCalendar(false); + setDraftValidUntilDate(undefined); + onOpenChange(true); }} /> - ) + ); } diff --git a/src/pages/quotes/components/OfferFormDrawer.tsx b/src/pages/quotes/components/OfferFormDrawer.tsx index 8fcf77b..899957f 100644 --- a/src/pages/quotes/components/OfferFormDrawer.tsx +++ b/src/pages/quotes/components/OfferFormDrawer.tsx @@ -1,35 +1,35 @@ -import Big from "big.js" -import { BaseDrawer } from "@/components/Drawers.tsx" -import { GrossToNetDiscountForm } from "@/components/GrossToNetDiscountForm.tsx" -import type { InfoReply } from "@/generated/client/types.gen.ts" -import type { ReactNode } from "react" +import Big from "big.js"; +import { BaseDrawer } from "@/components/Drawers.tsx"; +import { GrossToNetDiscountForm } from "@/components/GrossToNetDiscountForm.tsx"; +import type { InfoReply } from "@/generated/client/types.gen.ts"; +import type { ReactNode } from "react"; export interface OfferFormResult { discount: { - days: number - discountRate: Big + days: number; + discountRate: Big; net: { - value: Big - currency: string - } + value: Big; + currency: string; + }; gross: { - value: Big - currency: string - } - } + value: Big; + currency: string; + }; + }; ttl: { - ttl: Date - } + ttl: Date; + }; } interface OfferFormDrawerProps { - title: string - description: string - value: InfoReply - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (data: OfferFormResult) => void - children: ReactNode + title: string; + description: string; + value: InfoReply; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: OfferFormResult) => void; + children: ReactNode; } export function OfferFormDrawer({ @@ -41,29 +41,40 @@ export function OfferFormDrawer({ onSubmit, children, }: OfferFormDrawerProps) { - const addDays = 30 * 24 * 60 * 60 * 1000 + const addDays = 30 * 24 * 60 * 60 * 1000; const handleFormSubmit = (values: { - days: number - discountRate: Big - net: { value: Big; currency: string } - gross: { value: Big; currency: string } + days: number; + discountRate: Big; + net: { value: Big; currency: string }; + gross: { value: Big; currency: string }; }) => { - const endDate = value.status === "Pending" ? new Date(value.suggested_expiration) : new Date(Date.now() + addDays) + const endDate = + value.status === "Pending" + ? new Date(value.suggested_expiration) + : new Date(Date.now() + addDays); const result: OfferFormResult = { discount: values, ttl: { ttl: endDate }, - } + }; - onSubmit(result) - } + onSubmit(result); + }; - const startDate = new Date() - const endDate = value.bill.maturity_date ? new Date(value.bill.maturity_date) : new Date() + const startDate = new Date(); + const endDate = value.bill.maturity_date + ? new Date(value.bill.maturity_date) + : new Date(); return ( - + - ) + ); } diff --git a/src/pages/quotes/components/PaymentRequestCard.tsx b/src/pages/quotes/components/PaymentRequestCard.tsx index aae0503..3ebbf08 100644 --- a/src/pages/quotes/components/PaymentRequestCard.tsx +++ b/src/pages/quotes/components/PaymentRequestCard.tsx @@ -1,11 +1,11 @@ -import { TruncatedTextPopover } from "@/components/TruncatedTextPopover.tsx" -import { useIntl } from "react-intl" +import { TruncatedTextPopover } from "@/components/TruncatedTextPopover.tsx"; +import { useIntl } from "react-intl"; interface PaymentRequestCardProps { - addressToPay?: string - linkToPay?: string - effectiveRequestTime: number | null - effectiveDeadlineTs: number | null + addressToPay?: string; + linkToPay?: string; + effectiveRequestTime: number | null; + effectiveDeadlineTs: number | null; } export function PaymentRequestCard({ @@ -14,7 +14,7 @@ export function PaymentRequestCard({ effectiveRequestTime, effectiveDeadlineTs, }: PaymentRequestCardProps) { - const intl = useIntl() + const intl = useIntl(); return (

@@ -32,7 +32,11 @@ export function PaymentRequestCard({ defaultMessage: "Address to pay", })} - +

)} {linkToPay && ( @@ -49,7 +53,11 @@ export function PaymentRequestCard({ rel="noopener noreferrer" className="text-blue-600 hover:underline flex items-center" > - + )} @@ -62,7 +70,10 @@ export function PaymentRequestCard({ })} - {new Date(effectiveRequestTime * 1000).toLocaleString(intl.locale, { timeZone: "UTC" })} + {new Date(effectiveRequestTime * 1000).toLocaleString( + intl.locale, + { timeZone: "UTC" }, + )} )} @@ -75,11 +86,14 @@ export function PaymentRequestCard({ })} - {new Date(effectiveDeadlineTs * 1000).toLocaleString(intl.locale, { timeZone: "UTC" })} + {new Date(effectiveDeadlineTs * 1000).toLocaleString( + intl.locale, + { timeZone: "UTC" }, + )} )} - ) + ); } diff --git a/src/pages/quotes/components/RequestToPayConfirmation.tsx b/src/pages/quotes/components/RequestToPayConfirmation.tsx index 95327c4..72df31d 100644 --- a/src/pages/quotes/components/RequestToPayConfirmation.tsx +++ b/src/pages/quotes/components/RequestToPayConfirmation.tsx @@ -1,36 +1,46 @@ -import { useEffect, useMemo, useState } from "react" -import { Button } from "@/components/ui/button.tsx" -import { ConfirmDrawer } from "@/components/Drawers.tsx" -import { LoaderIcon } from "lucide-react" -import { CalendarModal, DatePickerButton } from "./CalendarModal.tsx" -import { useQuery } from "@tanstack/react-query" -import { getEbillOptions } from "@/generated/client/@tanstack/react-query.gen" -import { useIntl } from "react-intl" -import { getItem, setItem } from "@/utils/local-storage" +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button.tsx"; +import { ConfirmDrawer } from "@/components/Drawers.tsx"; +import { LoaderIcon } from "lucide-react"; +import { CalendarModal, DatePickerButton } from "./CalendarModal.tsx"; +import { useQuery } from "@tanstack/react-query"; +import { getEbillOptions } from "@/generated/client/@tanstack/react-query.gen"; +import { useIntl } from "react-intl"; +import { getItem, setItem } from "@/utils/local-storage"; interface RequestToPayConfirmationProps { - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (deadline: Date) => void - isFetching: boolean - isPending: boolean - maturityDate?: string | null - billId: string + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (deadline: Date) => void; + isFetching: boolean; + isPending: boolean; + maturityDate?: string | null; + billId: string; } -const REQUEST_TO_PAY_DEADLINE_STORAGE_KEY = "requestToPayDeadlineUtc" -const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000 +const REQUEST_TO_PAY_DEADLINE_STORAGE_KEY = "requestToPayDeadlineUtc"; +const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000; const getMinSelectableDate = (maturityDate?: string | null): Date => { - const now = new Date() - const maturity = maturityDate ? new Date(maturityDate) : null - const baseDate = maturity && maturity > now ? maturity : now - return new Date(baseDate.getTime() + TWO_DAYS_MS) -} + const now = new Date(); + const maturity = maturityDate ? new Date(maturityDate) : null; + const baseDate = maturity && maturity > now ? maturity : now; + return new Date(baseDate.getTime() + TWO_DAYS_MS); +}; const toUtcEndOfDay = (date: Date): Date => { - return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999)) -} + return new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + 23, + 59, + 59, + 999, + ), + ); +}; export function RequestToPayConfirmation({ open, @@ -41,44 +51,55 @@ export function RequestToPayConfirmation({ maturityDate, billId, }: RequestToPayConfirmationProps) { - const intl = useIntl() - const [validUntilDate, setValidUntilDate] = useState(undefined) - const [showPaymentCalendar, setShowPaymentCalendar] = useState(false) - const [draftValidUntilDate, setDraftValidUntilDate] = useState(undefined) - const minSelectableDate = useMemo(() => getMinSelectableDate(maturityDate), [maturityDate]) + const intl = useIntl(); + const [validUntilDate, setValidUntilDate] = useState( + undefined, + ); + const [showPaymentCalendar, setShowPaymentCalendar] = useState(false); + const [draftValidUntilDate, setDraftValidUntilDate] = useState< + Date | undefined + >(undefined); + const minSelectableDate = useMemo( + () => getMinSelectableDate(maturityDate), + [maturityDate], + ); const ebillQuery = useQuery({ ...getEbillOptions({ path: { bid: billId } }), enabled: true, retry: 0, refetchInterval: (query) => { - return query.state.data ? false : 2000 + return query.state.data ? false : 2000; }, refetchIntervalInBackground: true, - }) + }); - const ebillAvailable = !ebillQuery.isLoading && !ebillQuery.error && !!ebillQuery.data + const ebillAvailable = + !ebillQuery.isLoading && !ebillQuery.error && !!ebillQuery.data; useEffect(() => { if (!open || validUntilDate) { - return + return; } - const stored = getItem(REQUEST_TO_PAY_DEADLINE_STORAGE_KEY) - const fallbackDeadline = toUtcEndOfDay(minSelectableDate) + const stored = getItem(REQUEST_TO_PAY_DEADLINE_STORAGE_KEY); + const fallbackDeadline = toUtcEndOfDay(minSelectableDate); if (stored) { - const parsed = new Date(stored) + const parsed = new Date(stored); if (!Number.isNaN(parsed.getTime()) && parsed >= minSelectableDate) { - setValidUntilDate(parsed) - setDraftValidUntilDate(parsed) - return + setValidUntilDate(parsed); + setDraftValidUntilDate(parsed); + return; } } - setValidUntilDate(fallbackDeadline) - setDraftValidUntilDate(fallbackDeadline) - setItem(REQUEST_TO_PAY_DEADLINE_STORAGE_KEY, fallbackDeadline.toISOString()) - }, [open, validUntilDate, minSelectableDate]) + setValidUntilDate(fallbackDeadline); + setDraftValidUntilDate(fallbackDeadline); + setItem( + REQUEST_TO_PAY_DEADLINE_STORAGE_KEY, + fallbackDeadline.toISOString(), + ); + }, [open, validUntilDate, minSelectableDate]); return ( <> @@ -89,15 +110,16 @@ export function RequestToPayConfirmation({ })} description={intl.formatMessage({ id: "quotes.requestToPay.confirmDescription", - defaultMessage: "Are you sure you want to request to pay this e-bill?", + defaultMessage: + "Are you sure you want to request to pay this e-bill?", })} open={open} onOpenChange={(isOpen) => { - onOpenChange(isOpen) + onOpenChange(isOpen); }} onSubmit={() => { if (validUntilDate) { - onSubmit(validUntilDate) + onSubmit(validUntilDate); } }} submitButtonText={intl.formatMessage({ @@ -111,9 +133,9 @@ export function RequestToPayConfirmation({ disabled={isFetching || isPending || !ebillAvailable} variant="default" onClick={(e) => { - e.preventDefault() - e.stopPropagation() - onOpenChange(true) + e.preventDefault(); + e.stopPropagation(); + onOpenChange(true); }} > {!ebillAvailable ? ( @@ -147,9 +169,9 @@ export function RequestToPayConfirmation({ { - setDraftValidUntilDate(validUntilDate) - onOpenChange(false) - setShowPaymentCalendar(true) + setDraftValidUntilDate(validUntilDate); + onOpenChange(false); + setShowPaymentCalendar(true); }} /> @@ -166,25 +188,28 @@ export function RequestToPayConfirmation({ })} minDate={minSelectableDate} onClose={() => { - setShowPaymentCalendar(false) - onOpenChange(true) + setShowPaymentCalendar(false); + onOpenChange(true); }} onDateChange={setDraftValidUntilDate} onConfirm={() => { if (draftValidUntilDate) { - const utcDeadline = toUtcEndOfDay(draftValidUntilDate) - setValidUntilDate(utcDeadline) - setItem(REQUEST_TO_PAY_DEADLINE_STORAGE_KEY, utcDeadline.toISOString()) + const utcDeadline = toUtcEndOfDay(draftValidUntilDate); + setValidUntilDate(utcDeadline); + setItem( + REQUEST_TO_PAY_DEADLINE_STORAGE_KEY, + utcDeadline.toISOString(), + ); } - setShowPaymentCalendar(false) - onOpenChange(true) + setShowPaymentCalendar(false); + onOpenChange(true); }} onCancel={() => { - setShowPaymentCalendar(false) - setDraftValidUntilDate(undefined) - onOpenChange(true) + setShowPaymentCalendar(false); + setDraftValidUntilDate(undefined); + onOpenChange(true); }} /> - ) + ); } diff --git a/src/pages/quotes/components/useQuoteMutations.ts b/src/pages/quotes/components/useQuoteMutations.ts index c267a75..ca9949f 100644 --- a/src/pages/quotes/components/useQuoteMutations.ts +++ b/src/pages/quotes/components/useQuoteMutations.ts @@ -1,83 +1,89 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { toast } from "sonner" +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import { updateQuoteMutation, postEbillReqtopayMutation, getQuoteOptions, getEbillOptions, -} from "@/generated/client/@tanstack/react-query.gen.ts" -import type { OfferFormResult } from "./OfferFormDrawer.tsx" -import Big from "big.js" -import { getApiErrorMessage } from "@/lib/api-error" +} from "@/generated/client/@tanstack/react-query.gen.ts"; +import type { OfferFormResult } from "./OfferFormDrawer.tsx"; +import Big from "big.js"; +import { getApiErrorMessage } from "@/lib/api-error"; export function useQuoteMutations(quoteId: string, billId: string) { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); const denyQuote = useMutation({ ...updateQuoteMutation(), onSettled: () => { - toast.dismiss(`quote-${quoteId}-deny`) + toast.dismiss(`quote-${quoteId}-deny`); }, onError: (error) => { - toast.error(`Error while denying quote: ${getApiErrorMessage(error)}`) - console.warn(error) + toast.error(`Error while denying quote: ${getApiErrorMessage(error)}`); + console.warn(error); }, onSuccess: () => { - toast.success("Quote has been denied.") + toast.success("Quote has been denied."); void queryClient.invalidateQueries({ queryKey: getQuoteOptions({ path: { qid: quoteId } }).queryKey, - }) + }); }, - }) + }); const offerQuote = useMutation({ ...updateQuoteMutation(), onSettled: () => { - toast.dismiss(`quote-${quoteId}-offer`) + toast.dismiss(`quote-${quoteId}-offer`); }, onError: (error) => { - toast.error(`Error while offering quote: ${getApiErrorMessage(error)}`) - console.warn(error) + toast.error(`Error while offering quote: ${getApiErrorMessage(error)}`); + console.warn(error); }, onSuccess: () => { - toast.success("Quote has been offered.") + toast.success("Quote has been offered."); void queryClient.invalidateQueries({ queryKey: getQuoteOptions({ path: { qid: quoteId } }).queryKey, - }) + }); }, - }) + }); const requestToPayMutation = useMutation({ ...postEbillReqtopayMutation(), onMutate: () => { - toast.loading("Requesting to pay…", { id: `quote-${quoteId}-request-to-pay` }) + toast.loading("Requesting to pay…", { + id: `quote-${quoteId}-request-to-pay`, + }); }, onSettled: () => { - toast.dismiss(`quote-${quoteId}-request-to-pay`) + toast.dismiss(`quote-${quoteId}-request-to-pay`); }, onError: (error) => { - toast.error(`Error while requesting to pay: ${getApiErrorMessage(error)}`) - console.warn(error) + toast.error( + `Error while requesting to pay: ${getApiErrorMessage(error)}`, + ); + console.warn(error); }, onSuccess: () => { - toast.success("Payment request has been created.") + toast.success("Payment request has been created."); void queryClient.invalidateQueries({ queryKey: getEbillOptions({ path: { bid: billId } }).queryKey, - }) + }); }, - }) + }); const handleDenyQuote = () => { - toast.loading("Denying quote…", { id: `quote-${quoteId}-deny` }) + toast.loading("Denying quote…", { id: `quote-${quoteId}-deny` }); denyQuote.mutate({ path: { qid: quoteId }, body: { action: "Deny" }, - }) - } + }); + }; const handleOfferQuote = (result: OfferFormResult) => { - toast.loading("Offering quote…", { id: `quote-${quoteId}-offer` }) - const net_amount = result.discount.net.value.round(0, Big.roundDown).toNumber() + toast.loading("Offering quote…", { id: `quote-${quoteId}-offer` }); + const net_amount = result.discount.net.value + .round(0, Big.roundDown) + .toNumber(); offerQuote.mutate({ path: { qid: quoteId }, @@ -86,8 +92,8 @@ export function useQuoteMutations(quoteId: string, billId: string) { discounted: net_amount, ttl: result.ttl.ttl.toISOString(), }, - }) - } + }); + }; const handleRequestToPay = (billSum: number, deadline: Date) => { requestToPayMutation.mutate({ @@ -96,8 +102,8 @@ export function useQuoteMutations(quoteId: string, billId: string) { amount: billSum, deadline: deadline.toISOString(), }, - }) - } + }); + }; return { denyQuote, @@ -106,5 +112,5 @@ export function useQuoteMutations(quoteId: string, billId: string) { handleDenyQuote, handleOfferQuote, handleRequestToPay, - } + }; } diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 0901ebc..ed8f3ab 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -1,14 +1,14 @@ -import { Breadcrumbs } from "@/components/Breadcrumbs" -import { PageTitle } from "@/components/PageTitle" -import { Skeleton } from "@/components/ui/skeleton" -import { Suspense } from "react" +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { PageTitle } from "@/components/PageTitle"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Suspense } from "react"; function Loader() { return (
- ) + ); } function PageBody() { @@ -16,7 +16,7 @@ function PageBody() { <>
- ) + ); } export default function SettingsPage() { @@ -29,5 +29,5 @@ export default function SettingsPage() { - ) + ); } diff --git a/src/utils/dates.test.ts b/src/utils/dates.test.ts index f43a2d2..7925bf6 100644 --- a/src/utils/dates.test.ts +++ b/src/utils/dates.test.ts @@ -1,55 +1,61 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" -import { daysBetween, formatDate, getDefaultDeadline, humanReadableDuration, humanReadableDurationDays } from "./dates" +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + daysBetween, + formatDate, + getDefaultDeadline, + humanReadableDuration, + humanReadableDurationDays, +} from "./dates"; describe("dates utils", () => { beforeEach(() => { - vi.useRealTimers() - }) + vi.useRealTimers(); + }); it("calculates day differences in UTC calendar days", () => { - const start = new Date("2026-02-18T23:59:59.000Z") - const end = new Date("2026-02-20T00:00:01.000Z") - expect(daysBetween(start, end)).toBe(2) - }) + const start = new Date("2026-02-18T23:59:59.000Z"); + const end = new Date("2026-02-20T00:00:01.000Z"); + expect(daysBetween(start, end)).toBe(2); + }); it("formats date in day-month-year (UTC)", () => { - const date = new Date("2026-02-20T10:11:12.000Z") - expect(formatDate("en-US", date)).toBe("20-Feb-26") - }) + const date = new Date("2026-02-20T10:11:12.000Z"); + expect(formatDate("en-US", date)).toBe("20-Feb-26"); + }); it("returns relative day label", () => { - const from = new Date("2026-02-20T00:00:00.000Z") - const until = new Date("2026-02-18T00:00:00.000Z") - expect(humanReadableDurationDays("en", from, until)).toBe("in 2 days") - }) + const from = new Date("2026-02-20T00:00:00.000Z"); + const until = new Date("2026-02-18T00:00:00.000Z"); + expect(humanReadableDurationDays("en", from, until)).toBe("in 2 days"); + }); it("returns relative hours label for hour-scale differences", () => { - const from = new Date("2026-02-20T12:00:00.000Z") - const until = new Date("2026-02-20T09:00:00.000Z") - expect(humanReadableDuration("en", from, until)).toBe("in 3 hours") - }) + const from = new Date("2026-02-20T12:00:00.000Z"); + const until = new Date("2026-02-20T09:00:00.000Z"); + expect(humanReadableDuration("en", from, until)).toBe("in 3 hours"); + }); it("uses maturity -2 days when maturity is in the future", () => { - vi.useFakeTimers() - vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")) + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")); - const deadline = getDefaultDeadline("2026-02-25") - expect(deadline.toISOString()).toBe("2026-02-23T23:59:59.999Z") - }) + const deadline = getDefaultDeadline("2026-02-25"); + expect(deadline.toISOString()).toBe("2026-02-23T23:59:59.999Z"); + }); it("uses now +2 days when maturity is in the past", () => { - vi.useFakeTimers() - vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")) + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")); - const deadline = getDefaultDeadline("2026-02-10") - expect(deadline.toISOString()).toBe("2026-02-20T23:59:59.999Z") - }) + const deadline = getDefaultDeadline("2026-02-10"); + expect(deadline.toISOString()).toBe("2026-02-20T23:59:59.999Z"); + }); it("uses now +2 days when maturity is missing", () => { - vi.useFakeTimers() - vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")) + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-18T12:00:00.000Z")); - const deadline = getDefaultDeadline() - expect(deadline.toISOString()).toBe("2026-02-20T23:59:59.999Z") - }) -}) + const deadline = getDefaultDeadline(); + expect(deadline.toISOString()).toBe("2026-02-20T23:59:59.999Z"); + }); +}); diff --git a/src/utils/dates.ts b/src/utils/dates.ts index cad526f..1f8310d 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,55 +1,94 @@ -import { differenceInCalendarYears, differenceInMinutes, subDays, addDays } from "date-fns" -import { differenceInCalendarDays, differenceInCalendarMonths, differenceInHours, differenceInSeconds } from "date-fns" +import { + differenceInCalendarYears, + differenceInMinutes, + subDays, + addDays, +} from "date-fns"; +import { + differenceInCalendarDays, + differenceInCalendarMonths, + differenceInHours, + differenceInSeconds, +} from "date-fns"; -const UTC_TIME_ZONE = "UTC" -const MS_PER_DAY = 24 * 60 * 60 * 1000 +const UTC_TIME_ZONE = "UTC"; +const MS_PER_DAY = 24 * 60 * 60 * 1000; export const daysBetween = (startDate: Date, endDate: Date): number => { - const startUtc = Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()) - const endUtc = Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()) - return Math.floor((endUtc - startUtc) / MS_PER_DAY) -} + const startUtc = Date.UTC( + startDate.getUTCFullYear(), + startDate.getUTCMonth(), + startDate.getUTCDate(), + ); + const endUtc = Date.UTC( + endDate.getUTCFullYear(), + endDate.getUTCMonth(), + endDate.getUTCDate(), + ); + return Math.floor((endUtc - startUtc) / MS_PER_DAY); +}; -export function humanReadableDuration(locale: string, from: Date, until = new Date(Date.now())) { - const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }) +export function humanReadableDuration( + locale: string, + from: Date, + until = new Date(Date.now()), +) { + const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { + numeric: "auto", + }); - const diffYears = differenceInCalendarYears(from, until) + const diffYears = differenceInCalendarYears(from, until); if (Math.abs(diffYears) >= 1) { - return relativeTimeFormatter.format(diffYears, "years") + return relativeTimeFormatter.format(diffYears, "years"); } - const diffMonths = differenceInCalendarMonths(from, until) + const diffMonths = differenceInCalendarMonths(from, until); if (Math.abs(diffMonths) >= 1) { - return relativeTimeFormatter.format(diffMonths, "months") + return relativeTimeFormatter.format(diffMonths, "months"); } - const diffDays = differenceInCalendarDays(from, until) + const diffDays = differenceInCalendarDays(from, until); if (Math.abs(diffDays) >= 1) { - return relativeTimeFormatter.format(diffDays, "days") + return relativeTimeFormatter.format(diffDays, "days"); } - const diffHours = differenceInHours(from, until) + const diffHours = differenceInHours(from, until); if (Math.abs(diffHours) > 1) { - return relativeTimeFormatter.format(diffHours, "hours") + return relativeTimeFormatter.format(diffHours, "hours"); } - const diffMinutes = differenceInMinutes(from, until) + const diffMinutes = differenceInMinutes(from, until); if (Math.abs(diffMinutes) > 1) { - return relativeTimeFormatter.format(diffMinutes, "minutes") + return relativeTimeFormatter.format(diffMinutes, "minutes"); } - const diffSeconds = differenceInSeconds(from, until) - return relativeTimeFormatter.format(diffSeconds, "seconds") + const diffSeconds = differenceInSeconds(from, until); + return relativeTimeFormatter.format(diffSeconds, "seconds"); } -export function humanReadableDurationDays(locale: string, from: Date, until = new Date(Date.now())) { - const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }) +export function humanReadableDurationDays( + locale: string, + from: Date, + until = new Date(Date.now()), +) { + const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { + numeric: "auto", + }); - const diffDays = differenceInCalendarDays(from, until) - return relativeTimeFormatter.format(diffDays, "day") + const diffDays = differenceInCalendarDays(from, until); + return relativeTimeFormatter.format(diffDays, "day"); } export const formatDate = (locale: string, date: Date): string => { - const year = new Intl.DateTimeFormat(locale, { year: "2-digit", timeZone: UTC_TIME_ZONE }).format(date) - const month = new Intl.DateTimeFormat(locale, { month: "short", timeZone: UTC_TIME_ZONE }).format(date) - const day = new Intl.DateTimeFormat(locale, { day: "2-digit", timeZone: UTC_TIME_ZONE }).format(date) - return `${day}-${month}-${year}` -} + const year = new Intl.DateTimeFormat(locale, { + year: "2-digit", + timeZone: UTC_TIME_ZONE, + }).format(date); + const month = new Intl.DateTimeFormat(locale, { + month: "short", + timeZone: UTC_TIME_ZONE, + }).format(date); + const day = new Intl.DateTimeFormat(locale, { + day: "2-digit", + timeZone: UTC_TIME_ZONE, + }).format(date); + return `${day}-${month}-${year}`; +}; export const formatDateLong = (date: Date, locale: string): string => { return new Intl.DateTimeFormat(locale, { @@ -57,8 +96,8 @@ export const formatDateLong = (date: Date, locale: string): string => { month: "long", day: "numeric", timeZone: UTC_TIME_ZONE, - }).format(date) -} + }).format(date); +}; export const formatDateShort = (date: Date, locale: string): string => { return new Intl.DateTimeFormat(locale, { @@ -66,24 +105,30 @@ export const formatDateShort = (date: Date, locale: string): string => { month: "short", day: "numeric", timeZone: UTC_TIME_ZONE, - }).format(date) -} + }).format(date); +}; export const formatMonthLong = (date: Date, locale: string): string => { - return new Intl.DateTimeFormat(locale, { month: "long", timeZone: UTC_TIME_ZONE }).format(date) -} + return new Intl.DateTimeFormat(locale, { + month: "long", + timeZone: UTC_TIME_ZONE, + }).format(date); +}; export const formatMonthYear = (date: Date, locale: string): string => { return new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", timeZone: UTC_TIME_ZONE, - }).format(date) -} + }).format(date); +}; export const formatYearNumeric = (date: Date, locale: string): string => { - return new Intl.DateTimeFormat(locale, { year: "numeric", timeZone: UTC_TIME_ZONE }).format(date) -} + return new Intl.DateTimeFormat(locale, { + year: "numeric", + timeZone: UTC_TIME_ZONE, + }).format(date); +}; /** * Calculate a smart default deadline based on maturity date. @@ -92,20 +137,20 @@ export const formatYearNumeric = (date: Date, locale: string): string => { * or if maturity is in the past or no maturity date provided, returns end of day UTC for today + 2 days. */ export const getDefaultDeadline = (maturityDate?: string | null): Date => { - let deadline: Date - const now = new Date() + let deadline: Date; + const now = new Date(); if (maturityDate) { - const maturity = new Date(maturityDate) + const maturity = new Date(maturityDate); if (maturity > now) { - deadline = subDays(maturity, 2) + deadline = subDays(maturity, 2); } else { - deadline = addDays(now, 2) + deadline = addDays(now, 2); } } else { - deadline = addDays(now, 2) + deadline = addDays(now, 2); } - deadline.setUTCHours(23, 59, 59, 999) - return deadline -} + deadline.setUTCHours(23, 59, 59, 999); + return deadline; +}; diff --git a/src/utils/dev.ts b/src/utils/dev.ts index 706f779..699acb4 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -1,5 +1,5 @@ export function getDeterministicColor(seed?: string): string { - if (!seed) return "#64748b" // Default gray + if (!seed) return "#64748b"; // Default gray const colorPalette = [ "#ef4444", @@ -19,13 +19,13 @@ export function getDeterministicColor(seed?: string): string { "#d946ef", "#ec4899", "#f43f5e", - ] + ]; - let hash = 0 + let hash = 0; for (let i = 0; i < seed.length; i++) { - hash = ((hash << 5) - hash + seed.charCodeAt(i)) & 0xffffffff + hash = ((hash << 5) - hash + seed.charCodeAt(i)) & 0xffffffff; } - const colorIndex = Math.abs(hash) % colorPalette.length - return colorPalette[colorIndex] + const colorIndex = Math.abs(hash) % colorPalette.length; + return colorPalette[colorIndex]; } diff --git a/src/utils/discount-util.test.ts b/src/utils/discount-util.test.ts index 31556d1..a5851ac 100644 --- a/src/utils/discount-util.test.ts +++ b/src/utils/discount-util.test.ts @@ -1,90 +1,128 @@ -import { describe, it, expect } from "vitest" -import Big from "big.js" -import { daysBetween } from "@/utils/dates" -import { Act360 } from "./discount-util" +import { describe, it, expect } from "vitest"; +import Big from "big.js"; +import { daysBetween } from "@/utils/dates"; +import { Act360 } from "./discount-util"; describe("discount-util", () => { describe("Act360", () => { describe("netToGross", () => { it("should calculate gross amount correctly (0)", () => { - const startDate = new Date(2024, 11, 6) - const endDate = new Date(2025, 2, 31) - const netAmount = new Big("10.12") - const discountRate = new Big("0.045") + const startDate = new Date(2024, 11, 6); + const endDate = new Date(2025, 2, 31); + const netAmount = new Big("10.12"); + const discountRate = new Big("0.045"); - const days = daysBetween(startDate, endDate) - const grossAmount = Act360.netToGross(netAmount, discountRate, days) - expect(grossAmount).toStrictEqual(new Big("10.26759670259987317692")) - expect(grossAmount!.toNumber()).toBe(10.267596702599873) - }) + const days = daysBetween(startDate, endDate); + const grossAmount = Act360.netToGross(netAmount, discountRate, days); + expect(grossAmount).toStrictEqual(new Big("10.26759670259987317692")); + expect(grossAmount!.toNumber()).toBe(10.267596702599873); + }); it("should calculate gross amount correctly (1)", () => { - expect(Act360.netToGross(new Big("1"), new Big("1"), -1)).toStrictEqual(new Big("0.99722991689750692521")) - expect(Act360.netToGross(new Big("1"), new Big("1"), 0)).toStrictEqual(new Big("1")) - expect(Act360.netToGross(new Big("1"), new Big("1"), 1)).toStrictEqual(new Big("1.00278551532033426184")) - expect(Act360.netToGross(new Big("1"), new Big("1"), 355)).toStrictEqual(new Big("71.99999999999999999424")) - expect(Act360.netToGross(new Big("1"), new Big("1"), 360)).toStrictEqual(undefined) - expect(Act360.netToGross(new Big("1"), new Big("1"), 365)).toStrictEqual(new Big("-71.99999999999999999424")) - }) + expect(Act360.netToGross(new Big("1"), new Big("1"), -1)).toStrictEqual( + new Big("0.99722991689750692521"), + ); + expect(Act360.netToGross(new Big("1"), new Big("1"), 0)).toStrictEqual( + new Big("1"), + ); + expect(Act360.netToGross(new Big("1"), new Big("1"), 1)).toStrictEqual( + new Big("1.00278551532033426184"), + ); + expect( + Act360.netToGross(new Big("1"), new Big("1"), 355), + ).toStrictEqual(new Big("71.99999999999999999424")); + expect( + Act360.netToGross(new Big("1"), new Big("1"), 360), + ).toStrictEqual(undefined); + expect( + Act360.netToGross(new Big("1"), new Big("1"), 365), + ).toStrictEqual(new Big("-71.99999999999999999424")); + }); it("should calculate gross amount correctly (2)", () => { - expect(Act360.netToGross(new Big(1), new Big("0.9863"), 365)).toStrictEqual(new Big("719999.999999999424")) - expect(Act360.netToGross(new Big(1), new Big("0.9864"), 365)).toStrictEqual(new Big("-10000")) - expect(Act360.netToGross(new Big(1), new Big("0.9865"), 365)).toStrictEqual( - new Big("-4965.51724137931031743163"), - ) - }) + expect( + Act360.netToGross(new Big(1), new Big("0.9863"), 365), + ).toStrictEqual(new Big("719999.999999999424")); + expect( + Act360.netToGross(new Big(1), new Big("0.9864"), 365), + ).toStrictEqual(new Big("-10000")); + expect( + Act360.netToGross(new Big(1), new Big("0.9865"), 365), + ).toStrictEqual(new Big("-4965.51724137931031743163")); + }); it("should calculate gross amount correctly (step-by-step)", () => { - const startDate = new Date(2024, 11, 6) - const endDate = new Date(2025, 2, 31) - const netAmount = new Big("10.12") - const discountRate = new Big("0.045") + const startDate = new Date(2024, 11, 6); + const endDate = new Date(2025, 2, 31); + const netAmount = new Big("10.12"); + const discountRate = new Big("0.045"); - const days = daysBetween(startDate, endDate) - expect(days, "sanity check").toBe(115) + const days = daysBetween(startDate, endDate); + expect(days, "sanity check").toBe(115); - const discountDays = discountRate.times(days).div(360) - expect(discountDays.toNumber(), "sanity check").toBe(0.014375) + const discountDays = discountRate.times(days).div(360); + expect(discountDays.toNumber(), "sanity check").toBe(0.014375); - const factor = new Big(1).minus(discountDays) + const factor = new Big(1).minus(discountDays); - const grossAmount = netAmount.div(factor) - expect(grossAmount).toStrictEqual(new Big("10.26759670259987317692")) - expect(grossAmount.toNumber()).toBe(10.267596702599873) + const grossAmount = netAmount.div(factor); + expect(grossAmount).toStrictEqual(new Big("10.26759670259987317692")); + expect(grossAmount.toNumber()).toBe(10.267596702599873); - const calcGrossAmount = Act360.netToGross(netAmount, discountRate, days) - expect(calcGrossAmount).toStrictEqual(grossAmount) - }) - }) + const calcGrossAmount = Act360.netToGross( + netAmount, + discountRate, + days, + ); + expect(calcGrossAmount).toStrictEqual(grossAmount); + }); + }); describe("grossToNet", () => { it("should calculate net amount correctly (0)", () => { - const startDate = new Date(2024, 11, 6) - const endDate = new Date(2025, 2, 31) - const grossAmount = new Big("10.12") - const discountRate = new Big("0.045") + const startDate = new Date(2024, 11, 6); + const endDate = new Date(2025, 2, 31); + const grossAmount = new Big("10.12"); + const discountRate = new Big("0.045"); - const days = daysBetween(startDate, endDate) - const netAmount = Act360.grossToNet(grossAmount, discountRate, days) - expect(netAmount).toStrictEqual(new Big("9.974525")) - expect(netAmount.toNumber()).toBe(9.974525) - }) + const days = daysBetween(startDate, endDate); + const netAmount = Act360.grossToNet(grossAmount, discountRate, days); + expect(netAmount).toStrictEqual(new Big("9.974525")); + expect(netAmount.toNumber()).toBe(9.974525); + }); it("should calculate net amount correctly (1)", () => { - expect(Act360.grossToNet(new Big("1"), new Big("1"), -1)).toStrictEqual(new Big("1.00277777777777777778")) - expect(Act360.grossToNet(new Big("1"), new Big("1"), 0)).toStrictEqual(new Big("1")) - expect(Act360.grossToNet(new Big("1"), new Big("1"), 1)).toStrictEqual(new Big("0.99722222222222222222")) - expect(Act360.grossToNet(new Big("1"), new Big("1"), 355)).toStrictEqual(new Big("0.01388888888888888889")) - expect(Act360.grossToNet(new Big("1"), new Big("1"), 360)).toStrictEqual(new Big("0")) - expect(Act360.grossToNet(new Big("1"), new Big("1"), 365)).toStrictEqual(new Big("-0.01388888888888888889")) - }) + expect(Act360.grossToNet(new Big("1"), new Big("1"), -1)).toStrictEqual( + new Big("1.00277777777777777778"), + ); + expect(Act360.grossToNet(new Big("1"), new Big("1"), 0)).toStrictEqual( + new Big("1"), + ); + expect(Act360.grossToNet(new Big("1"), new Big("1"), 1)).toStrictEqual( + new Big("0.99722222222222222222"), + ); + expect( + Act360.grossToNet(new Big("1"), new Big("1"), 355), + ).toStrictEqual(new Big("0.01388888888888888889")); + expect( + Act360.grossToNet(new Big("1"), new Big("1"), 360), + ).toStrictEqual(new Big("0")); + expect( + Act360.grossToNet(new Big("1"), new Big("1"), 365), + ).toStrictEqual(new Big("-0.01388888888888888889")); + }); it("should calculate net amount correctly (2)", () => { - expect(Act360.grossToNet(new Big(1), new Big("0.9863"), 365)).toStrictEqual(new Big("0.00000138888888888889")) - expect(Act360.grossToNet(new Big(1), new Big("0.9864"), 365)).toStrictEqual(new Big("-0.0001")) - expect(Act360.grossToNet(new Big(1), new Big("0.9865"), 365)).toStrictEqual(new Big("-0.00020138888888888889")) - }) - }) - }) -}) + expect( + Act360.grossToNet(new Big(1), new Big("0.9863"), 365), + ).toStrictEqual(new Big("0.00000138888888888889")); + expect( + Act360.grossToNet(new Big(1), new Big("0.9864"), 365), + ).toStrictEqual(new Big("-0.0001")); + expect( + Act360.grossToNet(new Big(1), new Big("0.9865"), 365), + ).toStrictEqual(new Big("-0.00020138888888888889")); + }); + }); + }); +}); diff --git a/src/utils/discount-util.ts b/src/utils/discount-util.ts index 9150d04..e353e5b 100644 --- a/src/utils/discount-util.ts +++ b/src/utils/discount-util.ts @@ -1,19 +1,23 @@ -import Big from "big.js" +import Big from "big.js"; -const BIG_1 = new Big("1") -const BIG_360 = new Big("360") +const BIG_1 = new Big("1"); +const BIG_360 = new Big("360"); const factor = (discountRate: Big, days: number) => { - const discountDays = discountRate.times(days).div(BIG_360) - return BIG_1.minus(discountDays) -} + const discountDays = discountRate.times(days).div(BIG_360); + return BIG_1.minus(discountDays); +}; export const Act360 = { - netToGross: (netAmount: Big, discountRate: Big, days: number): Big | undefined => { - const divisor = factor(discountRate, days) - return divisor.toNumber() !== 0 ? netAmount.div(divisor) : undefined + netToGross: ( + netAmount: Big, + discountRate: Big, + days: number, + ): Big | undefined => { + const divisor = factor(discountRate, days); + return divisor.toNumber() !== 0 ? netAmount.div(divisor) : undefined; }, grossToNet: (grossAmount: Big, discountRate: Big, days: number): Big => { - return grossAmount.times(factor(discountRate, days)) + return grossAmount.times(factor(discountRate, days)); }, -} +}; diff --git a/src/utils/keyset.test.ts b/src/utils/keyset.test.ts index 4253cf4..0c8b189 100644 --- a/src/utils/keyset.test.ts +++ b/src/utils/keyset.test.ts @@ -1,36 +1,40 @@ -import { describe, expect, it, vi } from "vitest" -import { serializeKeysetId } from "./keyset" +import { describe, expect, it, vi } from "vitest"; +import { serializeKeysetId } from "./keyset"; describe("serializeKeysetId", () => { it("returns input as-is when id is already a string", () => { - expect(serializeKeysetId("abc123")).toBe("abc123") - }) + expect(serializeKeysetId("abc123")).toBe("abc123"); + }); it("serializes Version00 with V1 bytes", () => { - const id = { version: "Version00", id: { V1: [0, 1, 255] } } - expect(serializeKeysetId(id as never)).toBe("000001ff") - }) + const id = { version: "Version00", id: { V1: [0, 1, 255] } }; + expect(serializeKeysetId(id as never)).toBe("000001ff"); + }); it("serializes Version01 with V2 bytes", () => { - const id = { version: "Version01", id: { V2: [10, 11, 12] } } - expect(serializeKeysetId(id as never)).toBe("010a0b0c") - }) + const id = { version: "Version01", id: { V2: [10, 11, 12] } }; + expect(serializeKeysetId(id as never)).toBe("010a0b0c"); + }); it("returns empty string for malformed id payload", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) - const id = { version: "Version00" } + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + const id = { version: "Version00" }; - expect(serializeKeysetId(id as never)).toBe("") - expect(errorSpy).toHaveBeenCalled() - errorSpy.mockRestore() - }) + expect(serializeKeysetId(id as never)).toBe(""); + expect(errorSpy).toHaveBeenCalled(); + errorSpy.mockRestore(); + }); it("returns empty string when id bytes shape is invalid", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) - const id = { version: "Version00", id: { invalid: [1, 2, 3] } } + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + const id = { version: "Version00", id: { invalid: [1, 2, 3] } }; - expect(serializeKeysetId(id as never)).toBe("") - expect(errorSpy).toHaveBeenCalled() - errorSpy.mockRestore() - }) -}) + expect(serializeKeysetId(id as never)).toBe(""); + expect(errorSpy).toHaveBeenCalled(); + errorSpy.mockRestore(); + }); +}); diff --git a/src/utils/keyset.ts b/src/utils/keyset.ts index 28f9a65..8ce3dbf 100644 --- a/src/utils/keyset.ts +++ b/src/utils/keyset.ts @@ -1,4 +1,4 @@ -import { Id } from "@/generated/client/types.gen" +import { Id } from "@/generated/client/types.gen"; /** * Serializes an Id object to a string format suitable for URLs. @@ -8,31 +8,31 @@ import { Id } from "@/generated/client/types.gen" export function serializeKeysetId(id: Id | string): string { // If it's already a string, return it directly if (typeof id === "string") { - return id + return id; } // Handle the case where the id might be malformed if (!id.id) { - console.error("Invalid Id object:", id) - return "" + console.error("Invalid Id object:", id); + return ""; } - let bytes: number[] + let bytes: number[]; if ("V1" in id.id) { - bytes = id.id.V1 + bytes = id.id.V1; } else if ("V2" in id.id) { - bytes = id.id.V2 + bytes = id.id.V2; } else { - console.error("Invalid IdBytes structure:", id.id) - return "" + console.error("Invalid IdBytes structure:", id.id); + return ""; } // Convert bytes to hex string - const hexString = bytes.map((b) => b.toString(16).padStart(2, "0")).join("") + const hexString = bytes.map((b) => b.toString(16).padStart(2, "0")).join(""); // Prepend version info (00 for Version00, 01 for Version01) - const versionPrefix = id.version === "Version00" ? "00" : "01" + const versionPrefix = id.version === "Version00" ? "00" : "01"; - return `${versionPrefix}${hexString}` + return `${versionPrefix}${hexString}`; } diff --git a/src/utils/local-storage.ts b/src/utils/local-storage.ts index df628c1..06953a0 100644 --- a/src/utils/local-storage.ts +++ b/src/utils/local-storage.ts @@ -1,24 +1,24 @@ export function setItem(key: string, value: unknown) { try { - window.localStorage.setItem(key, JSON.stringify(value)) + window.localStorage.setItem(key, JSON.stringify(value)); } catch (err) { - console.error(err) + console.error(err); } } export function getItem(key: string): T | undefined { try { - const data = window.localStorage.getItem(key) - return data ? (JSON.parse(data) as T) : undefined + const data = window.localStorage.getItem(key); + return data ? (JSON.parse(data) as T) : undefined; } catch (err) { - console.error(err) + console.error(err); } } export function removeItem(key: string) { try { - window.localStorage.removeItem(key) + window.localStorage.removeItem(key); } catch (err) { - console.error(err) + console.error(err); } } diff --git a/src/utils/numbers.test.ts b/src/utils/numbers.test.ts index 1d03258..c723500 100644 --- a/src/utils/numbers.test.ts +++ b/src/utils/numbers.test.ts @@ -1,34 +1,34 @@ -import { describe, it, expect } from "vitest" -import { parseFloatSafe, parseIntSafe } from "./numbers" +import { describe, it, expect } from "vitest"; +import { parseFloatSafe, parseIntSafe } from "./numbers"; describe("util", () => { describe("parseFloatSafe", () => { it("should safely parse floats", () => { - expect(parseFloatSafe("")).toBe(undefined) - expect(parseFloatSafe("NaN")).toBe(undefined) - expect(parseFloatSafe("Infinity")).toBe(undefined) - expect(parseFloatSafe(String(1 / 0))).toBe(undefined) - expect(parseFloatSafe("foobar")).toBe(undefined) - expect(parseFloatSafe("0")).toBe(0) - expect(parseFloatSafe("1")).toBe(1) - expect(parseFloatSafe("-1")).toBe(-1) - expect(parseFloatSafe("1.23456789")).toBe(1.23456789) - expect(parseFloatSafe("-1.23456789")).toBe(-1.23456789) - }) - }) + expect(parseFloatSafe("")).toBe(undefined); + expect(parseFloatSafe("NaN")).toBe(undefined); + expect(parseFloatSafe("Infinity")).toBe(undefined); + expect(parseFloatSafe(String(1 / 0))).toBe(undefined); + expect(parseFloatSafe("foobar")).toBe(undefined); + expect(parseFloatSafe("0")).toBe(0); + expect(parseFloatSafe("1")).toBe(1); + expect(parseFloatSafe("-1")).toBe(-1); + expect(parseFloatSafe("1.23456789")).toBe(1.23456789); + expect(parseFloatSafe("-1.23456789")).toBe(-1.23456789); + }); + }); describe("parseIntSafe", () => { it("should safely parse ints", () => { - expect(parseIntSafe("")).toBe(undefined) - expect(parseIntSafe("NaN")).toBe(undefined) - expect(parseIntSafe("Infinity")).toBe(undefined) - expect(parseIntSafe(String(1 / 0))).toBe(undefined) - expect(parseIntSafe("foobar")).toBe(undefined) - expect(parseIntSafe("0")).toBe(0) - expect(parseIntSafe("1")).toBe(1) - expect(parseIntSafe("-1")).toBe(-1) - expect(parseIntSafe("1.23456789")).toBe(1) - expect(parseIntSafe("-1.23456789")).toBe(-1) - }) - }) -}) + expect(parseIntSafe("")).toBe(undefined); + expect(parseIntSafe("NaN")).toBe(undefined); + expect(parseIntSafe("Infinity")).toBe(undefined); + expect(parseIntSafe(String(1 / 0))).toBe(undefined); + expect(parseIntSafe("foobar")).toBe(undefined); + expect(parseIntSafe("0")).toBe(0); + expect(parseIntSafe("1")).toBe(1); + expect(parseIntSafe("-1")).toBe(-1); + expect(parseIntSafe("1.23456789")).toBe(1); + expect(parseIntSafe("-1.23456789")).toBe(-1); + }); + }); +}); diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index ed1ae06..8072448 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -1,11 +1,11 @@ export const parseFloatSafe = (str: string | undefined) => { - if (str === undefined) return undefined - const parsed = parseFloat(str) - return isNaN(parsed) || !isFinite(parsed) ? undefined : parsed -} + if (str === undefined) return undefined; + const parsed = parseFloat(str); + return isNaN(parsed) || !isFinite(parsed) ? undefined : parsed; +}; export const parseIntSafe = (str: string | undefined) => { - if (str === undefined) return undefined - const parsed = parseInt(str, 10) - return isNaN(parsed) || !isFinite(parsed) ? undefined : parsed -} + if (str === undefined) return undefined; + const parsed = parseInt(str, 10); + return isNaN(parsed) || !isFinite(parsed) ? undefined : parsed; +}; diff --git a/src/utils/qrCodeUtils.ts b/src/utils/qrCodeUtils.ts index f168c6c..976453b 100644 --- a/src/utils/qrCodeUtils.ts +++ b/src/utils/qrCodeUtils.ts @@ -1,19 +1,19 @@ -export const QR_CODE_MAX_LENGTH = 2956 +export const QR_CODE_MAX_LENGTH = 2956; export function canGenerateQRCode(text: string): boolean { - return text.length <= QR_CODE_MAX_LENGTH + return text.length <= QR_CODE_MAX_LENGTH; } export async function canGenerateQRCodeAsync(text: string): Promise { if (text.length > QR_CODE_MAX_LENGTH) { - return false + return false; } try { - const { toString } = await import("qrcode") - await toString(text, { errorCorrectionLevel: "M" }) - return true + const { toString } = await import("qrcode"); + await toString(text, { errorCorrectionLevel: "M" }); + return true; } catch { - return false + return false; } } diff --git a/src/utils/quote-status.test.ts b/src/utils/quote-status.test.ts index 0cb21c5..3e91959 100644 --- a/src/utils/quote-status.test.ts +++ b/src/utils/quote-status.test.ts @@ -1,25 +1,25 @@ -import { describe, expect, it } from "vitest" -import { getQuoteStatusVariant } from "./quote-status" +import { describe, expect, it } from "vitest"; +import { getQuoteStatusVariant } from "./quote-status"; describe("getQuoteStatusVariant", () => { it("maps offered and pending statuses to default", () => { - expect(getQuoteStatusVariant("Offered")).toBe("default") - expect(getQuoteStatusVariant("OfferExpired")).toBe("default") - expect(getQuoteStatusVariant("Pending")).toBe("default") - }) + expect(getQuoteStatusVariant("Offered")).toBe("default"); + expect(getQuoteStatusVariant("OfferExpired")).toBe("default"); + expect(getQuoteStatusVariant("Pending")).toBe("default"); + }); it("maps accepted and minting statuses to success", () => { - expect(getQuoteStatusVariant("Accepted")).toBe("success") - expect(getQuoteStatusVariant("Minting")).toBe("success") - }) + expect(getQuoteStatusVariant("Accepted")).toBe("success"); + expect(getQuoteStatusVariant("Minting")).toBe("success"); + }); it("maps denied-like statuses to destructive", () => { - expect(getQuoteStatusVariant("Denied")).toBe("destructive") - expect(getQuoteStatusVariant("Canceled")).toBe("destructive") - expect(getQuoteStatusVariant("Rejected")).toBe("destructive") - }) + expect(getQuoteStatusVariant("Denied")).toBe("destructive"); + expect(getQuoteStatusVariant("Canceled")).toBe("destructive"); + expect(getQuoteStatusVariant("Rejected")).toBe("destructive"); + }); it("falls back to outline for unknown values", () => { - expect(getQuoteStatusVariant("UnknownStatus")).toBe("outline") - }) -}) + expect(getQuoteStatusVariant("UnknownStatus")).toBe("outline"); + }); +}); diff --git a/src/utils/quote-status.ts b/src/utils/quote-status.ts index 750d0e6..133b90d 100644 --- a/src/utils/quote-status.ts +++ b/src/utils/quote-status.ts @@ -1,21 +1,26 @@ -export type QuoteStatusVariant = "default" | "secondary" | "destructive" | "success" | "outline" +export type QuoteStatusVariant = + | "default" + | "secondary" + | "destructive" + | "success" + | "outline"; export const getQuoteStatusVariant = (status: string): QuoteStatusVariant => { switch (status) { case "Offered": case "OfferExpired": - return "default" + return "default"; case "Pending": - return "default" + return "default"; case "Accepted": case "Minting": case "MintingEnabled": - return "success" + return "success"; case "Denied": case "Canceled": case "Rejected": - return "destructive" + return "destructive"; default: - return "outline" + return "outline"; } -} +}; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 3935b0a..05f3586 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,15 +1,17 @@ export const truncateString = (str: string, maxLength: number): string => str.length <= maxLength ? str - : str.slice(0, Math.floor((maxLength - 3) / 2)) + "…" + str.slice(-Math.floor((maxLength - 3) / 2)) + : str.slice(0, Math.floor((maxLength - 3) / 2)) + + "…" + + str.slice(-Math.floor((maxLength - 3) / 2)); export const formatNumber = (locale: string, value: number): string => { - return new Intl.NumberFormat(locale).format(value) -} + return new Intl.NumberFormat(locale).format(value); +}; export const getInitials = (name?: string): string => { if (!name) { - return "?" + return "?"; } return name @@ -18,25 +20,25 @@ export const getInitials = (name?: string): string => { .map((n) => n[0]) .join("") .toUpperCase() - .slice(0, 2) -} + .slice(0, 2); +}; export const getDeterministicColor = (seed?: string): string => { if (!seed) { - return "#999999" + return "#999999"; } - let hash = 0 + let hash = 0; for (let i = 0; i < seed.length; i++) { - hash = seed.charCodeAt(i) + ((hash << 5) - hash) + hash = seed.charCodeAt(i) + ((hash << 5) - hash); } - const hue = Math.abs(hash % 360) - const saturation = 65 + (Math.abs(hash) % 20) - const lightness = 45 + (Math.abs(hash >> 8) % 15) + const hue = Math.abs(hash % 360); + const saturation = 65 + (Math.abs(hash) % 20); + const lightness = 45 + (Math.abs(hash >> 8) % 15); - return `hsl(${hue}, ${saturation}%, ${lightness}%)` -} + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; +}; /** * Formats a status label for display to users. @@ -44,10 +46,10 @@ export const getDeterministicColor = (seed?: string): string => { */ export const formatStatusLabel = (status: string): string => { if (status === "OfferExpired") { - return "Offer expired" + return "Offer expired"; } if (status === "MintingEnabled") { - return "Minting enabled" + return "Minting enabled"; } - return status -} + return status; +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index dbaaf9a..9fb89c8 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -28,9 +28,5 @@ "@/*": ["./src/*"] } }, - "include": [ - "src", - "vitest-setup.ts", - "openapi-ts.config.ts" - ] + "include": ["src", "vitest-setup.ts", "openapi-ts.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index e0a7e51..54cd21b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,11 @@ -import path from "path" -import { defineConfig as defineViteConfig, mergeConfig } from "vite" -import { defineConfig as defineVitestConfig, configDefaults } from "vitest/config" -import react from "@vitejs/plugin-react" -import tailwindcss from "@tailwindcss/vite" +import path from "path"; +import { defineConfig as defineViteConfig, mergeConfig } from "vite"; +import { + defineConfig as defineVitestConfig, + configDefaults, +} from "vitest/config"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; // https://vite.dev/config/ const viteConfig = defineViteConfig({ @@ -12,13 +15,13 @@ const viteConfig = defineViteConfig({ "@": path.resolve(__dirname, "./src"), }, }, -}) +}); const vitestConfig = defineVitestConfig({ test: { environment: "jsdom", include: ["**/*.test.{ts,tsx}"], - setupFiles: ['./vitest-setup.ts'], + setupFiles: ["./vitest-setup.ts"], coverage: { provider: "v8", reporter: ["text", "json", "lcov"], @@ -26,6 +29,6 @@ const vitestConfig = defineVitestConfig({ }, exclude: [...configDefaults.exclude], }, -}) +}); -export default mergeConfig(viteConfig, vitestConfig) +export default mergeConfig(viteConfig, vitestConfig); diff --git a/vitest-setup.ts b/vitest-setup.ts index d178cb9..32d5a5e 100644 --- a/vitest-setup.ts +++ b/vitest-setup.ts @@ -1,3 +1,4 @@ -import '@testing-library/jest-dom/vitest' - -;(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true +import "@testing-library/jest-dom/vitest"; +( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true;