diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index b90905fa..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: ['plugin:astro/recommended'], - plugins: ['unicorn'], - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, - sourceType: 'module', - ecmaVersion: 'latest' - }, - overrides: [ - { - files: ['*.astro'], - parser: 'astro-eslint-parser', - parserOptions: { - parser: '@typescript-eslint/parser', - extraFileExtensions: ['.astro'] - }, - rules: { - // Add any specific rules for .astro files here - } - } - ], - rules: { - 'unicorn/filename-case': [ - 'error', - { - case: 'kebabCase', - }, - ], - } -} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 74% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index f20cdec4..69247bb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: CI Check on: pull_request: @@ -7,7 +7,7 @@ on: - master jobs: - test: + ci: runs-on: ubuntu-latest permissions: @@ -21,10 +21,18 @@ jobs: - name: 📦 Setup Node + PNPM + install deps uses: ./.github/actions/setup-node-pnpm-install - - name: "Test" + - name: "Astro Check" + run: pnpm run astro check + + - name: "Run ESLint" + run: pnpm run lint:ci + + - name: "Run Tests" run: pnpm run test:ci - - name: "Report Coverage" + + - name: "Generate Coverage Report" # Set if: always() to also generate the report if tests are failing # Only works if you set `reportOnFailure: true` in your vite config as specified above if: always() uses: davelosert/vitest-coverage-report-action@v2 + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 05112174..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint - -on: - pull_request: - push: - branches: - - master - -jobs: - lint: - runs-on: ubuntu-latest - - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: 📦 Setup Node + PNPM + install deps - uses: ./.github/actions/setup-node-pnpm-install - - - name: Run linting - run: pnpm run lint:ci diff --git a/.vscode/settings.json b/.vscode/settings.json index 5de7bc0d..4b356f5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,50 @@ { + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, + + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + + // Silent the stylistic rules in your IDE, but still auto fix them + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } + ], + + // Enable eslint for all supported languages "eslint.validate": [ "javascript", "javascriptreact", - "astro", "typescript", - "typescriptreact" - ], - "prettier.documentSelectors": ["**/*.astro"], - "[astro]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} \ No newline at end of file + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ] +} diff --git a/RFC-conditional-survey-logic.md b/RFC-conditional-survey-logic.md new file mode 100644 index 00000000..3cf9eeb6 --- /dev/null +++ b/RFC-conditional-survey-logic.md @@ -0,0 +1,340 @@ +# RFC: Conditional Logic for Survey Questions + +## Problem Statement + +Our survey shows all questions to all respondents, leading to: + +- Longer survey completion times +- Lower response rates +- Irrelevant questions for many users (e.g., "plans to return to Morocco" shown to everyone, even those not working abroad) + +**Goal**: Add conditional logic to show/hide questions and sections based on previous answers. + +## Proposed YAML Schema + +### Current Format (No Changes for Existing Surveys) + +```yaml +title: Profile +label: profile +position: 1 +questions: + - label: Where are you currently working? + required: true + choices: + - Currently working in Morocco + - Currently working outside Morocco + - Not currently working +``` + +### New Format: Simple Conditions (Recommended) + +Add optional `showIf` field to questions: + +```yaml +questions: + - label: Where are you currently working? + required: true + choices: + - Currently working in Morocco + - Currently working outside Morocco + - Not currently working + - Student + + # Only show if user selected "Currently working outside Morocco" (index 1) + - label: Do you have any plans to come back to Morocco? + required: true + showIf: + question: profile-q-0 # References question by ID + equals: 1 # Choice index + choices: + - Yes, within 12 months + - Yes, within 24 months + - No plans to return +``` + +**Simple Condition Operators:** + +```yaml +# Single choice - exact match +showIf: + question: profile-q-2 + equals: 1 + +# Single choice - not equal +showIf: + question: work-q-0 + notEquals: 3 + +# Single choice - one of multiple values +showIf: + question: work-q-0 + in: [0, 1, 2] # Employed full-time, part-time, or freelancer + +# Multiple choice - user selected specific option +showIf: + question: tech-q-5 + includes: 3 # User checked choice index 3 + +# Check if question was answered (not skipped) +showIf: + question: work-q-2 + answered: true +``` + +### Advanced Format: Complex Conditions (When Needed) + +For complex scenarios requiring AND/OR logic: + +```yaml +questions: + - label: Are you satisfied with your work-life balance? + required: true + conditions: + and: # All must be true + - question: work-q-0 + operator: equals + value: 0 # Full-time employed + - question: profile-q-2 + operator: equals + value: 0 # Working in Morocco + choices: + - Very satisfied + - Satisfied + - Dissatisfied + + - label: What are your freelancing challenges? + required: true + conditions: + or: # At least one must be true + - question: work-q-0 + operator: equals + value: 2 # Freelancer + - question: work-q-0 + operator: equals + value: 1 # Part-time + choices: + - Finding clients + - Pricing services +``` + +**Advanced Operators:** + +```yaml +# All operators from simple format, plus: +operator: notIn +operator: includesAny # For multiple choice: has any of these +operator: includesAll # For multiple choice: has all of these +operator: notIncludes +operator: notAnswered +``` + +### Section-Level Conditions + +Hide entire sections based on profile answers: + +```yaml +title: Work Experience +label: work +position: 3 + +# Only show this section if user is not a student +showIf: + question: profile-q-3 # Occupation + notEquals: 3 # Student choice index + +questions: + - label: What is your job title? + # ... rest of questions +``` + +Or with advanced conditions: + +```yaml +title: Advanced Career Topics +label: career +position: 5 + +conditions: + and: + - question: profile-q-5 # Years of experience + operator: in + value: [3, 4, 5] # 3+ years + - question: work-q-0 + operator: in + value: [0, 1, 2] # Currently employed + +questions: + # ... questions only for experienced employed developers +``` + +## Cross-Section References + +Questions can reference answers from any previous section: + +```yaml +# In work.yml (section 3) +questions: + - label: Would you consider remote work abroad? + showIf: + question: profile-q-2 # From profile.yml (section 1) + equals: 0 # Currently in Morocco + choices: + - Yes + - No +``` + +## Question ID Format + +Question IDs follow the pattern: `{section-label}-q-{index}` + +Examples: + +- `profile-q-0` - First question in profile section +- `profile-q-1` - Second question in profile section +- `work-q-0` - First question in work section + +**Important**: Index is zero-based and represents the question's position in the YAML file. + +## Answer Data Format (No Changes) + +Conditional logic doesn't change how answers are stored: + +```javascript +{ + "profile-q-0": 1, // Single choice: index 1 + "tech-q-2": [0, 2, 4], // Multiple choice: indices array + "work-q-5": null, // Skipped question + "profile-q-8": 5, // "Other" selected + "profile-q-8-others": "Text input" // Other text +} +``` + +Hidden questions are simply not answered (null or not present). + +## Operator Reference Table + +| Operator | Question Type | Description | Example | +| ------------- | --------------- | ----------------------------------- | --------------------- | +| `equals` | Single choice | Answer equals specific value | `equals: 1` | +| `notEquals` | Single choice | Answer not equal to value | `notEquals: 2` | +| `in` | Single choice | Answer is one of these values | `in: [0, 1, 2]` | +| `notIn` | Single choice | Answer is not one of these values | `notIn: [3, 4]` | +| `includes` | Multiple choice | Answer array includes value | `includes: 3` | +| `notIncludes` | Multiple choice | Answer array doesn't include value | `notIncludes: 4` | +| `includesAny` | Multiple choice | Answer has any of these values | `includesAny: [0, 1]` | +| `includesAll` | Multiple choice | Answer has all of these values | `includesAll: [1, 3]` | +| `answered` | Any | Question was answered (not skipped) | `answered: true` | +| `notAnswered` | Any | Question was skipped | `notAnswered: true` | + +## Use Cases + +### Use Case 1: Work Status Follow-ups + +```yaml +- label: Current employment status? + choices: + - Employed full-time + - Employed part-time + - Freelancer + - Student + - Unemployed + +- label: Your job title? + showIf: + question: work-q-0 + in: [0, 1, 2] # Any employed status + choices: + - Frontend Developer + - Backend Developer + +- label: Are you looking for work? + showIf: + question: work-q-0 + in: [3, 4] # Student or unemployed + choices: + - Yes, actively + - No +``` + +### Use Case 2: Location-Based Questions + +```yaml +- label: Where do you work? + choices: + - Morocco + - Outside Morocco + +- label: Which Moroccan city? + showIf: + question: profile-q-5 + equals: 0 + choices: + - Casablanca + - Rabat + +- label: Do you plan to return to Morocco? + showIf: + question: profile-q-5 + equals: 1 + choices: + - Yes + - No +``` + +### Use Case 3: Experience-Based Sections + +```yaml +title: Senior Developer Topics +label: senior +position: 7 + +showIf: + question: profile-q-4 # Years of experience + in: [4, 5] # 5+ years + +questions: + - label: Do you mentor junior developers? + choices: + - Yes, regularly + - Occasionally + - No +``` + +## Backward Compatibility + +- ✅ Existing surveys without conditions work unchanged +- ✅ Questions without `showIf` or `conditions` are always visible +- ✅ Sections without conditions are always visible +- ✅ Answer data format remains the same + +## Implementation Notes + +1. **Hybrid approach**: Support both simple `showIf` and advanced `conditions` formats +2. **Auto-detection**: Parser determines format based on field structure +3. **Validation**: Question IDs in conditions should be validated against existing questions +4. **Order dependency**: Conditions can only reference previous questions (to avoid circular dependencies) +5. **Progressive enhancement**: Start with simple cases, add complexity as needed + +## Questions for Discussion + +1. Should we allow forward references (referencing questions that come later)? +2. Should we add a "preview mode" to test conditions without submitting? +3. Should section-level conditions be required, or is question-level enough initially? +4. Do we need a visual indicator in the survey that questions were skipped due to conditions? + +## Next Steps + +1. Gather feedback on YAML format proposal +2. Decide on operator set (minimal vs comprehensive) +3. Create TypeScript types for conditional logic +4. Implement condition evaluation engine +5. Update survey components to support filtering +6. Write tests and documentation +7. Add sample conditions to one section as a pilot + +--- + +**Status**: RFC / Seeking Feedback +**Created**: 2025-01-23 +**Author**: GeeksBlaBla Team diff --git a/custom-json.d.ts b/custom-json.d.ts index d35fa33e..35d5d421 100644 --- a/custom-json.d.ts +++ b/custom-json.d.ts @@ -3,22 +3,22 @@ * Mainly to prevent code editor from loading typing from json files which is can be very heavy */ -type Question = { +interface Question { label: string; choices: string[]; multiple: boolean | null; required: boolean | null; -}; +} -type QuestionMap = { +interface QuestionMap { [key: string]: Question; -}; +} -type Results = { +interface Results { results: { [key: string]: number | number[] | string | string[] | null | undefined; }[]; -}; +} // 2020 declare module "@/results/2020/data/results.json" { diff --git a/custom-yaml.d.ts b/custom-yaml.d.ts index 1f3a6f81..126a17a8 100644 --- a/custom-yaml.d.ts +++ b/custom-yaml.d.ts @@ -1,23 +1,11 @@ /** - * This file is used to define the types for the results.json and questions.json files - * Mainly to prevent code editor from loading typing from json files which is can be very heavy + * Module declaration for YAML imports + * Types are now imported from @/lib/validators/survey-schema (Zod-inferred) */ -type SurveyQuestion = { - label: string; - choices: string[]; - multiple: boolean | null; - required: boolean | null; -}; - -type SurveyQuestionsYamlFile = { - title: string; - label: string; - position: number; - questions: SurveyQuestion[]; -}; - declare module "@/survey/*.yml" { + import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; + const value: SurveyQuestionsYamlFile; export default value; } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0279a272 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import antfu from "@antfu/eslint-config"; + +export default antfu({ + astro: true, + react: true, + typescript: { + tsconfigPath: "tsconfig.json" + }, + stylistic: { + quotes: "double", + semi: true + } +}, { + rules: { + "unicorn/filename-case": [ + "error", + { + case: "kebabCase" + } + ], + "ts/strict-boolean-expressions": "off", + "style/comma-dangle": ["error", "never"] + } +}); diff --git a/package.json b/package.json index a9be294f..c4922d0b 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "test": "vitest", "test:ui": "vitest --ui", "test:ci": "vitest --coverage.enabled true", - "lint": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\" && eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"", - "lint:ci": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"", + "lint": "eslint --fix \"**/*.{js,jsx,ts,tsx,astro}\"", + "lint:ci": "eslint \"**/*.{js,jsx,ts,tsx,astro}\"", "validate-survey": "tsx ./scripts/validate-survey.ts", "prepare": "husky", "export-results": " tsx --env-file=.env.local ./scripts/export-results.ts", @@ -57,6 +57,8 @@ "zod": "^4.1.13" }, "devDependencies": { + "@antfu/eslint-config": "^6.2.0", + "@eslint-react/eslint-plugin": "^2.3.7", "@rollup/plugin-yaml": "^4.1.2", "@tailwindcss/typography": "^0.5.15", "@testing-library/jest-dom": "^6.5.0", @@ -66,26 +68,22 @@ "@vitejs/plugin-react": "^5.0.4", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "eslint": "^8.57.0", - "eslint-plugin-astro": "^0.34.0", - "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint": "^9.18.0", + "eslint-plugin-astro": "^1.2.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-unicorn": "^55.0.0", "husky": "^9.1.6", "jsdom": "^25.0.0", "lint-staged": "^15.2.10", - "prettier": "^3.3.3", - "prettier-plugin-astro": "^0.14.1", "rimraf": "^6.0.1", "tsx": "^4.19.1", "vitest": "^3.2.4" }, "lint-staged": { "*.{js,jsx,ts,tsx,astro}": [ - "eslint --fix", - "prettier --write" - ], - "*.{md,mdx}": [ - "prettier --write" + "eslint --fix" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 278d1743..355080fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,12 @@ importers: specifier: ^4.1.13 version: 4.1.13 devDependencies: + '@antfu/eslint-config': + specifier: ^6.2.0 + version: 6.2.0(@eslint-react/eslint-plugin@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(@vue/compiler-sfc@3.5.25)(astro-eslint-parser@1.2.2)(eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier-plugin-astro@0.14.1)(typescript@5.5.4)(vitest@3.2.4) + '@eslint-react/eslint-plugin': + specifier: ^2.3.7 + version: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) '@rollup/plugin-yaml': specifier: ^4.1.2 version: 4.1.2(rollup@4.52.5) @@ -131,7 +137,7 @@ importers: version: 14.5.2(@testing-library/dom@10.4.0) '@typescript-eslint/parser': specifier: ^5.62.0 - version: 5.62.0(eslint@8.57.0)(typescript@5.5.4) + version: 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) '@vitejs/plugin-react': specifier: ^5.0.4 version: 5.0.4(vite@6.4.0(@types/node@22.5.0)(jiti@2.6.1)(tsx@4.19.1)(yaml@2.8.1)) @@ -142,17 +148,23 @@ importers: specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: ^9.18.0 + version: 9.39.1(jiti@2.6.1) eslint-plugin-astro: - specifier: ^0.34.0 - version: 0.34.0(eslint@8.57.0)(typescript@5.5.4) + specifier: ^1.2.0 + version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: - specifier: ^6.10.0 - version: 6.10.0(eslint@8.57.0) + specifier: ^6.10.2 + version: 6.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: + specifier: ^7.0.0 + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.16 + version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-unicorn: specifier: ^55.0.0 - version: 55.0.0(eslint@8.57.0) + version: 55.0.0(eslint@9.39.1(jiti@2.6.1)) husky: specifier: ^9.1.6 version: 9.1.6 @@ -162,12 +174,6 @@ importers: lint-staged: specifier: ^15.2.10 version: 15.2.10 - prettier: - specifier: ^3.3.3 - version: 3.3.3 - prettier-plugin-astro: - specifier: ^0.14.1 - version: 0.14.1 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -191,9 +197,67 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@antfu/eslint-config@6.2.0': + resolution: {integrity: sha512-ksasd+mJk441HHodwPh3Nhmwo9jSHUmgQyfTxMQM05U7SjDS0fy2KnXnPx0Vhc/CqKiJnx8wGpQCCJibyASX9Q==} + hasBin: true + peerDependencies: + '@eslint-react/eslint-plugin': ^2.0.1 + '@next/eslint-plugin-next': '>=15.0.0' + '@prettier/plugin-xml': ^3.4.1 + '@unocss/eslint-plugin': '>=0.50.0' + astro-eslint-parser: ^1.0.2 + eslint: ^9.10.0 + eslint-plugin-astro: ^1.2.0 + eslint-plugin-format: '>=0.1.0' + eslint-plugin-jsx-a11y: '>=6.10.2' + eslint-plugin-react-hooks: ^7.0.0 + eslint-plugin-react-refresh: ^0.4.19 + eslint-plugin-solid: ^0.14.3 + eslint-plugin-svelte: '>=2.35.1' + eslint-plugin-vuejs-accessibility: ^2.4.1 + prettier-plugin-astro: ^0.14.0 + prettier-plugin-slidev: ^1.0.5 + svelte-eslint-parser: '>=0.37.0' + peerDependenciesMeta: + '@eslint-react/eslint-plugin': + optional: true + '@next/eslint-plugin-next': + optional: true + '@prettier/plugin-xml': + optional: true + '@unocss/eslint-plugin': + optional: true + astro-eslint-parser: + optional: true + eslint-plugin-astro: + optional: true + eslint-plugin-format: + optional: true + eslint-plugin-jsx-a11y: + optional: true + eslint-plugin-react-hooks: + optional: true + eslint-plugin-react-refresh: + optional: true + eslint-plugin-solid: + optional: true + eslint-plugin-svelte: + optional: true + eslint-plugin-vuejs-accessibility: + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-slidev: + optional: true + svelte-eslint-parser: + optional: true + '@antfu/install-pkg@0.4.1': resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@antfu/utils@0.7.10': resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} @@ -352,6 +416,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -374,6 +442,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -422,6 +495,10 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -433,6 +510,12 @@ packages: resolution: {integrity: sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA==} engines: {node: '>=18'} + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@cloudflare/workers-types@4.20251011.0': resolution: {integrity: sha512-gQpih+pbq3sP4uXltUeCSbPgZxTNp2gQd8639SaIbQMwgA6oJNHLhIART1fWy6DQACngiRzDVULA2x0ohmkGTQ==} @@ -510,6 +593,18 @@ packages: resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} engines: {node: '>=18.0.0'} + '@es-joy/jsdoccomment@0.50.2': + resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} + engines: {node: '>=18'} + + '@es-joy/jsdoccomment@0.76.0': + resolution: {integrity: sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==} + engines: {node: '>=20.11.0'} + + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -948,23 +1043,99 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.5.0': + resolution: {integrity: sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.11.1': resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint-react/ast@2.3.7': + resolution: {integrity: sha512-PxAoMuk3dpcimo0rtWx9UEd0/NqNyg7pYs18kqoCuLxfLUpoV6wwwz/hIWNa3JvWu/5ALeelo5SCbUc7jTXQlQ==} + engines: {node: '>=20.19.0'} + + '@eslint-react/core@2.3.7': + resolution: {integrity: sha512-wHve68VM/WDS6ttHkUYAlHgRW+SJe9xCZB2yb0lJRAZzUM2f2vrbv1i9wKwJSmKUm4LA0Hha0oSg+XY2eWknwA==} + engines: {node: '>=20.19.0'} + + '@eslint-react/eff@2.3.7': + resolution: {integrity: sha512-RdsS0smV9WEriBkbVqCEggRy2HjTXtlqPjl5eJQGq+e4Cy8SPLS1XlbkdNW5iQI1mQwEqhkpy6Ucwt5VB6aKzA==} + engines: {node: '>=20.19.0'} + + '@eslint-react/eslint-plugin@2.3.7': + resolution: {integrity: sha512-pJUNL2JOiQ3x3OWBIdEaQeClsTqevmSJfsxXozTb3aujBaniBaD5B4XTyDNWiq2ZyPqAgKBjLBTKGXyjsaZwBA==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + '@eslint-react/shared@2.3.7': + resolution: {integrity: sha512-5QefzMEPOY8CPdNyEDlb3reXcLBLjxN1oRAOxhML//wWktYggFrG3oswKZ4NIrWwjgLSVQz1/kKd4D+82CcIMA==} + engines: {node: '>=20.19.0'} + + '@eslint-react/var@2.3.7': + resolution: {integrity: sha512-vBBWIWrkUR+J7Q08oADwPJE/iYTr+ro+DWHlReF6uwQ0s8refbefaJrbHiYV85XjqSKrUp2hHgTEBPuOf6QfGQ==} + engines: {node: '>=20.19.0'} + + '@eslint/compat@1.4.1': + resolution: {integrity: sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/markdown@7.5.1': + resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} @@ -1207,18 +1378,21 @@ packages: engines: {node: '>=6'} hasBin: true - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@iconify/tools@4.0.5': resolution: {integrity: sha512-l8KoA1lxlN/FFjlMd3vjfD7BtcX/QnFWtlBapILMlJSBgM5zhDYak/ldw/LkKG3258q/0YmXa48sO/QpxX7ptg==} @@ -1537,8 +1711,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.1.1': - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@polka/url@1.0.0-next.29': @@ -1881,6 +2055,16 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/base62@1.0.0': + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} + engines: {node: '>=18'} + + '@stylistic/eslint-plugin@5.6.1': + resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -2001,6 +2185,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.7': resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} @@ -2079,6 +2266,14 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.48.0': + resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.48.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@5.62.0': resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2089,14 +2284,48 @@ packages: typescript: optional: true + '@typescript-eslint/parser@8.48.0': + resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.48.0': + resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/scope-manager@8.48.0': + resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.48.0': + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.48.0': + resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@5.62.0': resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/types@8.48.0': + resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2106,10 +2335,27 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.48.0': + resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.48.0': + resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/visitor-keys@8.48.0': + resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -2139,6 +2385,19 @@ packages: '@vitest/browser': optional: true + '@vitest/eslint-plugin@1.5.0': + resolution: {integrity: sha512-j3uuIAPTYWYnSit9lspb08/EKsxEmGqjQf+Wpb1DQkxc+mMkhL58ZknDCgjYhY4Zu76oxZ0hVWTHlmRW0mJq5w==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2199,6 +2458,21 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + + '@vue/compiler-dom@3.5.25': + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + + '@vue/compiler-sfc@3.5.25': + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + + '@vue/compiler-ssr@3.5.25': + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -2316,6 +2590,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2325,9 +2603,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} - aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -2380,9 +2655,9 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro-eslint-parser@0.17.0: - resolution: {integrity: sha512-yTgzioUI9MKgBF4LxP7YI+uuZLrnXgHDeW4dpa3VqCNbDmPzL7ix93tc0JJIwWGcskoSAAHZZVaBSp8bHyZZZA==} - engines: {node: ^14.18.0 || >=16.0.0} + astro-eslint-parser@1.2.2: + resolution: {integrity: sha512-JepyLROIad6f44uyqMF6HKE2QbunNzp3mYKRcPoDGt0QkxXmH222FAFC64WTyQu2Kg8NNEXHTN/sWuUId9sSxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} astro-icon@1.1.1: resolution: {integrity: sha512-HKBesWk2Faw/0+klLX+epQVqdTfSzZz/9+5vxXUjTJaN/HnpDf608gRPgHh7ZtwBPNJMEFoU5GLegxoDcT56OQ==} @@ -2392,9 +2667,9 @@ packages: engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true - astrojs-compiler-sync@0.3.5: - resolution: {integrity: sha512-y420rhIIJ2HHDkYeqKArBHSdJNIIGMztLH90KGIX3zjcJyt/cr9Z2wYA8CP5J1w6KE7xqMh0DAkhfjhNDpQb2Q==} - engines: {node: ^14.18.0 || >=16.0.0} + astrojs-compiler-sync@1.1.1: + resolution: {integrity: sha512-0mKvB9sDQRIZPsEJadw6OaFbGJ92cJPPR++ICca9XEyiUAZqgVuk25jNmzHPT0KF80rI94trSZrUR5iHFXGGOQ==} + engines: {node: ^18.18.0 || >=20.9.0} peerDependencies: '@astrojs/compiler': '>=0.27.0' @@ -2456,6 +2731,10 @@ packages: resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} hasBin: true + baseline-browser-mapping@2.8.31: + resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} + hasBin: true + bignumber.js@9.1.2: resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} @@ -2466,6 +2745,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + birecord@0.1.1: + resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2496,6 +2778,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -2506,6 +2793,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2535,6 +2826,9 @@ packages: caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + caniuse-lite@1.0.30001757: + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2558,6 +2852,9 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -2678,15 +2975,28 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2707,6 +3017,9 @@ packages: core-js-compat@3.38.1: resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} + core-js-compat@3.47.0: + resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -2718,6 +3031,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} @@ -2820,10 +3137,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2876,6 +3189,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@27.5.1: + resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -2891,10 +3208,6 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -3029,6 +3342,9 @@ packages: electron-to-chromium@1.5.237: resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + electron-to-chromium@1.5.260: + resolution: {integrity: sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==} + emmet@2.4.7: resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==} @@ -3051,10 +3367,18 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3078,13 +3402,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - - es-iterator-helpers@1.0.19: - resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -3150,36 +3467,237 @@ packages: peerDependencies: eslint: '>=6.0.0' - eslint-plugin-astro@0.34.0: - resolution: {integrity: sha512-nzw2H4g7HPXPLsWVpGUxuJ/ViVPLI8lM/AaUCJ51qTLTWtaMhvlvoe2d7yIPMFc+7xeCzQdo1POK8eR7NFsdKQ==} + eslint-compat-utils@0.6.5: + resolution: {integrity: sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-flat-gitignore@2.1.0: + resolution: {integrity: sha512-cJzNJ7L+psWp5mXM7jBX+fjHtBvvh06RBlcweMhKD8jWqQw0G78hOW5tpVALGHGFPsBV+ot2H+pdDGJy6CV8pA==} + peerDependencies: + eslint: ^9.5.0 + + eslint-flat-config-utils@2.1.4: + resolution: {integrity: sha512-bEnmU5gqzS+4O+id9vrbP43vByjF+8KOs+QuuV4OlqAuXmnRW2zfI/Rza1fQvdihQ5h4DUo0NqFAiViD4mSrzQ==} + + eslint-json-compat-utils@0.2.1: + resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==} + engines: {node: '>=12'} + peerDependencies: + '@eslint/json': '*' + eslint: '*' + jsonc-eslint-parser: ^2.4.0 + peerDependenciesMeta: + '@eslint/json': + optional: true + + eslint-merge-processors@2.0.0: + resolution: {integrity: sha512-sUuhSf3IrJdGooquEUB5TNpGNpBoQccbnaLHsb1XkBLUPPqCNivCpY05ZcpCOiV9uHwO2yxXEWVczVclzMxYlA==} + peerDependencies: + eslint: '*' + + eslint-plugin-antfu@3.1.1: + resolution: {integrity: sha512-7Q+NhwLfHJFvopI2HBZbSxWXngTwBLKxW1AGXLr2lEGxcEIK/AsDs8pn8fvIizl5aZjBbVbVK5ujmMpBe4Tvdg==} + peerDependencies: + eslint: '*' + + eslint-plugin-astro@1.5.0: + resolution: {integrity: sha512-IWy4kY3DKTJxd7g652zIWpBGFuxw7NIIt16kyqc8BlhnIKvI8yGJj+Maua0DiNYED3F/D8AmzoTTTA6A95WX9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.57.0' + + eslint-plugin-command@3.3.1: + resolution: {integrity: sha512-fBVTXQ2y48TVLT0+4A6PFINp7GcdIailHAXbvPBixE7x+YpYnNQhFZxTdvnb+aWk+COgNebQKen/7m4dmgyWAw==} + peerDependencies: + eslint: '*' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - eslint: '>=7.0.0' + eslint: '>=8' + + eslint-plugin-import-lite@0.3.0: + resolution: {integrity: sha512-dkNBAL6jcoCsXZsQ/Tt2yXmMDoNt5NaBh/U7yvccjiK8cai6Ay+MK77bMykmqQA2bTF6lngaLCDij6MTO3KkvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=4.5' + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-jsdoc@61.4.1: + resolution: {integrity: sha512-3c1QW/bV25sJ1MsIvsvW+EtLtN6yZMduw7LVQNVt72y2/5BbV5Pg5b//TE5T48LRUxoEQGaZJejCmcj3wCxBzw==} + engines: {node: '>=20.11.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-jsonc@2.21.0: + resolution: {integrity: sha512-HttlxdNG5ly3YjP1cFMP62R4qKLxJURfBZo2gnMY+yQojZxkLyOpY1H1KRTKBmvQeSG9pIpSGEhDjE17vvYosg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' - eslint-plugin-jsx-a11y@6.10.0: - resolution: {integrity: sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==} + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} engines: {node: '>=4.0'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + eslint-plugin-n@17.23.1: + resolution: {integrity: sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-only-tests@3.3.0: + resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} + engines: {node: '>=5.0.0'} + + eslint-plugin-perfectionist@4.15.1: + resolution: {integrity: sha512-MHF0cBoOG0XyBf7G0EAFCuJJu4I18wy0zAoT1OHfx2o6EOx1EFTIzr2HGeuZa1kDcusoX0xJ9V7oZmaeFd773Q==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + eslint: '>=8.45.0' + + eslint-plugin-pnpm@1.3.0: + resolution: {integrity: sha512-Lkdnj3afoeUIkDUu8X74z60nrzjQ2U55EbOeI+qz7H1He4IO4gmUKT2KQIl0It52iMHJeuyLDWWNgjr6UIK8nw==} + peerDependencies: + eslint: ^9.0.0 + + eslint-plugin-react-dom@2.3.7: + resolution: {integrity: sha512-T8r1oKTEh42LUA3VB8J6EXGoX2m3TFCGDp7ehcyoNoJNf+o8BdWI4zg62euR6t/CwEUrvxF2wdADGxXH4+J5XA==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + eslint-plugin-react-hooks-extra@2.3.7: + resolution: {integrity: sha512-IqVbpbRcsZ16AXP3wuePBj3nuPnZF2MPapdlg2ygfNKNQehlSVAYHZHzzuSjhI+4UR/w+AhuhQfcanz14DN/+A==} + engines: {node: '>=20.0.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {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 + + eslint-plugin-react-naming-convention@2.3.7: + resolution: {integrity: sha512-pW0l0kY8Wf42g3R/D8DVVibRc7BvU49VFshKCYQ4DLA53FPJsdOd9b28aKyyvgDAh65HDFEJKLvp03C+OkY7Fg==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react-web-api@2.3.7: + resolution: {integrity: sha512-DSosa2MGofBzaEdPaqXUwskk91NNa2Igw7bYNQ83eRd1zjLHictuSmboc8HFkufBF50G9Mhovn0QgGF3r//s6g==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + eslint-plugin-react-x@2.3.7: + resolution: {integrity: sha512-NDFRTSSDUp3uvmQA/Khm6xfxG4Fbc7/fDX2M5TgbqbKsN+kmu9T1J4jr/5AoWe4DU82ECl7RrWQUHZ49ahVlfQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^9.39.1 + typescript: ^5.9.3 + + eslint-plugin-regexp@2.10.0: + resolution: {integrity: sha512-ovzQT8ESVn5oOe5a7gIDPD5v9bCSjIFJu57sVPDqgPRXicQzOnYfFN21WoQBQF18vrhT5o7UMKFwJQVVjyJ0ng==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + + eslint-plugin-toml@0.12.0: + resolution: {integrity: sha512-+/wVObA9DVhwZB1nG83D2OAQRrcQZXy+drqUnFJKymqnmbnbfg/UPmEMCKrJNcEboUGxUjYrJlgy+/Y930mURQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + eslint-plugin-unicorn@55.0.0: resolution: {integrity: sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==} engines: {node: '>=18.18'} peerDependencies: eslint: '>=8.56.0' - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-plugin-unicorn@62.0.0: + resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-unused-imports@4.3.0: + resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vue@10.6.0: + resolution: {integrity: sha512-TsoFluWxOpsJlE/l2jJygLQLWBPJ3Qdkesv7tBIunICbTcG0dS1/NBw/Ol4tJw5kHWlAVds4lUmC29/vlPUcEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-plugin-yml@1.19.0: + resolution: {integrity: sha512-S+4GbcCWksFKAvFJtf0vpdiCkZZvDJCV4Zsi9ahmYkYOYcf+LRqqzvzkb/ST7vTYV6sFwXOvawzYyL/jFT2nQA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-processor-vue-blocks@2.0.0: + resolution: {integrity: sha512-u4W0CJwGoWY3bjXAuFpc/b6eK3NQEI8MoeW7ritKj3G3z/WtHrKjkqf+wk8mPEy5rlMGS+k6AZYOw2XBoN/02Q==} + peerDependencies: + '@vue/compiler-sfc': ^3.3.0 + eslint: '>=9.0.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} @@ -3240,6 +3758,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3259,6 +3780,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3275,6 +3800,9 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} @@ -3298,9 +3826,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -3316,6 +3844,10 @@ packages: find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3331,12 +3863,9 @@ packages: firebase@10.13.0: resolution: {integrity: sha512-a8gm8c9CYO98QuXJn7m5W5Gj7kHV8fme81/mQ9dBs+VMz9uI5HdavnMVPXCILputpZFMFpiKK+u7VVsn5lQg+w==} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -3375,6 +3904,10 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -3474,14 +4007,22 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} globals@15.9.0: resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} engines: {node: '>=18'} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -3490,6 +4031,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-auth-library@9.14.1: resolution: {integrity: sha512-Rj+PMjoNFGFTmtItH7gHfbHpGVSb3vmnGK3nwNBqxQF9NoBpttSZI/rc0WiM63ma2uGDQtYEkMHkK9U6937NiA==} engines: {node: '>=14'} @@ -3580,6 +4124,12 @@ packages: hastscript@8.0.0: resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3593,6 +4143,9 @@ packages: html-entities@2.5.2: resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3650,6 +4203,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@2.0.2: resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} engines: {node: '>=16.x'} @@ -3670,6 +4227,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3696,10 +4257,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -3707,10 +4264,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-async-function@2.0.0: - resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} - engines: {node: '>= 0.4'} - is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -3726,6 +4279,10 @@ packages: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -3754,9 +4311,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.0.2: - resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3769,10 +4323,6 @@ packages: resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} engines: {node: '>=18'} - is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3780,15 +4330,17 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-immutable-type@5.0.1: + resolution: {integrity: sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==} + peerDependencies: + eslint: '*' + typescript: '>=4.7.4' + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} hasBin: true - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -3801,10 +4353,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3816,10 +4364,6 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.3: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} engines: {node: '>= 0.4'} @@ -3844,17 +4388,9 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - is-weakset@2.0.3: - resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} - engines: {node: '>= 0.4'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -3881,9 +4417,6 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - iterator.prototype@1.1.2: - resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3921,6 +4454,18 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsdoc-type-pratt-parser@4.8.0: + resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} + engines: {node: '>=12.0.0'} + + jsdoc-type-pratt-parser@6.10.0: + resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==} + engines: {node: '>=20.0.0'} + jsdom@25.0.0: resolution: {integrity: sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==} engines: {node: '>=18'} @@ -3972,6 +4517,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-eslint-parser@2.4.1: + resolution: {integrity: sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-parser@2.3.1: resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} @@ -4070,6 +4619,10 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -4160,6 +4713,9 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -4186,6 +4742,12 @@ packages: mdast-util-from-markdown@2.0.1: resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} @@ -4204,6 +4766,9 @@ packages: mdast-util-gfm@3.0.0: resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.0: resolution: {integrity: sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==} @@ -4250,6 +4815,9 @@ packages: micromark-core-commonmark@2.0.1: resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -4334,6 +4902,9 @@ packages: micromark-util-normalize-identifier@2.0.0: resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + micromark-util-resolve-all@2.0.0: resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} @@ -4424,6 +4995,9 @@ packages: mlly@1.7.1: resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4458,6 +5032,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -4503,6 +5081,9 @@ packages: node-releases@2.0.25: resolution: {integrity: sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4533,6 +5114,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-deep-merge@2.0.0: + resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -4541,10 +5125,6 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -4643,6 +5223,9 @@ packages: resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} engines: {node: '>=14'} + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4650,6 +5233,9 @@ packages: parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + parse5-htmlparser2-tree-adapter@7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} @@ -4740,10 +5326,19 @@ packages: pkg-types@1.2.0: resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + pnpm-workspace-yaml@1.3.0: + resolution: {integrity: sha512-Krb5q8Totd5mVuLx7we+EFHq/AfxA75nbfTm25Q1pIf606+RlaKUG+PXH8SDihfe5b5k4H09gE+sL47L1t5lbw==} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -4786,6 +5381,10 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -4860,6 +5459,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-string@9.1.0: resolution: {integrity: sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==} engines: {node: '>=18'} @@ -4957,9 +5559,9 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - reflect.getprototypeof@1.0.6: - resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} - engines: {node: '>= 0.4'} + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -4973,6 +5575,10 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -4985,6 +5591,10 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + rehype-parse@9.0.0: resolution: {integrity: sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==} @@ -5039,6 +5649,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5088,11 +5702,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -5136,6 +5745,10 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -5242,6 +5855,9 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + spdx-license-ids@3.0.20: resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} @@ -5255,10 +5871,6 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} - stream-events@1.0.5: resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} @@ -5269,6 +5881,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-ts@2.2.1: + resolution: {integrity: sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5281,8 +5896,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string.prototype.includes@2.0.0: - resolution: {integrity: sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==} + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} string.prototype.trim@1.2.9: resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} @@ -5317,6 +5933,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5367,8 +5987,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.9.1: - resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} tailwind-merge@2.5.2: @@ -5384,6 +6004,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -5400,9 +6024,6 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5456,6 +6077,14 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-valid-identifier@1.0.0: + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} + engines: {node: '>=20'} + + toml-eslint-parser@0.10.0: + resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + tosource@2.0.0-alpha.3: resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} engines: {node: '>=10'} @@ -5481,6 +6110,17 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -5498,6 +6138,9 @@ packages: '@swc/wasm': optional: true + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} @@ -5532,10 +6175,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -5721,6 +6360,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -6042,6 +6687,12 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -6086,14 +6737,6 @@ packages: which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - which-builtin-type@1.1.4: - resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -6151,6 +6794,10 @@ packages: utf-8-validate: optional: true + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -6178,6 +6825,10 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml-eslint-parser@1.3.1: + resolution: {integrity: sha512-MdSgP9YA9QjtAO2+lt4O7V2bnH22LPnfeVLiQqjY3cOyn8dy/Ief8otjIe6SPPTK03nM7O3Yl0LTfWuF7l+9yw==} + engines: {node: ^14.17.0 || >=16.0.0} + yaml-language-server@1.15.0: resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} hasBin: true @@ -6242,6 +6893,12 @@ packages: typescript: ^4.9.4 || ^5.0.2 zod: ^3 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -6262,11 +6919,70 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@antfu/eslint-config@6.2.0(@eslint-react/eslint-plugin@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(@vue/compiler-sfc@3.5.25)(astro-eslint-parser@1.2.2)(eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier-plugin-astro@0.14.1)(typescript@5.5.4)(vitest@3.2.4)': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@clack/prompts': 0.11.0 + '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint/markdown': 7.5.1 + '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@vitest/eslint-plugin': 1.5.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)(vitest@3.2.4) + ansis: 4.2.0 + cac: 6.7.14 + eslint: 9.39.1(jiti@2.6.1) + eslint-config-flat-gitignore: 2.1.0(eslint@9.39.1(jiti@2.6.1)) + eslint-flat-config-utils: 2.1.4 + eslint-merge-processors: 2.0.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-antfu: 3.1.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-command: 3.3.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-lite: 0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-jsdoc: 61.4.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-jsonc: 2.21.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-n: 17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-perfectionist: 4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-pnpm: 1.3.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-regexp: 2.10.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-toml: 0.12.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-unicorn: 62.0.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-vue: 10.6.0(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) + eslint-plugin-yml: 1.19.0(eslint@9.39.1(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)) + globals: 16.5.0 + jsonc-eslint-parser: 2.4.1 + local-pkg: 1.1.2 + parse-gitignore: 2.0.0 + toml-eslint-parser: 0.10.0 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1)) + yaml-eslint-parser: 1.3.1 + optionalDependencies: + '@eslint-react/eslint-plugin': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + astro-eslint-parser: 1.2.2 + eslint-plugin-astro: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-refresh: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + prettier-plugin-astro: 0.14.1 + transitivePeerDependencies: + - '@eslint/json' + - '@vue/compiler-sfc' + - supports-color + - typescript + - vitest + '@antfu/install-pkg@0.4.1': dependencies: package-manager-detector: 0.2.0 tinyexec: 0.3.0 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.4.1 + tinyexec: 1.0.1 + '@antfu/utils@0.7.10': {} '@astro-community/astro-embed-youtube@0.5.3(astro@5.14.5(@netlify/blobs@10.1.0)(@types/node@22.5.0)(jiti@2.6.1)(rollup@4.52.5)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.8.1))': @@ -6579,6 +7295,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': @@ -6601,6 +7319,10 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -6670,6 +7392,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@bundled-es-modules/message-format@6.2.4': {} @@ -6678,6 +7405,17 @@ snapshots: dependencies: fontkit: 2.0.4 + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cloudflare/workers-types@4.20251011.0': optional: true @@ -6784,6 +7522,24 @@ snapshots: tslib: 2.8.1 optional: true + '@es-joy/jsdoccomment@0.50.2': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.48.0 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@es-joy/jsdoccomment@0.76.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.48.0 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 6.10.0 + + '@es-joy/resolve.exports@1.2.0': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -7003,19 +7759,127 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + '@eslint-community/eslint-plugin-eslint-comments@4.5.0(eslint@9.39.1(jiti@2.6.1))': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.39.1(jiti@2.6.1) + ignore: 5.3.2 + + '@eslint-community/eslint-utils@4.4.0(eslint@9.39.1(jiti@2.6.1))': dependencies: - eslint: 8.57.0 + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': + dependencies: + eslint: 9.39.1(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.11.1': {} - '@eslint/eslintrc@2.1.4': + '@eslint-community/regexpp@4.12.2': {} + + '@eslint-react/ast@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-react/eff': 2.3.7 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + string-ts: 2.2.1 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/core@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + birecord: 0.1.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/eff@2.3.7': {} + + '@eslint-react/eslint-plugin@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + eslint-plugin-react-dom: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-react-hooks-extra: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-react-naming-convention: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-react-web-api: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint-plugin-react-x: 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@eslint-react/shared@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-react/eff': 2.3.7 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + ts-pattern: 5.9.0 + zod: 4.1.13 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/var@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint/compat@1.4.1(eslint@9.39.1(jiti@2.6.1))': + dependencies: + '@eslint/core': 0.17.0 + optionalDependencies: + eslint: 9.39.1(jiti@2.6.1) + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.3.6 - espree: 9.6.1 - globals: 13.24.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -7024,7 +7888,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@9.39.1': {} + + '@eslint/markdown@7.5.1': + dependencies: + '@eslint/core': 0.17.0 + '@eslint/plugin-kit': 0.4.1 + github-slugger: 2.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-frontmatter: 2.0.1 + mdast-util-gfm: 3.1.0 + micromark-extension-frontmatter: 2.0.0 + micromark-extension-gfm: 3.0.0 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 '@fastify/busboy@2.1.1': {} @@ -7421,17 +8306,16 @@ snapshots: protobufjs: 7.4.0 yargs: 17.7.2 - '@humanwhocodes/config-array@0.11.14': + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@iconify/tools@4.0.5': dependencies: @@ -7802,7 +8686,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/core@0.1.1': {} + '@pkgr/core@0.2.9': {} '@polka/url@1.0.0-next.29': {} @@ -8075,6 +8959,18 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/base62@1.0.0': {} + + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1))': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/types': 8.48.0 + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -8222,6 +9118,8 @@ snapshots: '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.7': dependencies: '@types/node': 22.5.0 @@ -8312,25 +9210,86 @@ snapshots: '@types/node': 22.5.0 optional: true - '@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.48.0 + eslint: 9.39.1(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) debug: 4.3.6 - eslint: 8.57.0 + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.48.0 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.48.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + debug: 4.4.3 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 + '@typescript-eslint/scope-manager@8.48.0': + dependencies: + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 + + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.5.4)': + dependencies: + typescript: 5.5.4 + + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@5.62.0': {} + '@typescript-eslint/types@8.48.0': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -8345,11 +9304,42 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/project-service': 8.48.0(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.48.0': + dependencies: + '@typescript-eslint/types': 8.48.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.2.0': {} '@vercel/nft@0.27.10(rollup@4.52.5)': @@ -8414,6 +9404,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/eslint-plugin@1.5.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4)(vitest@3.2.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + optionalDependencies: + typescript: 5.5.4 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.5.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@25.0.0)(tsx@4.19.1)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -8517,6 +9518,38 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.25': + dependencies: + '@vue/compiler-core': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-sfc@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.25 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.25': + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/shared@3.5.25': {} + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -8640,8 +9673,7 @@ snapshots: ansi-styles@6.2.1: {} - ansis@4.2.0: - optional: true + ansis@4.2.0: {} any-promise@1.3.0: {} @@ -8650,6 +9682,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + are-docs-informative@0.0.2: {} + arg@4.1.3: optional: true @@ -8657,10 +9691,6 @@ snapshots: argparse@2.0.1: {} - aria-query@5.1.3: - dependencies: - deep-equal: 2.2.3 - aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -8725,23 +9755,22 @@ snapshots: astring@1.9.0: {} - astro-eslint-parser@0.17.0(typescript@5.5.4): + astro-eslint-parser@1.2.2: dependencies: - '@astrojs/compiler': 2.10.3 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4) - astrojs-compiler-sync: 0.3.5(@astrojs/compiler@2.10.3) - debug: 4.3.6 - entities: 4.5.0 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - globby: 11.1.0 + '@astrojs/compiler': 2.13.0 + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + astrojs-compiler-sync: 1.1.1(@astrojs/compiler@2.13.0) + debug: 4.4.3 + entities: 6.0.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + fast-glob: 3.3.3 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.3 transitivePeerDependencies: - supports-color - - typescript astro-icon@1.1.1: dependencies: @@ -8853,10 +9882,10 @@ snapshots: - uploadthing - yaml - astrojs-compiler-sync@0.3.5(@astrojs/compiler@2.10.3): + astrojs-compiler-sync@1.1.1(@astrojs/compiler@2.13.0): dependencies: - '@astrojs/compiler': 2.10.3 - synckit: 0.9.1 + '@astrojs/compiler': 2.13.0 + synckit: 0.11.11 async-retry@1.3.3: dependencies: @@ -8913,6 +9942,8 @@ snapshots: baseline-browser-mapping@2.8.18: {} + baseline-browser-mapping@2.8.31: {} + bignumber.js@9.1.2: optional: true @@ -8922,6 +9953,8 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + birecord@0.1.1: {} + boolbase@1.0.0: {} boxen@8.0.1: @@ -8967,12 +10000,22 @@ snapshots: node-releases: 2.0.25 update-browserslist-db: 1.1.3(browserslist@4.26.3) + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.31 + caniuse-lite: 1.0.30001757 + electron-to-chromium: 1.5.260 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} builtin-modules@3.3.0: {} + builtin-modules@5.0.0: {} + cac@6.7.14: {} call-bind@1.0.7: @@ -8996,6 +10039,8 @@ snapshots: caniuse-lite@1.0.30001751: {} + caniuse-lite@1.0.30001757: {} + ccount@2.0.1: {} chai@5.3.3: @@ -9024,6 +10069,8 @@ snapshots: chalk@5.3.0: {} + change-case@5.4.4: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -9140,12 +10187,20 @@ snapshots: commander@7.2.0: {} + comment-parser@1.4.1: {} + common-ancestor-path@1.0.1: {} + compare-versions@6.1.1: {} + concat-map@0.0.1: {} confbox@0.1.7: {} + confbox@0.1.8: {} + + confbox@0.2.2: {} + consola@3.4.2: {} convert-source-map@1.9.0: {} @@ -9160,6 +10215,10 @@ snapshots: dependencies: browserslist: 4.23.3 + core-js-compat@3.47.0: + dependencies: + browserslist: 4.28.0 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -9177,6 +10236,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.3.5: dependencies: uncrypto: 0.1.3 @@ -9272,27 +10337,6 @@ snapshots: deep-eql@5.0.2: {} - deep-equal@2.2.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - es-get-iterator: 1.1.3 - get-intrinsic: 1.2.4 - is-arguments: 1.1.1 - is-array-buffer: 3.0.4 - is-date-object: 1.0.5 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.3 - isarray: 2.0.5 - object-is: 1.1.6 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.2 - side-channel: 1.0.6 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.2 - which-typed-array: 1.1.15 - deep-is@0.1.4: {} define-data-property@1.1.4: @@ -9336,6 +10380,8 @@ snapshots: didyoumean@1.2.2: {} + diff-sequences@27.5.1: {} + diff@4.0.2: optional: true @@ -9347,10 +10393,6 @@ snapshots: dlv@1.1.3: {} - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -9409,6 +10451,8 @@ snapshots: electron-to-chromium@1.5.237: {} + electron-to-chromium@1.5.260: {} + emmet@2.4.7: dependencies: '@emmetio/abbreviation': 2.3.3 @@ -9420,8 +10464,7 @@ snapshots: emoji-regex@9.2.2: {} - empathic@2.0.0: - optional: true + empathic@2.0.0: {} encoding-sniffer@0.2.0: dependencies: @@ -9432,8 +10475,15 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@4.5.0: {} + entities@6.0.1: {} + env-paths@3.0.0: optional: true @@ -9498,35 +10548,6 @@ snapshots: es-errors@1.3.0: {} - es-get-iterator@1.1.3: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - is-arguments: 1.1.1 - is-map: 2.0.3 - is-set: 2.0.3 - is-string: 1.0.7 - isarray: 2.0.5 - stop-iteration-iterator: 1.0.0 - - es-iterator-helpers@1.0.19: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - es-set-tostringtag: 2.0.3 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - globalthis: 1.0.4 - has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 - internal-slot: 1.0.7 - iterator.prototype: 1.1.2 - safe-array-concat: 1.1.2 - es-module-lexer@1.7.0: {} es-object-atoms@1.0.0: @@ -9655,29 +10676,111 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@8.57.0): + eslint-compat-utils@0.5.1(eslint@9.39.1(jiti@2.6.1)): dependencies: - eslint: 8.57.0 + eslint: 9.39.1(jiti@2.6.1) semver: 7.6.3 - eslint-plugin-astro@0.34.0(eslint@8.57.0)(typescript@5.5.4): + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@jridgewell/sourcemap-codec': 1.5.0 - '@typescript-eslint/types': 5.62.0 - astro-eslint-parser: 0.17.0(typescript@5.5.4) - eslint: 8.57.0 - eslint-compat-utils: 0.5.1(eslint@8.57.0) - globals: 13.24.0 - postcss: 8.4.41 - postcss-selector-parser: 6.1.2 + eslint: 9.39.1(jiti@2.6.1) + semver: 7.7.3 + + eslint-config-flat-gitignore@2.1.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@eslint/compat': 1.4.1(eslint@9.39.1(jiti@2.6.1)) + eslint: 9.39.1(jiti@2.6.1) + + eslint-flat-config-utils@2.1.4: + dependencies: + pathe: 2.0.3 + + eslint-json-compat-utils@0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + esquery: 1.6.0 + jsonc-eslint-parser: 2.4.1 + + eslint-merge-processors@2.0.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + + eslint-plugin-antfu@3.1.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + + eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.48.0 + astro-eslint-parser: 1.2.2 + eslint: 9.39.1(jiti@2.6.1) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1)) + globals: 16.5.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-command@3.3.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@es-joy/jsdoccomment': 0.50.2 + eslint: 9.39.1(jiti@2.6.1) + + eslint-plugin-es-x@7.8.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.11.1 + eslint: 9.39.1(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@9.39.1(jiti@2.6.1)) + + eslint-plugin-import-lite@0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/types': 8.48.0 + eslint: 9.39.1(jiti@2.6.1) + optionalDependencies: + typescript: 5.5.4 + + eslint-plugin-jsdoc@61.4.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@es-joy/jsdoccomment': 0.76.0 + '@es-joy/resolve.exports': 1.2.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 9.39.1(jiti@2.6.1) + espree: 10.4.0 + esquery: 1.6.0 + html-entities: 2.6.0 + object-deep-merge: 2.0.0 + parse-imports-exports: 0.2.4 + semver: 7.7.3 + spdx-expression-parse: 4.0.0 + to-valid-identifier: 1.0.0 transitivePeerDependencies: - supports-color - - typescript - eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.0): + eslint-plugin-jsonc@2.21.0(eslint@9.39.1(jiti@2.6.1)): dependencies: - aria-query: 5.1.3 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + diff-sequences: 27.5.1 + eslint: 9.39.1(jiti@2.6.1) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1)) + eslint-json-compat-utils: 0.2.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1) + espree: 9.6.1 + graphemer: 1.4.0 + jsonc-eslint-parser: 2.4.1 + natural-compare: 1.4.0 + synckit: 0.11.11 + transitivePeerDependencies: + - '@eslint/json' + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 array-includes: 3.1.8 array.prototype.flatmap: 1.3.2 ast-types-flow: 0.0.8 @@ -9685,24 +10788,188 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - es-iterator-helpers: 1.0.19 - eslint: 8.57.0 + eslint: 9.39.1(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.2 object.fromentries: 2.0.8 safe-regex-test: 1.0.3 - string.prototype.includes: 2.0.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-n@17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + enhanced-resolve: 5.18.3 + eslint: 9.39.1(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@9.39.1(jiti@2.6.1)) + get-tsconfig: 4.8.1 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.7.3 + ts-declaration-location: 1.0.7(typescript@5.5.4) + transitivePeerDependencies: + - typescript + + eslint-plugin-no-only-tests@3.3.0: {} + + eslint-plugin-perfectionist@4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-pnpm@1.3.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + empathic: 2.0.0 + eslint: 9.39.1(jiti@2.6.1) + jsonc-eslint-parser: 2.4.1 + pathe: 2.0.3 + pnpm-workspace-yaml: 1.3.0 + tinyglobby: 0.2.15 + yaml-eslint-parser: 1.3.1 + + eslint-plugin-react-dom@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/core': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + compare-versions: 6.1.1 + eslint: 9.39.1(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks-extra@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/core': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.5 + eslint: 9.39.1(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.1.13 + zod-validation-error: 4.0.2(zod@4.1.13) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-naming-convention@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/core': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + + eslint-plugin-react-web-api@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/core': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-x@2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@eslint-react/ast': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/core': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/eff': 2.3.7 + '@eslint-react/shared': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@eslint-react/var': 2.3.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + compare-versions: 6.1.1 + eslint: 9.39.1(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + string-ts: 2.2.1 + ts-api-utils: 2.1.0(typescript@5.5.4) + ts-pattern: 5.9.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + eslint-plugin-regexp@2.10.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.11.1 + comment-parser: 1.4.1 + eslint: 9.39.1(jiti@2.6.1) + jsdoc-type-pratt-parser: 4.8.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-toml@0.12.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1)) + lodash: 4.17.21 + toml-eslint-parser: 0.10.0 + transitivePeerDependencies: + - supports-color - eslint-plugin-unicorn@55.0.0(eslint@8.57.0): + eslint-plugin-unicorn@55.0.0(eslint@9.39.1(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.24.7 - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.39.1(jiti@2.6.1)) ci-info: 4.0.0 clean-regexp: 1.0.0 core-js-compat: 3.38.1 - eslint: 8.57.0 + eslint: 9.39.1(jiti@2.6.1) esquery: 1.6.0 globals: 15.9.0 indent-string: 4.0.0 @@ -9715,56 +10982,121 @@ snapshots: semver: 7.6.3 strip-indent: 3.0.0 - eslint-scope@7.2.2: + eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint/plugin-kit': 0.4.1 + change-case: 5.4.4 + ci-info: 4.3.1 + clean-regexp: 1.0.0 + core-js-compat: 3.47.0 + eslint: 9.39.1(jiti@2.6.1) + esquery: 1.6.0 + find-up-simple: 1.0.1 + globals: 16.5.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.13.0 + semver: 7.7.3 + strip-indent: 4.1.1 + + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + + eslint-plugin-vue@10.6.0(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.39.1(jiti@2.6.1)) + eslint: 9.39.1(jiti@2.6.1) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.0 + semver: 7.7.3 + vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + + eslint-plugin-yml@1.19.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + diff-sequences: 27.5.1 + escape-string-regexp: 4.0.0 + eslint: 9.39.1(jiti@2.6.1) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@2.6.1)) + natural-compare: 1.4.0 + yaml-eslint-parser: 1.3.1 + transitivePeerDependencies: + - supports-color + + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@vue/compiler-sfc': 3.5.25 + eslint: 9.39.1(jiti@2.6.1) + + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.0: + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.6 - doctrine: 3.0.0 + cross-spawn: 7.0.6 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + espree@9.6.1: dependencies: acorn: 8.12.1 @@ -9837,6 +11169,8 @@ snapshots: expect-type@1.2.2: {} + exsolve@1.0.8: {} + extend@3.0.2: {} extract-zip@2.0.1: @@ -9861,6 +11195,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -9876,6 +11218,10 @@ snapshots: dependencies: reusify: 1.0.4 + fault@2.0.1: + dependencies: + format: 0.2.2 + faye-websocket@0.11.4: dependencies: websocket-driver: 0.7.4 @@ -9899,9 +11245,9 @@ snapshots: fflate@0.8.2: {} - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 file-uri-to-path@1.0.0: {} @@ -9913,6 +11259,8 @@ snapshots: find-root@1.1.0: {} + find-up-simple@1.0.1: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -9973,13 +11321,10 @@ snapshots: transitivePeerDependencies: - '@react-native-async-storage/async-storage' - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: - flatted: 3.3.1 + flatted: 3.3.3 keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.1: {} flatted@3.3.3: {} @@ -10026,6 +11371,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + format@0.2.2: {} + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -10145,12 +11492,14 @@ snapshots: globals@11.12.0: {} - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} + + globals@15.15.0: {} globals@15.9.0: {} + globals@16.5.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -10165,6 +11514,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + google-auth-library@9.14.1: dependencies: base64-js: 1.5.1 @@ -10376,6 +11727,12 @@ snapshots: property-information: 6.5.0 space-separated-tokens: 2.0.2 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -10389,6 +11746,8 @@ snapshots: html-entities@2.5.2: optional: true + html-entities@2.6.0: {} + html-escaper@2.0.2: {} html-escaper@3.0.3: {} @@ -10451,6 +11810,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-size@2.0.2: optional: true @@ -10465,6 +11826,8 @@ snapshots: indent-string@4.0.0: {} + indent-string@5.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -10491,11 +11854,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-arguments@1.1.1: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -10503,10 +11861,6 @@ snapshots: is-arrayish@0.2.1: {} - is-async-function@2.0.0: - dependencies: - has-tostringtag: 1.0.2 - is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -10524,6 +11878,10 @@ snapshots: dependencies: builtin-modules: 3.3.0 + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + is-callable@1.2.7: {} is-core-module@2.15.1: @@ -10544,10 +11902,6 @@ snapshots: is-extglob@2.1.1: {} - is-finalizationregistry@1.0.2: - dependencies: - call-bind: 1.0.7 - is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} @@ -10556,22 +11910,26 @@ snapshots: dependencies: get-east-asian-width: 1.2.0 - is-generator-function@1.0.10: - dependencies: - has-tostringtag: 1.0.2 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-hexadecimal@2.0.1: {} + is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4): + dependencies: + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.5.4) + eslint: 9.39.1(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.5.4) + ts-declaration-location: 1.0.7(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 - is-map@2.0.3: {} - is-negative-zero@2.0.3: {} is-number-object@1.0.7: @@ -10580,8 +11938,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} is-potential-custom-element-name@1.0.1: {} @@ -10591,8 +11947,6 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 - is-set@2.0.3: {} - is-shared-array-buffer@1.0.3: dependencies: call-bind: 1.0.7 @@ -10614,17 +11968,10 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-weakmap@2.0.2: {} - is-weakref@1.0.2: dependencies: call-bind: 1.0.7 - is-weakset@2.0.3: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -10654,14 +12001,6 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - iterator.prototype@1.1.2: - dependencies: - define-properties: 1.2.1 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - reflect.getprototypeof: 1.0.6 - set-function-name: 2.0.2 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -10699,6 +12038,12 @@ snapshots: dependencies: argparse: 2.0.1 + jsdoc-type-pratt-parser@4.1.0: {} + + jsdoc-type-pratt-parser@4.8.0: {} + + jsdoc-type-pratt-parser@6.10.0: {} + jsdom@25.0.0: dependencies: cssstyle: 4.1.0 @@ -10752,6 +12097,13 @@ snapshots: json5@2.2.3: {} + jsonc-eslint-parser@2.4.1: + dependencies: + acorn: 8.15.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.7.3 + jsonc-parser@2.3.1: {} jsonc-parser@3.3.1: {} @@ -10902,6 +12254,12 @@ snapshots: mlly: 1.7.1 pkg-types: 1.2.0 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -10979,6 +12337,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: dependencies: '@babel/parser': 7.25.4 @@ -11026,6 +12388,34 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.0 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -11083,6 +12473,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -11195,6 +12597,13 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.0 @@ -11397,6 +12806,10 @@ snapshots: dependencies: micromark-util-symbol: 2.0.0 + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-resolve-all@2.0.0: dependencies: micromark-util-types: 2.0.0 @@ -11500,6 +12913,13 @@ snapshots: pkg-types: 1.2.0 ufo: 1.5.4 + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mrmime@2.0.1: {} ms@2.1.2: {} @@ -11522,6 +12942,8 @@ snapshots: natural-compare@1.4.0: {} + natural-orderby@5.0.0: {} + neotraverse@0.6.18: {} nlcst-to-string@4.0.0: @@ -11552,6 +12974,8 @@ snapshots: node-releases@2.0.25: {} + node-releases@2.0.27: {} + nopt@8.1.0: dependencies: abbrev: 3.0.1 @@ -11579,15 +13003,12 @@ snapshots: object-assign@4.1.1: {} + object-deep-merge@2.0.0: {} + object-hash@3.0.0: {} object-inspect@1.13.2: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - object-keys@1.1.1: {} object.assign@4.1.5: @@ -11699,8 +13120,11 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-gitignore@2.0.0: - optional: true + parse-gitignore@2.0.0: {} + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 parse-json@5.2.0: dependencies: @@ -11718,6 +13142,8 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-statements@1.0.11: {} + parse5-htmlparser2-tree-adapter@7.0.0: dependencies: domhandler: 5.0.3 @@ -11785,8 +13211,24 @@ snapshots: mlly: 1.7.1 pathe: 1.1.2 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + pluralize@8.0.0: {} + pnpm-workspace-yaml@1.3.0: + dependencies: + yaml: 2.8.1 + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.41): @@ -11832,6 +13274,11 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-value-parser@4.2.0: {} postcss@8.4.41: @@ -11850,14 +13297,16 @@ snapshots: prettier-plugin-astro@0.14.1: dependencies: - '@astrojs/compiler': 2.10.3 + '@astrojs/compiler': 2.13.0 prettier: 3.3.3 sass-formatter: 0.7.9 + optional: true prettier@2.8.7: optional: true - prettier@3.3.3: {} + prettier@3.3.3: + optional: true pretty-format@27.5.1: dependencies: @@ -11915,6 +13364,8 @@ snapshots: punycode@2.3.1: {} + quansync@0.2.11: {} + query-string@9.1.0: dependencies: decode-uri-component: 0.4.1 @@ -12041,15 +13492,9 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - reflect.getprototypeof@1.0.6: + refa@0.12.1: dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - globalthis: 1.0.4 - which-builtin-type: 1.1.4 + '@eslint-community/regexpp': 4.11.1 regenerator-runtime@0.14.1: {} @@ -12063,6 +13508,11 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.11.1 + refa: 0.12.1 + regexp-tree@0.1.27: {} regexp.prototype.flags@1.5.2: @@ -12076,6 +13526,10 @@ snapshots: dependencies: jsesc: 0.5.0 + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + rehype-parse@9.0.0: dependencies: '@types/hast': 3.0.4 @@ -12175,6 +13629,8 @@ snapshots: requires-port@1.0.0: {} + reserved-identifiers@1.2.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -12236,10 +13692,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rimraf@6.0.1: dependencies: glob: 11.0.0 @@ -12279,7 +13731,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 - s.color@0.0.15: {} + s.color@0.0.15: + optional: true safe-array-concat@1.1.2: dependencies: @@ -12301,6 +13754,7 @@ snapshots: sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 + optional: true saxes@6.0.0: dependencies: @@ -12310,6 +13764,12 @@ snapshots: dependencies: loose-envify: 1.4.0 + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.11.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + semver@5.7.2: {} semver@6.3.1: {} @@ -12438,6 +13898,11 @@ snapshots: spdx-exceptions: 2.5.0 spdx-license-ids: 3.0.20 + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + spdx-license-ids@3.0.20: {} split-on-first@3.0.0: {} @@ -12446,10 +13911,6 @@ snapshots: std-env@3.10.0: {} - stop-iteration-iterator@1.0.0: - dependencies: - internal-slot: 1.0.7 - stream-events@1.0.5: dependencies: stubs: 3.0.0 @@ -12460,6 +13921,8 @@ snapshots: string-argv@0.3.2: {} + string-ts@2.2.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -12478,8 +13941,9 @@ snapshots: get-east-asian-width: 1.2.0 strip-ansi: 7.1.0 - string.prototype.includes@2.0.0: + string.prototype.includes@2.0.1: dependencies: + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.23.3 @@ -12526,6 +13990,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-indent@4.1.1: {} + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -12561,6 +14027,7 @@ snapshots: suf-log@2.5.3: dependencies: s.color: 0.0.15 + optional: true supports-color@5.5.0: dependencies: @@ -12584,10 +14051,9 @@ snapshots: symbol-tree@3.2.4: {} - synckit@0.9.1: + synckit@0.11.11: dependencies: - '@pkgr/core': 0.1.1 - tslib: 2.7.0 + '@pkgr/core': 0.2.9 tailwind-merge@2.5.2: {} @@ -12622,6 +14088,8 @@ snapshots: transitivePeerDependencies: - ts-node + tapable@2.3.0: {} + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -12657,8 +14125,6 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12702,6 +14168,15 @@ snapshots: dependencies: is-number: 7.0.0 + to-valid-identifier@1.0.0: + dependencies: + '@sindresorhus/base62': 1.0.0 + reserved-identifiers: 1.2.0 + + toml-eslint-parser@0.10.0: + dependencies: + eslint-visitor-keys: 3.4.3 + tosource@2.0.0-alpha.3: {} totalist@3.0.1: {} @@ -12723,6 +14198,15 @@ snapshots: trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-declaration-location@1.0.7(typescript@5.5.4): + dependencies: + picomatch: 4.0.3 + typescript: 5.5.4 + ts-interface-checker@0.1.13: {} ts-node@10.9.2(@types/node@22.5.0)(typescript@5.5.4): @@ -12744,6 +14228,8 @@ snapshots: yn: 3.1.1 optional: true + ts-pattern@5.9.0: {} + tsconfck@3.1.6(typescript@5.5.4): optionalDependencies: typescript: 5.5.4 @@ -12770,8 +14256,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} @@ -12940,6 +14424,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -13220,6 +14710,18 @@ snapshots: vscode-uri@3.1.0: {} + vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -13264,28 +14766,6 @@ snapshots: is-string: 1.0.7 is-symbol: 1.0.4 - which-builtin-type@1.1.4: - dependencies: - function.prototype.name: 1.1.6 - has-tostringtag: 1.0.2 - is-async-function: 2.0.0 - is-date-object: 1.0.5 - is-finalizationregistry: 1.0.2 - is-generator-function: 1.0.10 - is-regex: 1.1.4 - is-weakref: 1.0.2 - isarray: 2.0.5 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.2 - which-typed-array: 1.1.15 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.3 - which-pm-runs@1.1.0: {} which-typed-array@1.1.15: @@ -13339,6 +14819,8 @@ snapshots: ws@8.18.0: {} + xml-name-validator@4.0.0: {} + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -13355,6 +14837,11 @@ snapshots: yallist@5.0.0: {} + yaml-eslint-parser@1.3.1: + dependencies: + eslint-visitor-keys: 3.4.3 + yaml: 2.8.1 + yaml-language-server@1.15.0: dependencies: ajv: 8.17.1 @@ -13376,8 +14863,7 @@ snapshots: yaml@2.5.0: {} - yaml@2.8.1: - optional: true + yaml@2.8.1: {} yargs-parser@21.1.1: {} @@ -13418,6 +14904,10 @@ snapshots: typescript: 5.5.4 zod: 3.25.76 + zod-validation-error@4.0.2(zod@4.1.13): + dependencies: + zod: 4.1.13 + zod@3.25.76: {} zod@4.1.13: {} diff --git a/prettier.config.cjs b/prettier.config.cjs deleted file mode 100644 index 2378c736..00000000 --- a/prettier.config.cjs +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import("prettier").Config} */ -module.exports = { - plugins: [require.resolve('prettier-plugin-astro')], - trailingComma: 'none', - overrides: [ - { - files: '*.astro', - options: { - parser: 'astro' - } - } - ] -} \ No newline at end of file diff --git a/results/2024/sections/ai.mdx b/results/2024/sections/ai.mdx index 17a45d7e..06229f93 100644 --- a/results/2024/sections/ai.mdx +++ b/results/2024/sections/ai.mdx @@ -5,7 +5,7 @@ position: 6 # AI -AI continues to be a hot topic in all the tech communities around the world. Globally, some companies consider it as an enabler and decided to fully embrace it, whereas other companies are much more reserved against its usage by their employees. This year, we continue to explore its impact on Moroccan software developers and how they are adjusting to this evolving trend. +AI continues to be a hot topic in all the tech communities around the world. Globally, some companies consider it as an enabler and decided to fully embrace it, whereas other companies are much more reserved against its usage by their employees. This year, we continue to explore its impact on Moroccan software developers and how they are adjusting to this evolving trend. Moroccan software developers are embracing the AI age. They are learning AI and building AI projects. @@ -54,8 +54,8 @@ Amongst the respondents who use AI tools, most of them are using OpenAI's models -In the enterprise world, compagnies are slowly but increasingly investing in AI with more use cases in -production. +In the enterprise world, compagnies are slowly but increasingly investing in AI +with more use cases in production. diff --git a/scripts/export-results.ts b/scripts/export-results.ts index 322462ad..f024817b 100644 --- a/scripts/export-results.ts +++ b/scripts/export-results.ts @@ -1,4 +1,5 @@ -import fs from "fs"; +import fs from "node:fs"; +import process from "node:process"; import { exportResults } from "@/lib/firebase/database"; function getFileName() { @@ -12,16 +13,20 @@ function writeToFile(filename: string, data: any) { fs.writeFile(filename, JSON.stringify(data), (err: any) => { if (err) { console.log(err); - } else { - console.log(`[SUCCESS] ${new Date()} JSON saved to ${filename}`); + } + else { + console.log(`[SUCCESS] ${new Date().toISOString()} JSON saved to ${filename}`); } }); } -const run = async () => { +async function run() { console.log("exporting results"); const data = await exportResults(); writeToFile(getFileName(), data); -}; +} -run(); +run().catch((error) => { + console.error("Error exporting results:", error); + process.exit(1); +}); diff --git a/scripts/generate-questions.ts b/scripts/generate-questions.ts index 6e44be1b..224ab823 100644 --- a/scripts/generate-questions.ts +++ b/scripts/generate-questions.ts @@ -1,19 +1,20 @@ // this is a simple script to convert the questions from yaml to json so it can be used in the playground and chart component +/* eslint-disable node/prefer-global/process */ -import yaml from "js-yaml"; -import fs from "fs"; -import path from "path"; -import { - validateAllSurveyFiles, - formatValidationReport -} from "../src/lib/validators/survey-validator"; import type { SurveyQuestion, SurveyQuestionsYamlFile } from "../src/lib/validators/survey-schema"; +import fs from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; import { SURVEY_DIR } from "../src/lib/validators/constants"; +import { + formatValidationReport, + validateAllSurveyFiles +} from "../src/lib/validators/survey-validator"; -const generate = async () => { +async function generate() { console.log("Starting survey questions generation...\n"); // Phase 1: Validate all survey files @@ -39,7 +40,7 @@ const generate = async () => { try { const files = fs .readdirSync(SURVEY_DIR) - .filter((file) => file.endsWith(".yml")) + .filter(file => file.endsWith(".yml")) .sort(); const data: SurveyQuestionsYamlFile[] = []; @@ -68,20 +69,25 @@ const generate = async () => { console.log( `\n✓ Successfully generated ${Object.keys(QS).length} questions` ); - } catch (e) { + } + catch (e) { console.error("\n❌ Error generating questions:", e); process.exit(1); } -}; +} function writeToFile(filename: string, data: Record) { fs.writeFile(filename, JSON.stringify(data), (err: any) => { if (err) { console.log(err); - } else { - console.log(`[SUCCESS] ${new Date()} JSON saved to ${filename}`); + } + else { + console.log(`[SUCCESS] ${new Date().toISOString()} JSON saved to ${filename}`); } }); } -generate(); +generate().catch((error) => { + console.error("Error generating questions:", error); + process.exit(1); +}); diff --git a/scripts/validate-survey.ts b/scripts/validate-survey.ts index 53b9aeb7..875c9a94 100644 --- a/scripts/validate-survey.ts +++ b/scripts/validate-survey.ts @@ -2,12 +2,13 @@ * Standalone script to validate all survey YAML files * This can be run independently or as part of the build process */ +/* eslint-disable node/prefer-global/process */ +import { SURVEY_DIR } from "../src/lib/validators/constants"; import { - validateAllSurveyFiles, - formatValidationReport + formatValidationReport, + validateAllSurveyFiles } from "../src/lib/validators/survey-validator"; -import { SURVEY_DIR } from "../src/lib/validators/constants"; async function main() { console.log("🔍 Validating survey YAML files...\n"); @@ -19,10 +20,14 @@ async function main() { if (validationReport.success) { console.log("\n✓ All validations passed!"); process.exit(0); - } else { + } + else { console.error("\n❌ Validation failed! Please fix the errors above."); process.exit(1); } } -main(); +main().catch((error) => { + console.error("Error validating survey:", error); + process.exit(1); +}); diff --git a/src/actions/init-session.ts b/src/actions/init-session.ts index 77490558..ca7eb56d 100644 --- a/src/actions/init-session.ts +++ b/src/actions/init-session.ts @@ -1,10 +1,11 @@ -import { getActiveApp } from "@/lib/firebase/server"; -import { defineAction, ActionError } from "astro:actions"; -import { getAuth } from "firebase-admin/auth"; +/* eslint-disable no-console */ +import { ActionError, defineAction } from "astro:actions"; import { z } from "astro:schema"; -import { initUserSubmission } from "@/lib/firebase/database"; +import { getAuth } from "firebase-admin/auth"; import { isCaptchaValid } from "@/lib/captcha"; -// Add this import +import { initUserSubmission } from "@/lib/firebase/database"; +import { getActiveApp } from "@/lib/firebase/server"; + export const initSession = defineAction({ accept: "json", input: z.object({ @@ -52,7 +53,8 @@ export const initSession = defineAction({ } await initUserSubmission(user); console.log("user session created"); - } catch (error) { + } + catch (error) { console.error("Error verifying id token:", error); throw new ActionError({ code: "UNAUTHORIZED", @@ -74,7 +76,8 @@ export const initSession = defineAction({ return { success: true }; - } catch (error) { + } + catch (error) { console.error("Error signing in:", error); throw new ActionError({ code: "UNAUTHORIZED", diff --git a/src/actions/submit-answers.ts b/src/actions/submit-answers.ts index 00727ff0..aa07da73 100644 --- a/src/actions/submit-answers.ts +++ b/src/actions/submit-answers.ts @@ -1,8 +1,9 @@ -import { getActiveApp } from "@/lib/firebase/server"; -import { defineAction, ActionError } from "astro:actions"; -import { getAuth } from "firebase-admin/auth"; +/* eslint-disable no-console */ +import { ActionError, defineAction } from "astro:actions"; import { z } from "astro:schema"; +import { getAuth } from "firebase-admin/auth"; import { saveAnswers } from "@/lib/firebase/database"; +import { getActiveApp } from "@/lib/firebase/server"; export const submitAnswers = defineAction({ accept: "json", @@ -36,7 +37,8 @@ export const submitAnswers = defineAction({ /* Save answers to database */ await saveAnswers(user.uid, answers); console.log("answers saved"); - } catch (error) { + } + catch (error) { console.error("Error token or saving answers:", error); throw new ActionError({ code: "INTERNAL_SERVER_ERROR", diff --git a/src/components/chart/bar-chart.tsx b/src/components/chart/bar-chart.tsx index a4ec7e37..d7c5e128 100644 --- a/src/components/chart/bar-chart.tsx +++ b/src/components/chart/bar-chart.tsx @@ -1,4 +1,5 @@ -import { getPercent, type FinalResult } from "./utils"; +import type { FinalResult } from "./utils"; +import { getPercent } from "./utils"; // Chart colors from theme const colors = [ @@ -14,65 +15,78 @@ const colors = [ "bg-chart-10" ]; -type BarChartProps = { +interface BarChartProps { results: FinalResult | null; sortByTotal?: boolean; showEmptyOptions?: boolean; -}; +} -type BarProps = { +interface BarProps { result: FinalResult["results"][number]; index: number; total: number; -}; +} -const Tooltip = ({ result }: { result: FinalResult["results"][number] }) => { - if (!result.grouped) return null; +function Tooltip({ result }: { result: FinalResult["results"][number] }) { + if (!result.grouped) + return null; return ( - {result.label} : {result.total}{" "} + {result.label} + {" "} + : + {result.total} + {" "} - {result.grouped.results.length > 6 ? ( - - {Array.from({ - length: Math.ceil(result.grouped.results.length / 6) - }).map((_, tableIndex) => ( - + {result.grouped.results.length > 6 + ? ( + + {Array.from({ + length: Math.ceil(result.grouped.results.length / 6) + }).map((_, tableIndex) => { + const tableGroups = result.grouped!.results.slice(tableIndex * 6, (tableIndex + 1) * 6); + return ( + + + {tableGroups.map(group => ( + + {group.label} + + {group.total} + {" "} + ( + {((group.total / result.total) * 100).toFixed(1)} + %) + + + ))} + + + ); + })} + + ) + : ( + - {result?.grouped?.results - .slice(tableIndex * 6, (tableIndex + 1) * 6) - .map((group, index) => ( - - {group.label} - - {group.total} ( - {((group.total / result.total) * 100).toFixed(1)} - %) - - - ))} + {result.grouped.results.map(group => ( + + {group.label} + + {group.total} + {" "} + ( + {((group.total / result.total) * 100).toFixed(1)} + %) + + + ))} - ))} - - ) : ( - - - {result.grouped.results.map((group, index) => ( - - {group.label} - - {group.total} ( - {((group.total / result.total) * 100).toFixed(1)}%) - - - ))} - - - )} + )} { ); -}; +} -const Bar = ({ result, index, total }: BarProps) => { +function Bar({ result, index, total }: BarProps) { const displayResults = result.grouped ? result.grouped.results : [result]; return ( @@ -102,50 +116,55 @@ const Bar = ({ result, index, total }: BarProps) => { - {getPercent(result.total, total)}% -{" "} + {getPercent(result.total, total)} + % - + {" "} {result.total} - {!result.grouped ? ( - - ) : ( - - {displayResults.map((group, groupIndex) => ( + {!result.grouped + ? ( - ))} - - )} + ) + : ( + + {displayResults.map((group, groupIndex) => ( + + ))} + + )} ); -}; +} -export const BarChart = ({ +export function BarChart({ results, sortByTotal = true, showEmptyOptions = true -}: BarChartProps) => { - if (!results) return null; +}: BarChartProps) { + if (!results) + return null; const displayResults = sortByTotal ? [...results.results].sort((a, b) => b.total - a.total) @@ -153,13 +172,13 @@ export const BarChart = ({ const filteredResults = showEmptyOptions ? displayResults - : displayResults.filter((result) => result.total > 0); + : displayResults.filter(result => result.total > 0); // Create a set of unique labels for the legend const legendLabels = new Set(); filteredResults.forEach((result) => { if (result.grouped) { - result.grouped.results.forEach((group) => legendLabels.add(group.label)); + result.grouped.results.forEach(group => legendLabels.add(group.label)); } }); @@ -178,7 +197,9 @@ export const BarChart = ({ {results.isFiltered && "NOTE: Filters applied"} - Total: {results.total} + Total: + {" "} + {results.total} {/* legend for grouped questions */} @@ -190,7 +211,8 @@ export const BarChart = ({ className={`w-4 h-4 ${ colors[index % colors.length] } mr-2 opacity-60`} - > + > + {label} ))} @@ -198,4 +220,4 @@ export const BarChart = ({ )} ); -}; +} diff --git a/src/components/chart/chart-actions.tsx b/src/components/chart/chart-actions.tsx index 41c67ff9..643014ff 100644 --- a/src/components/chart/chart-actions.tsx +++ b/src/components/chart/chart-actions.tsx @@ -1,14 +1,14 @@ import type { Year } from "./data"; -import { ShareButtons } from "./share-buttons"; import type { FinalResult } from "./utils"; import queryString from "query-string"; +import { ShareButtons } from "./share-buttons"; -type ShareButtonsProps = { +interface ShareButtonsProps { results: FinalResult; year?: Year; -}; +} -export const ChartActions = ({ results, year }: ShareButtonsProps) => { +export function ChartActions({ results, year }: ShareButtonsProps) { const shareUrl = `/#${new URLSearchParams({ question_id: results.id }).toString()}`; @@ -19,21 +19,21 @@ export const ChartActions = ({ results, year }: ShareButtonsProps) => { ); -}; +} -const PlaygroundButton = ({ +function PlaygroundButton({ results, year }: { results: FinalResult; year?: Year; -}) => { +}) { return ( ); -}; +} diff --git a/src/components/chart/chart.astro b/src/components/chart/chart.astro index f317921d..d6d8e96f 100644 --- a/src/components/chart/chart.astro +++ b/src/components/chart/chart.astro @@ -1,9 +1,10 @@ --- -import { getQuestion, type QuestionCondition } from "./utils"; - import type { Year } from "./data"; + +import type { QuestionCondition } from "./utils"; import getContext from "@astro-utils/context"; import { Chart as RChart } from "./chart"; +import { getQuestion } from "./utils"; export interface Props { id: string; @@ -28,14 +29,16 @@ const results = getQuestion({ id, year, condition, groupBy }); --- { - results ? ( + results +? ( - ) : ( + ) +: ( {" "} No results for {id} in {year} diff --git a/src/components/chart/chart.test.ts b/src/components/chart/chart.test.ts index 44d5ae51..71531d73 100644 --- a/src/components/chart/chart.test.ts +++ b/src/components/chart/chart.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { getQuestion } from "./utils"; const questions = { @@ -49,7 +49,7 @@ const questions = { const results = [ { - userId: "user-0", + "userId": "user-0", "profile-q-0": 0, "profile-q-1": [0], "profile-q-2": 1, @@ -58,7 +58,7 @@ const results = [ "profile-q-5": [0, 1, 2, 3] }, { - userId: "user-1", + "userId": "user-1", "profile-q-0": 1, "profile-q-1": [0, 1], "profile-q-2": 1, @@ -67,7 +67,7 @@ const results = [ "profile-q-5": [2, 3, 4] }, { - userId: "user-2", + "userId": "user-2", "profile-q-0": 0, "profile-q-1": [1, 2], "profile-q-2": 2, @@ -76,7 +76,7 @@ const results = [ "profile-q-5": [0, 1, 4, 5] }, { - userId: "user-3", + "userId": "user-3", "profile-q-0": 1, "profile-q-1": [0], "profile-q-2": 0, @@ -86,7 +86,7 @@ const results = [ }, { // no answer for q-2 - userId: "user-4", + "userId": "user-4", "profile-q-0": 1, "profile-q-1": [0], "profile-q-3": [2], @@ -101,12 +101,12 @@ const dataSource = { }; describe("getQuestion Simple calculations", () => { - test("throws error for non-existent question id", () => { + it("throws error for non-existent question id", () => { const result = getQuestion({ id: "non-existent-id", dataSource }); expect(result).toBeNull(); }); - test("handles empty results", () => { + it("handles empty results", () => { const emptyDataSource = { ...dataSource, results: [] }; const result = getQuestion({ id: "profile-q-0", @@ -115,10 +115,10 @@ describe("getQuestion Simple calculations", () => { expect(result).not.toBeNull(); expect(result?.total).toBe(0); expect(result?.results).toHaveLength(2); - expect(result?.results.every((r) => r.total === 0)).toBeTruthy(); + expect(result?.results.every(r => r.total === 0)).toBeTruthy(); }); - test("returns correct data for a simple question", () => { + it("returns correct data for a simple question", () => { const result = getQuestion({ id: "profile-q-0", dataSource }); expect(result?.label).toBe("question 0"); expect(result?.total).toBe(5); @@ -129,17 +129,17 @@ describe("getQuestion Simple calculations", () => { expect(result?.results[1].total).toBe(3); }); - test("handles missing answers correctly", () => { + it("handles missing answers correctly", () => { const result = getQuestion({ id: "profile-q-2", dataSource }); expect(result?.total).toBe(4); // user-4 didn't answer this question }); - test("returns all choices even if not present in results", () => { + it("returns all choices even if not present in results", () => { const result = getQuestion({ id: "profile-q-4", dataSource }); expect(result?.results).toHaveLength(5); - expect(result?.results.some((r) => r.total === 0)).toBeTruthy(); + expect(result?.results.some(r => r.total === 0)).toBeTruthy(); }); - test("handles multiple choice questions correctly", () => { + it("handles multiple choice questions correctly", () => { const result = getQuestion({ id: "profile-q-1", dataSource }); expect(result?.total).toBe(5); expect(result?.results).toHaveLength( @@ -150,9 +150,10 @@ describe("getQuestion Simple calculations", () => { expect(result?.results[2].total).toBe(1); }); - test("total should equal the total of all results if the question is not multiple", () => { + it("total should equal the total of all results if the question is not multiple", () => { const result = getQuestion({ id: "profile-q-0", dataSource }); - if (!result) return; + if (!result) + return; const totalAllChoices = result.results.reduce( (acc, curr) => acc + curr.total, 0 @@ -163,13 +164,17 @@ describe("getQuestion Simple calculations", () => { describe("getQuestion Filters", () => { // filters - test("applies simple condition filter correctly with non multiple choice question", () => { + it("applies simple condition filter correctly with non multiple choice question", () => { const result = getQuestion({ id: "profile-q-0", dataSource, - condition: (v) => v["profile-q-4"] === 3 + condition: (v) => { + const value = v["profile-q-4"]; + return typeof value === "number" && value === 3; + } }); - if (!result) return; + if (!result) + return; const totalAllChoices = result.results.reduce( (acc, curr) => acc + curr.total, 0 @@ -179,16 +184,19 @@ describe("getQuestion Filters", () => { expect(result.results[1].total).toBe(2); }); - test("applies simple condition filter correctly with multiple choice question", () => { + it("applies simple condition filter correctly with multiple choice question", () => { const result = getQuestion({ id: "profile-q-0", dataSource, - condition: (v) => v["profile-q-5"].includes(3) + condition: (v) => { + const value = v["profile-q-5"]; + return Array.isArray(value) && (value as number[]).includes(3); + } }); expect(result?.total).toBe(4); }); - test("handles array condition filter", () => { + it("handles array condition filter", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -196,7 +204,7 @@ describe("getQuestion Filters", () => { }); expect(result?.total).toBe(2); }); - test("handles multiple value array condition filter", () => { + it("handles multiple value array condition filter", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -205,7 +213,7 @@ describe("getQuestion Filters", () => { expect(result?.total).toBe(3); }); - test("handles multiple value array condition filter", () => { + it("handles multiple value array condition filter with multiple questions", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -219,7 +227,7 @@ describe("getQuestion Filters", () => { expect(result?.results[1].total).toBe(2); }); - test("handles complex array condition filter with multiple questions", () => { + it("handles complex array condition filter with multiple questions", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -233,17 +241,17 @@ describe("getQuestion Filters", () => { expect(result?.results[1].total).toBe(0); }); - test("handles array condition filter with no matching values", () => { + it("handles array condition filter with no matching values", () => { const result = getQuestion({ id: "profile-q-0", dataSource, condition: [{ question_id: "profile-q-4", values: ["999"] }] }); expect(result?.total).toBe(0); - expect(result?.results.every((r) => r.total === 0)).toBeTruthy(); + expect(result?.results.every(r => r.total === 0)).toBeTruthy(); }); - test("handles array condition filter with empty values array", () => { + it("handles array condition filter with empty values array", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -252,32 +260,36 @@ describe("getQuestion Filters", () => { expect(result?.total).toBe(5); }); - test("handles function condition with complex logic", () => { + it("handles function condition with complex logic", () => { const result = getQuestion({ id: "profile-q-0", dataSource, - condition: (v) => - v["profile-q-4"] % 2 === 0 && v["profile-q-5"].includes(1) + condition: (v) => { + const value4 = v["profile-q-4"]; + const value5 = v["profile-q-5"]; + return typeof value4 === "number" && value4 % 2 === 0 && Array.isArray(value5) && (value5 as number[]).includes(1); + } }); expect(result?.total).toBe(1); expect(result?.results[0].total).toBe(1); expect(result?.results[1].total).toBe(0); }); - test("applies filter correctly to multiple choice questions", () => { + it("applies filter correctly to multiple choice questions", () => { const result = getQuestion({ id: "profile-q-1", dataSource, condition: [{ question_id: "profile-q-0", values: ["1"] }] }); - if (!result) return; + if (!result) + return; expect(result.total).toBe(3); expect(result.results[0].total).toBe(3); expect(result.results[1].total).toBe(1); expect(result.results[2].total).toBe(0); }); - test("handles missing answers in filter conditions", () => { + it("handles missing answers in filter conditions", () => { const result = getQuestion({ id: "profile-q-0", dataSource, @@ -286,14 +298,17 @@ describe("getQuestion Filters", () => { expect(result?.total).toBe(4); // user-4 should be excluded due to missing answer }); - test("sets isFiltered correctly", () => { + it("sets isFiltered correctly", () => { const resultNoFilter = getQuestion({ id: "profile-q-0", dataSource }); expect(resultNoFilter?.isFiltered).toBe(false); const resultWithFunctionFilter = getQuestion({ id: "profile-q-0", dataSource, - condition: (v) => v["profile-q-4"] === 3 + condition: (v) => { + const value = v["profile-q-4"]; + return typeof value === "number" && value === 3; + } }); expect(resultWithFunctionFilter?.isFiltered).toBe(true); @@ -314,25 +329,27 @@ describe("getQuestion Filters", () => { }); describe("getQuestion Grouping", () => { - test("groups results correctly", () => { + it("groups results correctly", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-4" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.results[0].grouped).not.toBeNull(); expect(result.results[0].grouped?.results).toHaveLength(5); }); - test("groups results correctly for single-choice questions", () => { + it("groups results correctly for single-choice questions", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-4" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.results[0].grouped).not.toBeNull(); expect(result.results[0].grouped?.results).toHaveLength(5); @@ -341,13 +358,14 @@ describe("getQuestion Grouping", () => { expect(result.results[1].grouped?.total).toBe(3); }); - test("groups results correctly for multiple-choice questions", () => { + it("groups results correctly for multiple-choice questions", () => { const result = getQuestion({ id: "profile-q-1", dataSource, groupBy: "profile-q-0" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(3); expect(result.results[0].grouped).not.toBeNull(); expect(result.results[0].grouped?.results).toHaveLength(2); @@ -355,13 +373,14 @@ describe("getQuestion Grouping", () => { expect(result.results[2].grouped?.results).toHaveLength(2); }); - test("handles grouping with missing answers", () => { + it("handles grouping with missing answers", () => { const result = getQuestion({ id: "profile-q-2", dataSource, groupBy: "profile-q-0" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(3); expect(result.results[0].grouped?.results).toHaveLength(2); expect(result.results[1].grouped?.results).toHaveLength(2); @@ -371,14 +390,18 @@ describe("getQuestion Grouping", () => { expect(result.results[2].grouped?.total).toBe(1); }); - test("groups results correctly with a filter applied", () => { + it("groups results correctly with a filter applied", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-4", - condition: (v) => v["profile-q-5"].includes(3) + condition: (v) => { + const value = v["profile-q-5"]; + return Array.isArray(value) && (value as number[]).includes(3); + } }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength( questions["profile-q-0"].choices.length ); @@ -393,25 +416,27 @@ describe("getQuestion Grouping", () => { expect(result.results[1].grouped?.total).toBe(result.results[1].total); }); - test("handles grouping by a question with more choices than answers", () => { + it("handles grouping by a question with more choices than answers", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-5" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.results[0].grouped?.results).toHaveLength(6); expect(result.results[1].grouped?.results).toHaveLength(6); }); - test("nested grouping works correctly", () => { + it("nested grouping works correctly", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-1" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.results[0].grouped?.results).toHaveLength(3); expect(result.results[1].grouped?.results).toHaveLength(3); @@ -421,47 +446,50 @@ describe("getQuestion Grouping", () => { expect(nestedGroup).toBeNull(); }); - test("grouping preserves all choices even if not present in results", () => { + it("grouping preserves all choices even if not present in results", () => { const result = getQuestion({ id: "profile-q-4", dataSource, groupBy: "profile-q-0" }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(5); expect( - result.results.every((r) => r.grouped?.results.length === 2) + result.results.every(r => r.grouped?.results.length === 2) ).toBeTruthy(); - expect(result.results.some((r) => r.total === 0)).toBeTruthy(); + expect(result.results.some(r => r.total === 0)).toBeTruthy(); }); - test("grouping handles condition that filters out all results", () => { + it("grouping handles condition that filters out all results", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-4", condition: () => false }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.results[0].grouped?.total).toBe(0); expect(result.results[1].grouped?.total).toBe(0); expect( - result.results[0].grouped?.results.every((r) => r.total === 0) + result.results[0].grouped?.results.every(r => r.total === 0) ).toBeTruthy(); expect( - result.results[1].grouped?.results.every((r) => r.total === 0) + result.results[1].grouped?.results.every(r => r.total === 0) ).toBeTruthy(); }); - test("grouping with array condition filter", () => { + it("grouping with array condition filter", () => { const result = getQuestion({ id: "profile-q-0", dataSource, groupBy: "profile-q-4", condition: [{ question_id: "profile-q-5", values: ["3"] }] }); - if (!result) return; + if (!result) + return; expect(result.results).toHaveLength(2); expect(result.total).toBe(4); expect(result.results[0].grouped?.total).toBe(result.results[0].total); diff --git a/src/components/chart/chart.tsx b/src/components/chart/chart.tsx index f243f9c1..8f6e5071 100644 --- a/src/components/chart/chart.tsx +++ b/src/components/chart/chart.tsx @@ -1,11 +1,11 @@ +import type { Year } from "./data"; +import type { FinalResult } from "./utils"; import React from "react"; import { BarChart } from "./bar-chart"; -import { PieChart } from "./pie-chart"; -import { type FinalResult } from "./utils"; import { ChartActions } from "./chart-actions"; -import type { Year } from "./data"; +import { PieChart } from "./pie-chart"; -type ChartProps = { +interface ChartProps { results: FinalResult | null; sortByTotal?: boolean; title?: boolean; @@ -13,7 +13,7 @@ type ChartProps = { pie?: boolean; year?: Year; showEmptyOptions?: boolean; -}; +} export const Chart: React.FC = ({ results, @@ -24,7 +24,8 @@ export const Chart: React.FC = ({ year, showEmptyOptions = true }) => { - if (!results) return null; + if (!results) + return null; const ChartComponent = pie ? PieChart : BarChart; @@ -42,7 +43,9 @@ export const Chart: React.FC = ({ {results?.otherOptions.length > 0 && ( - Others ({results?.otherOptions.length}) + Others ( + {results?.otherOptions.length} + ) submitted by participants diff --git a/src/components/chart/data.ts b/src/components/chart/data.ts index 7c83631b..e024108b 100644 --- a/src/components/chart/data.ts +++ b/src/components/chart/data.ts @@ -1,15 +1,15 @@ -import DATA_2020 from "@/results/2020/data/results.json"; import Questions_2020 from "@/results/2020/data/questions.json"; -import DATA_2021 from "@/results/2021/data/results.json"; +import DATA_2020 from "@/results/2020/data/results.json"; import Questions_2021 from "@/results/2021/data/questions.json"; +import DATA_2021 from "@/results/2021/data/results.json"; -import DATA_2022 from "@/results/2022/data/results.json"; import Questions_2022 from "@/results/2022/data/questions.json"; +import DATA_2022 from "@/results/2022/data/results.json"; -import DATA_2023 from "@/results/2023/data/results.json"; import Questions_2023 from "@/results/2023/data/questions.json"; -import DATA_2024 from "@/results/2024/data/results.json"; +import DATA_2023 from "@/results/2023/data/results.json"; import Questions_2024 from "@/results/2024/data/questions.json"; +import DATA_2024 from "@/results/2024/data/results.json"; export type Year = "2020" | "2021" | "2022" | "2023" | "2024"; export type Question = globalThis.Question; @@ -24,28 +24,28 @@ export type SurveyDataType = { }; const surveyData: SurveyDataType = { - "2020": { + 2020: { questions: Questions_2020 as unknown as QuestionMap, results: DATA_2020.results as unknown as Results["results"] }, - "2021": { + 2021: { questions: Questions_2021 as unknown as QuestionMap, results: DATA_2021.results as unknown as Results["results"] }, - "2022": { + 2022: { questions: Questions_2022 as unknown as QuestionMap, results: DATA_2022.results as unknown as Results["results"] }, - "2023": { + 2023: { questions: Questions_2023 as unknown as QuestionMap, results: DATA_2023.results as unknown as Results["results"] }, - "2024": { + 2024: { questions: Questions_2024 as unknown as QuestionMap, results: DATA_2024.results as unknown as Results["results"] } }; -export const getSurveyData = (year: Year) => { +export function getSurveyData(year: Year) { return surveyData[year]; -}; +} diff --git a/src/components/chart/pie-chart.tsx b/src/components/chart/pie-chart.tsx index 683fab2c..14e42843 100644 --- a/src/components/chart/pie-chart.tsx +++ b/src/components/chart/pie-chart.tsx @@ -1,4 +1,6 @@ -import { getPercent, type FinalResult } from "./utils"; +import type { FinalResult } from "./utils"; +import { useMemo } from "react"; +import { getPercent } from "./utils"; // Chart colors using CSS variables const colors = [ @@ -14,11 +16,11 @@ const colors = [ "var(--chart-10)" ]; -type PieChartProps = { +interface PieChartProps { results: FinalResult | null; sortByTotal?: boolean; -}; -const PieSlice = ({ +} +function PieSlice({ result, total, startAngle, @@ -30,7 +32,7 @@ const PieSlice = ({ startAngle: number; endAngle: number; color: string; -}) => { +}) { const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; const x1 = 50 + 50 * Math.cos((Math.PI * startAngle) / 180); const y1 = 50 + 50 * Math.sin((Math.PI * startAngle) / 180); @@ -41,7 +43,7 @@ const PieSlice = ({ const textX = 50 + 35 * Math.cos((Math.PI * midAngle) / 180); const textY = 50 + 35 * Math.sin((Math.PI * midAngle) / 180); - const percentage = parseFloat(getPercent(result.total, total)); + const percentage = Number.parseFloat(getPercent(result.total, total)); return ( @@ -60,42 +62,58 @@ const PieSlice = ({ fontSize="8px" className="font-medium" > - {percentage}% + {percentage} + % )} {`${result.label}: ${result.total} (${percentage}%)`} ); -}; +} -export const PieChart = ({ results, sortByTotal = true }: PieChartProps) => { - if (!results) return null; +export function PieChart({ results, sortByTotal = true }: PieChartProps) { + const displayResults = useMemo(() => { + if (!results) + return []; + return sortByTotal + ? [...results.results].sort((a, b) => b.total - a.total) + : results.results; + }, [results, sortByTotal]); - const displayResults = sortByTotal - ? [...results.results].sort((a, b) => b.total - a.total) - : results.results; + const slicesWithAngles = useMemo(() => { + if (!results) + return []; + return displayResults.reduce>((acc, result, index) => { + const sliceAngle = (result.total / results.total) * 360; + const previousEndAngle = acc.length > 0 ? acc[acc.length - 1].endAngle : 0; + const startAngle = previousEndAngle; + const endAngle = previousEndAngle + sliceAngle; + acc.push({ + result, + index, + startAngle, + endAngle + }); + return acc; + }, []); + }, [displayResults, results]); - let startAngle = 0; + if (!results) + return null; return ( - {displayResults.map((result, index) => { - const sliceAngle = (result.total / results.total) * 360; - const endAngle = startAngle + sliceAngle; - const slice = ( - - ); - startAngle = endAngle; - return slice; - })} + {slicesWithAngles.map(({ result, index, startAngle, endAngle }) => ( + + ))} {displayResults.map((result, index) => ( @@ -103,14 +121,20 @@ export const PieChart = ({ results, sortByTotal = true }: PieChartProps) => { + > + - {result.label}: {result.total} ( - {getPercent(result.total, results.total)}%) + {result.label} + : + {result.total} + {" "} + ( + {getPercent(result.total, results.total)} + %) ))} ); -}; +} diff --git a/src/components/chart/share-buttons.tsx b/src/components/chart/share-buttons.tsx index b1aba3d1..a646189f 100644 --- a/src/components/chart/share-buttons.tsx +++ b/src/components/chart/share-buttons.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; -type ShareButtonsProps = { +interface ShareButtonsProps { url: string; title: string; -}; +} -export const ShareButtons = ({ url, title }: ShareButtonsProps) => { +export function ShareButtons({ url, title }: ShareButtonsProps) { const [copied, setCopied] = useState(false); const shareLinks = { @@ -14,11 +14,10 @@ export const ShareButtons = ({ url, title }: ShareButtonsProps) => { facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}` }; - const copyToClipboard = () => { - navigator.clipboard.writeText(url).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); + const copyToClipboard = async () => { + await navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }; return ( @@ -86,7 +85,10 @@ export const ShareButtons = ({ url, title }: ShareButtonsProps) => { { + void copyToClipboard(); + }} className="!text-muted-foreground hover:!text-foreground transition-colors relative group" aria-label="Copy link" > @@ -116,4 +118,4 @@ export const ShareButtons = ({ url, title }: ShareButtonsProps) => { ); -}; +} diff --git a/src/components/chart/utils.ts b/src/components/chart/utils.ts index 0ea96646..57501dea 100644 --- a/src/components/chart/utils.ts +++ b/src/components/chart/utils.ts @@ -1,5 +1,5 @@ +import type { Question, QuestionMap, Results, Year } from "./data"; import { getSurveyData } from "./data"; -import type { Year, Question, Results, QuestionMap } from "./data"; /* Calculate the count of each option (choices for a question) Return the results as an object with two properties: @@ -18,28 +18,27 @@ import type { Year, Question, Results, QuestionMap } from "./data"; } */ -type OptionsCounts = { +interface OptionsCounts { total: number; results: { choiceIndex: string; total: number }[]; -}; -const calculateChoicesCounts = ( - data: Results["results"], - id: string -): OptionsCounts => { +} +function calculateChoicesCounts(data: Results["results"], id: string): OptionsCounts { const answers = data - .map((r) => r[id]) + .map(r => r[id]) .filter( - (v) => + v => v !== undefined && v !== null && (!Array.isArray(v) || v.length > 0) ); // in case some answers are missing or empty arrays const counts = answers.reduce( (acc, curr) => { - if (curr === undefined || curr === null) return acc; + if (curr === undefined || curr === null) + return acc; if (Array.isArray(curr)) { curr.forEach((element) => { acc[element] = (acc[element] || 0) + 1; }); - } else { + } + else { acc[curr] = (acc[curr] || 0) + 1; } return acc; @@ -48,7 +47,7 @@ const calculateChoicesCounts = ( ); const total = answers.length; - const results = Object.keys(counts).map((key) => ({ + const results = Object.keys(counts).map(key => ({ choiceIndex: key, total: counts[key] })); @@ -56,32 +55,34 @@ const calculateChoicesCounts = ( total, results }; -}; +} /** * the condition can be a function or an array of objects with question_id and values we want to filter by */ -export type QuestionCondition = - | ((v: any) => boolean) - | Array<{ question_id: string; values: string[] }>; - -const filterResultByCondition = ( - data: Results["results"], - condition?: QuestionCondition -): Results["results"] => { - if (!condition) return data; - if (typeof condition === "function") return data.filter(condition); +export type QuestionCondition + = | ((v: Results["results"][number]) => boolean) + | Array<{ question_id: string; values: string[] }>; + +function filterResultByCondition(data: Results["results"], condition?: QuestionCondition): Results["results"] { + if (!condition) + return data; + if (typeof condition === "function") + return data.filter(condition); if (Array.isArray(condition)) { return data.filter((v) => { for (let index = 0; index < condition.length; index++) { const element = condition[index]; - const vs = Array.isArray(v[element.question_id]) - ? v[element.question_id] - : [v[element.question_id]]; - - const intersection = - //@ts-ignore - vs.filter((x) => element?.values?.includes(x?.toString())) || []; + const questionValue = v[element.question_id]; + const vs: Array = Array.isArray(questionValue) + ? questionValue + : [questionValue]; + + const intersection = vs.filter((x): x is string | number => { + if (x === null || x === undefined) + return false; + return element?.values?.includes(String(x)) ?? false; + }); if (element.values.length !== 0 && intersection.length === 0) return false; } @@ -90,9 +91,9 @@ const filterResultByCondition = ( } return data; -}; +} -type GetQuestionParams = { +interface GetQuestionParams { id: string; condition?: QuestionCondition; groupBy?: string; @@ -101,14 +102,14 @@ type GetQuestionParams = { results: Results["results"]; }; year?: Year; -}; +} -type GroupedResult = { +interface GroupedResult { choiceIndex: string; total: number; label: string; grouped: FinalResult | null; -}; +} export type FinalResult = Question & { isFiltered: boolean; @@ -118,16 +119,17 @@ export type FinalResult = Question & { otherOptions: string[]; }; -export const getQuestion = ({ +export function getQuestion({ id, condition, groupBy, dataSource, year = "2020" -}: GetQuestionParams): FinalResult | null => { - const data = dataSource ? dataSource : getSurveyData(year); +}: GetQuestionParams): FinalResult | null { + const data = dataSource || getSurveyData(year); const question = data.questions[id]; - if (!question) return null; + if (!question) + return null; const resultsData = data.results; // Filter the data based on the condition const filteredResults = filterResultByCondition(resultsData, condition); @@ -141,7 +143,7 @@ export const getQuestion = ({ total: 0, label: c, ...(resultsWithChoicesCounts.results.find( - (r) => r.choiceIndex === index.toString() + r => r.choiceIndex === index.toString() ) || {}), grouped: groupBy === undefined @@ -149,7 +151,8 @@ export const getQuestion = ({ : getQuestion({ id: groupBy, condition: (v) => { - if (Array.isArray(v[id])) return v[id].includes(index); + if (Array.isArray(v[id])) + return (v[id] as number[]).includes(index); else return v[id] === index; }, dataSource: { ...data, results: filteredResults }, @@ -157,10 +160,10 @@ export const getQuestion = ({ }) })); - const isFiltered = - typeof condition === "function" + const isFiltered + = typeof condition === "function" ? true - : condition?.find((c) => c.values.length > 0) !== undefined; + : condition?.find(c => c.values.length > 0) !== undefined; return { total: resultsWithChoicesCounts.total, @@ -170,14 +173,19 @@ export const getQuestion = ({ isFiltered, otherOptions } as FinalResult; -}; +} -export const getPercent = (value: number, total: number) => { +export function getPercent(value: number, total: number) { return ((value * 100) / total).toFixed(1); -}; +} -export const getOtherOptions = (data: Results["results"], id: string) => { +export function getOtherOptions(data: Results["results"], id: string): string[] { const key = `${id}-others`; - const results = data.filter((r) => r[key]).map((r) => r[key]); + const results = data + .filter(r => r[key]) + .map((r) => { + const value = r[key]; + return typeof value === "string" ? value : String(value ?? ""); + }); return results; -}; +} diff --git a/src/components/faq-item.astro b/src/components/faq-item.astro index bffb3a42..16541b7f 100644 --- a/src/components/faq-item.astro +++ b/src/components/faq-item.astro @@ -31,7 +31,8 @@ const { checked, label }: Props = Astro.props; stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - d="M19 9l-7 7-7-7"> + d="M19 9l-7 7-7-7" + > open an issueopen an issue. diff --git a/src/components/footer.astro b/src/components/footer.astro index c1899a4b..22512d42 100644 --- a/src/components/footer.astro +++ b/src/components/footer.astro @@ -1,6 +1,6 @@ --- -import Logo from "../assets/logo.svg?raw"; import { website } from "@/website"; +import Logo from "../assets/logo.svg?raw"; const communityLinks = [ { label: "geeksblabla.community", href: "https://geeksblabla.community" }, @@ -46,7 +46,7 @@ const socialLinks = [ { - communityLinks.map((link) => ( + communityLinks.map(link => ( { - socialLinks.map((link) => ( + socialLinks.map(link => ( diff --git a/src/components/header.astro b/src/components/header.astro index 4f38e4c3..45eb05aa 100644 --- a/src/components/header.astro +++ b/src/components/header.astro @@ -1,10 +1,10 @@ --- -import Logo from "../assets/logo.svg?raw"; -import Github from "../assets/github.svg?raw"; +import { website } from "@/website"; import Chart from "../assets/Chart.svg?raw"; +import Github from "../assets/github.svg?raw"; +import Logo from "../assets/logo.svg?raw"; import HeaderLink from "./header-link.astro"; -import { ThemeToggle } from "./theme-toggle"; -import { website } from "@/website"; +import ThemeToggle from "./theme-toggle.astro"; const navItems = [ { year: 2020, href: "/2020" }, @@ -50,7 +50,8 @@ const currentPath = Astro.url.pathname; + d="M4 6h16M4 12h16M4 18h16" + > + d="M6 18L18 6M6 6l12 12" + > @@ -73,7 +75,7 @@ const currentPath = Astro.url.pathname; > { - navItems.map((item) => ( + navItems.map(item => ( - + @@ -97,7 +99,7 @@ const currentPath = Astro.url.pathname; > { - navItems.map((item) => ( + navItems.map(item => ( - + diff --git a/src/components/home/hero.astro b/src/components/home/hero.astro index 0c7b6736..931ce298 100644 --- a/src/components/home/hero.astro +++ b/src/components/home/hero.astro @@ -1,7 +1,7 @@ --- -import Crea2 from "../../assets/undraw_visual_data.svg"; -import Crea4 from "../../assets/data_image.svg"; import { YouTube } from "@astro-community/astro-embed-youtube"; +import Crea4 from "../../assets/data_image.svg"; +import Crea2 from "../../assets/undraw_visual_data.svg"; --- @@ -35,10 +35,12 @@ import { YouTube } from "@astro-community/astro-embed-youtube"; > + fill="currentColor" + > + fill="currentColor" + > Morocco 2025 🇲🇦 @@ -71,12 +73,14 @@ import { YouTube } from "@astro-community/astro-embed-youtube"; fill-rule="evenodd" clip-rule="evenodd" d="M0 21.3963C0.189514 19.1422 0.475057 16.717 0.554355 14.2852C0.582363 13.435 0.32301 12.6326 1.24839 12.1517C1.43863 12.053 1.7169 11.8956 1.85767 11.9661C4.2446 13.1626 6.90906 13.1934 9.41312 13.8814C11.09 14.3423 12.6519 15.089 13.7134 16.5797C13.9251 16.8774 13.9105 17.3427 14 17.7305C13.6228 17.8077 13.2227 18.01 12.8727 17.9421C10.3283 17.4477 7.78825 16.9245 5.25946 16.353C4.46612 16.1737 4.32244 16.4862 4.22859 17.1961C4.0118 18.8342 3.66769 20.4541 3.43198 22.0899C3.33086 22.7891 3.36905 23.509 3.35123 24.2197C3.34977 24.2791 3.44107 24.3474 3.43052 24.3989C3.32213 24.9318 3.2712 25.8796 3.07114 25.9142C2.49387 26.0144 1.77655 25.8915 1.25603 25.5961C-0.352473 24.6832 0.143681 23.0129 0 21.3963Z" - fill="currentColor"> + fill="currentColor" + > + fill="currentColor" + > Take part in the survey{" "} diff --git a/src/components/home/past-repports.tsx b/src/components/home/past-repports.tsx index 582e2360..f2bae232 100644 --- a/src/components/home/past-repports.tsx +++ b/src/components/home/past-repports.tsx @@ -53,122 +53,130 @@ const reports = [ } ]; -export const PastReports = () => ( - - - - Last year's reports - +export function PastReports() { + return ( + + + + Last year's reports + - - {reports.map((report, index) => ( - - - - ))} - + + {reports.map(report => ( + + + + ))} + - - - -); + + + + ); +} -type ReportCardProps = { +interface ReportCardProps { title: string; link: string; takeaways: string[]; totalSubmissions: number; -}; +} -export const ReportCard = ({ +export function ReportCard({ title, link, takeaways, totalSubmissions -}: ReportCardProps) => ( - - - {title} - +}: ReportCardProps) { + return ( + + + {title} + - - - Total Submissions: {totalSubmissions.toLocaleString()} - - - Key Takeaways: - - - {takeaways.map((takeaway, index) => ( - - - - - {takeaway} - - ))} - - - - - Read full report + + + Total Submissions: + {" "} + {totalSubmissions.toLocaleString()} + + + Key Takeaways: + + + {takeaways.map(takeaway => ( + + + + + {takeaway} + + ))} + + + + + Read full report + + + + + + + ); +} + +function DataPlaygroundSection() { + return ( + + + Or explore the data yourself! + + + Want to dive deeper into the survey results? Try our interactive data + playground! + + + Launch Data Playground - + - -); - -const DataPlaygroundSection = () => ( - - - Or explore the data yourself! - - - Want to dive deeper into the survey results? Try our interactive data - playground! - - - Launch Data Playground - - - - - -); + ); +} diff --git a/src/components/home/why.tsx b/src/components/home/why.tsx index f5f90e02..fd1d38c9 100644 --- a/src/components/home/why.tsx +++ b/src/components/home/why.tsx @@ -1,117 +1,129 @@ import React from "react"; // SVG Icon components -const BrainIcon = () => ( - - - - -); +function BrainIcon() { + return ( + + + + + ); +} -const UsersIcon = () => ( - - - - -); +function UsersIcon() { + return ( + + + + + ); +} -const LightbulbIcon = () => ( - - - - -); +function LightbulbIcon() { + return ( + + + + + ); +} -const ChartIcon = () => ( - - - - -); +function ChartIcon() { + return ( + + + + + ); +} -export const Why = () => ( - - - - - Why Join Our Awesome Survey? - - - }> - Be part of creating a global report that sheds light on Morocco's - development scene. Your insights matter! - - }> - Help local communities make informed decisions about learning paths - and create a bigger impact together. - - }> - Discover what others are thinking and find exciting new tools and - solutions to level up your work. - - }> - Stay ahead of the curve with insights on hot technologies and see how - you stack up against other developers. - +export function Why() { + return ( + + + + + Why Join Our Awesome Survey? + + + }> + Be part of creating a global report that sheds light on Morocco's + development scene. Your insights matter! + + }> + Help local communities make informed decisions about learning paths + and create a bigger impact together. + + }> + Discover what others are thinking and find exciting new tools and + solutions to level up your work. + + }> + Stay ahead of the curve with insights on hot technologies and see how + you stack up against other developers. + + - -); + ); +} -type CardProps = { +interface CardProps { title: string; icon: React.ReactNode; children: React.ReactNode; -}; +} -const Card = ({ title, icon, children }: CardProps) => ( - - - {icon} - - {title} - +function Card({ title, icon, children }: CardProps) { + return ( + + + {icon} + + {title} + + + {children} - {children} - -); + ); +} diff --git a/src/components/layout.astro b/src/components/layout.astro index 6cb9033b..df0ba8a6 100644 --- a/src/components/layout.astro +++ b/src/components/layout.astro @@ -1,9 +1,9 @@ --- -import Header from "./header.astro"; -import Footer from "./footer.astro"; import { AstroSeo } from "@astrolib/seo"; -import "../globals.css"; import GoogleAnalytics from "@/components/google-analytics.astro"; +import Footer from "./footer.astro"; +import Header from "./header.astro"; +import "../globals.css"; interface Props { title?: string; @@ -13,9 +13,9 @@ interface Props { const { title: titleProp, year, description: descriptionProp } = Astro.props; const title = titleProp || "State Of Dev In Morocco 🇲🇦"; -const description = - descriptionProp || - "Participate and let us know what working in tech really looks like in Morocco 🇲🇦"; +const description + = descriptionProp + || "Participate and let us know what working in tech really looks like in Morocco 🇲🇦"; --- @@ -27,75 +27,75 @@ const description = + } + }); +})(); + <> { - year === undefined ? ( - - ) : ( - - ) + year === undefined + ? ( + + ) + : ( + + ) } > diff --git a/src/components/playground/chart-types.ts b/src/components/playground/chart-types.ts new file mode 100644 index 00000000..53085fbd --- /dev/null +++ b/src/components/playground/chart-types.ts @@ -0,0 +1,6 @@ +export type ChartType = "bar" | "pie"; + +export const chartTypes: { label: string; value: ChartType }[] = [ + { label: "Bar Chart", value: "bar" }, + { label: "Pie Chart", value: "pie" } +]; diff --git a/src/components/playground/filters-options.tsx b/src/components/playground/filters-options.tsx index a0a6b65c..6e34d66c 100644 --- a/src/components/playground/filters-options.tsx +++ b/src/components/playground/filters-options.tsx @@ -1,62 +1,68 @@ -import React from "react"; -import { Controller, useFieldArray, type Control } from "react-hook-form"; +import type { Control } from "react-hook-form"; +import type { StylesConfig } from "react-select"; +import type { QuestionMap } from "../chart/data"; import type { PlaygroundFormData } from "./playground-form"; +import React from "react"; +import { Controller, useFieldArray } from "react-hook-form"; import Select from "react-select"; -import type { QuestionMap } from "../chart/data"; + +interface OptionType { label: string; value: string } // Custom styles for React Select to match flat design -const customSelectStyles = { - control: (base: any, state: any) => ({ +const customSelectStyles: StylesConfig = { + control: (base, state) => ({ ...base, - borderRadius: 0, - borderWidth: "2px", - borderColor: state.isFocused ? "var(--primary)" : "var(--input)", - backgroundColor: "var(--card)", - boxShadow: "none", + "borderRadius": 0, + "borderWidth": "2px", + "borderColor": state.isFocused ? "var(--primary)" : "var(--input)", + "backgroundColor": "var(--card)", + "boxShadow": "none", "&:hover": { borderColor: "var(--primary)" } }), - menu: (base: any) => ({ + menu: base => ({ ...base, borderRadius: 0, borderWidth: "2px", borderColor: "var(--border)", backgroundColor: "var(--card)" }), - option: (base: any, state: any) => ({ + option: (base, state) => ({ ...base, - borderRadius: 0, - backgroundColor: state.isSelected + "borderRadius": 0, + "backgroundColor": state.isSelected ? "var(--primary)" : state.isFocused ? "var(--muted)" : "var(--card)", - color: state.isSelected ? "var(--primary-foreground)" : "var(--foreground)", + "color": state.isSelected ? "var(--primary-foreground)" : "var(--foreground)", "&:hover": { backgroundColor: state.isSelected ? "var(--primary)" : "var(--muted)" } }) }; -type FilterOptionsProps = { +interface FilterOptionsProps { options: { label: string; value: string }[]; questions: QuestionMap; control: Control; -}; +} export const FilterOptions = React.memo( ({ questions, options, control }: FilterOptionsProps) => { - // not sure why but we need this to make sure filters in the url works as expected - if (Object.keys(questions).length === 0) { - return null; - } + // Hooks must be called unconditionally - move before early return const { fields, append, remove } = useFieldArray({ control, name: "filters" }); + // not sure why but we need this to make sure filters in the url works as expected + if (Object.keys(questions).length === 0) { + return null; + } + return ( @@ -73,11 +79,12 @@ export const FilterOptions = React.memo( q.value === value.question_id + q => q.value === value.question_id )} - onChange={(val) => - onChange({ question_id: val?.value, values: [] }) - } + onChange={(val) => { + const questionId = val && "value" in val ? String(val.value) : ""; + onChange({ question_id: questionId, values: [] }); + }} inputId="question-select" options={options} placeholder="Select a question" @@ -110,7 +117,7 @@ export const FilterOptions = React.memo( {questions[value.question_id].choices.map( (choice, choiceIndex) => ( { const [result, setResult] = useState(null); @@ -19,12 +23,12 @@ export const SurveyPlayground: React.FC = () => { const [chartType, setChartType] = useState("bar"); const handleFormChange = React.useCallback((formData: PlaygroundFormData) => { - const { questions } = getSurveyData(formData.year as Year); + const { questions } = getSurveyData(formData.year); setQuestions(questions); if (formData.question_id) { const condition: QuestionCondition = formData.filters - .filter((f) => f.question_id && f.values.length > 0) - .map((f) => ({ question_id: f.question_id, values: f.values })); + .filter(f => f.question_id && f.values.length > 0) + .map(f => ({ question_id: f.question_id, values: f.values })); const result = getQuestion({ id: formData.question_id, year: formData.year, @@ -33,7 +37,8 @@ export const SurveyPlayground: React.FC = () => { }); setResult(result); setChartType(formData.chart_type as ChartType); - } else { + } + else { setResult(null); } }, []); @@ -45,32 +50,34 @@ export const SurveyPlayground: React.FC = () => { - {result ? ( - - - - - - - ) : ( - - Select a question to generate a chart - - )} + {result + ? ( + + + + + + + ) + : ( + + Select a question to generate a chart + + )} ); }; -const ShareActions = () => { +function ShareActions() { const shareUrl = window.location.href; const shareTitle = `Check out this report`; return ; -}; +} diff --git a/src/components/playground/playground-form.tsx b/src/components/playground/playground-form.tsx index 58f8db6b..071e9a36 100644 --- a/src/components/playground/playground-form.tsx +++ b/src/components/playground/playground-form.tsx @@ -1,71 +1,68 @@ +import type { StylesConfig } from "react-select"; +import type { QuestionMap, Year } from "../chart/data"; +import queryString from "query-string"; + import React, { useEffect } from "react"; -import { useForm, Controller } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import Select from "react-select"; - -import { type Year, type QuestionMap } from "../chart/data"; +import { chartTypes } from "./chart-types"; import { FilterOptions } from "./filters-options"; -import queryString from "query-string"; const isBrowser = typeof window !== "undefined"; -export type ChartType = "bar" | "pie"; - -export const chartTypes: { label: string; value: ChartType }[] = [ - { label: "Bar Chart", value: "bar" }, - { label: "Pie Chart", value: "pie" } -]; - const years = ["2020", "2021", "2022", "2023", "2024"]; +interface OptionType { label: string; value: string } + // Custom styles for React Select to match flat design -const customSelectStyles = { - control: (base: any, state: any) => ({ +const customSelectStyles: StylesConfig = { + control: (base, state) => ({ ...base, - borderRadius: 0, - borderWidth: "2px", - borderColor: state.isFocused ? "var(--primary)" : "var(--input)", - backgroundColor: "var(--card)", - boxShadow: "none", + "borderRadius": 0, + "borderWidth": "2px", + "borderColor": state.isFocused ? "var(--primary)" : "var(--input)", + "backgroundColor": "var(--card)", + "boxShadow": "none", "&:hover": { borderColor: "var(--primary)" } }), - menu: (base: any) => ({ + menu: base => ({ ...base, borderRadius: 0, borderWidth: "2px", borderColor: "var(--border)", backgroundColor: "var(--card)" }), - option: (base: any, state: any) => ({ + option: (base, state) => ({ ...base, - borderRadius: 0, - backgroundColor: state.isSelected + "borderRadius": 0, + "backgroundColor": state.isSelected ? "var(--primary)" : state.isFocused ? "var(--muted)" : "var(--card)", - color: state.isSelected ? "var(--primary-foreground)" : "var(--foreground)", + "color": state.isSelected ? "var(--primary-foreground)" : "var(--foreground)", "&:hover": { backgroundColor: state.isSelected ? "var(--primary)" : "var(--muted)" } }) }; -type Filter = { +interface Filter { question_id: string; values: string[]; -}; +} -export type PlaygroundFormData = { +export interface PlaygroundFormData { year: Year; question_id: string; filters: Filter[]; group_by: string; chart_type: string; // Add this line -}; +} -const getDefaultValues = (): PlaygroundFormData => { +function getDefaultValues(): PlaygroundFormData { const { question_id = "profile-q-0", group_by = "", @@ -83,18 +80,19 @@ const getDefaultValues = (): PlaygroundFormData => { }; let filters: Filter[] = []; try { - filters = JSON.parse(c as unknown as string); - } catch (error) { + filters = JSON.parse(c as unknown as string) as Filter[]; + } + catch { filters = [{ question_id: "", values: [] }]; } return { question_id, filters, group_by, year, chart_type }; -}; +} -type PlaygroundFormProps = { +interface PlaygroundFormProps { questions: QuestionMap; onChange: (data: PlaygroundFormData) => void; -}; +} export const PlaygroundForm = React.memo( ({ questions, onChange }: PlaygroundFormProps) => { @@ -111,6 +109,7 @@ export const PlaygroundForm = React.memo( [questions] ); + // eslint-disable-next-line react-hooks/incompatible-library const formData = watch(); useEffect(() => { onChange(formData); @@ -118,7 +117,7 @@ export const PlaygroundForm = React.memo( useEffect(() => { if (isBrowser) { - const filters = formData.filters.filter((f) => f.values.length > 0); + const filters = formData.filters.filter(f => f.values.length > 0); const search = { year: formData.year, question_id: formData.question_id, @@ -153,7 +152,7 @@ export const PlaygroundForm = React.memo( {...field} className="w-full p-2 border-2 border-input bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" > - {years.map((year) => ( + {years.map(year => ( {year} @@ -178,7 +177,7 @@ export const PlaygroundForm = React.memo( {...field} className="w-full p-2 border-2 border-input bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring" > - {chartTypes.map((chartType) => ( + {chartTypes.map(chartType => ( {chartType.label} @@ -204,9 +203,9 @@ export const PlaygroundForm = React.memo( q.value === formData.question_id + q => q.value === formData.question_id )} - onChange={(val) => field.onChange(val?.value)} + onChange={val => field.onChange((val as OptionType | null)?.value)} inputId="question-select" options={questionOptions} placeholder="Select a question" @@ -239,9 +238,9 @@ export const PlaygroundForm = React.memo( isClearable {...field} value={questionOptions.find( - (q) => q.value === formData.group_by + q => q.value === formData.group_by )} - onChange={(val) => field.onChange(val?.value)} + onChange={val => field.onChange((val as OptionType | null)?.value)} inputId="group-by-select" options={questionOptions} placeholder="Select a question to group by" diff --git a/src/components/report/hero.astro b/src/components/report/hero.astro index be8494e4..fdfa7811 100644 --- a/src/components/report/hero.astro +++ b/src/components/report/hero.astro @@ -1,8 +1,9 @@ --- -import Crea2 from "../../assets/undraw_visual_data.svg"; -import Crea4 from "../../assets/data_image.svg"; -import { YouTube } from "@astro-community/astro-embed-youtube"; import type { Year } from "../chart/data"; +import { YouTube } from "@astro-community/astro-embed-youtube"; +import Crea4 from "../../assets/data_image.svg"; +import Crea2 from "../../assets/undraw_visual_data.svg"; + export interface Props { year: Year; } @@ -85,10 +86,12 @@ const { year } = Astro.props; > + fill="currentColor" + > + fill="currentColor" + > {data[year].year} Report 🇲🇦 diff --git a/src/components/report/index.astro b/src/components/report/index.astro index e7059786..38165821 100644 --- a/src/components/report/index.astro +++ b/src/components/report/index.astro @@ -1,15 +1,15 @@ --- -export const prerender = true; - +import type { Year } from "@/components/chart/data"; import Context from "@astro-utils/context/Context.astro"; -import Tabs from "./tabs.astro"; -import TabItem from "./tab-item.astro"; -import Toc from "./toc.astro"; +import Chart from "@/components/chart/chart.astro"; import Hero from "./hero.astro"; +import TabItem from "./tab-item.astro"; -import type { Year } from "@/components/chart/data"; +import Tabs from "./tabs.astro"; -import Chart from "@/components/chart/chart.astro"; +import Toc from "./toc.astro"; + +export const prerender = true; export interface Props { year: Year; @@ -17,13 +17,13 @@ export interface Props { const { year = "2023" } = Astro.props; const allDocs = await Astro.glob(`../../../results/**/*.mdx`); const docs = allDocs - .filter((file) => file.file.includes(`${year}/sections`)) + .filter(file => file.file.includes(`${year}/sections`)) .sort((a, b) => a.frontmatter.position - b.frontmatter.position); const headings = docs - .map((doc) => doc.getHeadings()) + .map(doc => doc.getHeadings()) .flat() - .filter((heading) => heading.depth <= 3); + .filter(heading => heading.depth <= 3); const customComponents = { Chart, TabItem, @@ -52,7 +52,7 @@ const customComponents = { return ( - {} + {index < docs.length - 1 && ( diff --git a/src/components/report/toc.astro b/src/components/report/toc.astro index 98b1dffd..f1cc4167 100644 --- a/src/components/report/toc.astro +++ b/src/components/report/toc.astro @@ -22,7 +22,7 @@ const { items } = Astro.props; class="px-6 py-6 z-20 border-l border-solid border-border hidden md:block" > { - items.map((item) => ( + items.map(item => ( 1 ? "ml-4" : ""} relative`}> + d="M4 6h16M4 10h16M4 14h16M4 18h8" + > diff --git a/src/components/social-media-card.astro b/src/components/social-media-card.astro index ef4b5c2f..18e27951 100644 --- a/src/components/social-media-card.astro +++ b/src/components/social-media-card.astro @@ -10,7 +10,6 @@ interface Props { }; } const { post }: Props = Astro.props; -console.log(post); const contentEncoded = encodeURIComponent(post.content); @@ -32,7 +31,8 @@ const shareLinks = { + clip-rule="evenodd" + > {post.user?.name ?? "Anonymous"} @@ -43,8 +43,10 @@ const shareLinks = { {post.content} - stateofdev.mastateofdev.ma @@ -121,12 +123,11 @@ const shareLinks = { aria-hidden="true" > - + Copied!Copied! diff --git a/src/components/survey/choice.tsx b/src/components/survey/choice.tsx index e8d5824c..d44d9732 100644 --- a/src/components/survey/choice.tsx +++ b/src/components/survey/choice.tsx @@ -1,6 +1,6 @@ -import { type ChangeEvent } from "react"; +import type { ChangeEvent } from "react"; -type ChoiceProps = { +interface ChoiceProps { text: string; id: string; index: number; @@ -9,9 +9,9 @@ type ChoiceProps = { multiple: boolean; checked: boolean; onChange: (index: number, checked: boolean) => void; -}; +} -export const Choice = ({ +export function Choice({ text, id, index, @@ -19,7 +19,7 @@ export const Choice = ({ multiple, checked, onChange -}: ChoiceProps) => { +}: ChoiceProps) { const handleChange = (e: ChangeEvent) => { onChange(index, e.target.checked); }; @@ -44,12 +44,14 @@ export const Choice = ({ hover:border-primary/50 transition-all duration-200" htmlFor={id} - > + > + + > + ×× diff --git a/src/components/survey/index.astro b/src/components/survey/index.astro index 34a4f939..1a25ac2a 100644 --- a/src/components/survey/index.astro +++ b/src/components/survey/index.astro @@ -1,13 +1,13 @@ --- +import { validateSurveyFile } from "@/lib/validators/survey-schema"; import profileQuestionsRaw from "@/survey/1-profile.yml"; import learningQuestionsRaw from "@/survey/2-learning-and-education.yml"; import workQuestionRaw from "@/survey/3-work.yml"; import aiQuestionRaw from "@/survey/4-ai.yml"; import techQuestionRaw from "@/survey/5-tech.yml"; import communityQuestionRaw from "@/survey/6-community.yml"; -import { SurveyApp } from "./survey-app"; import ExitPopup from "./exit-popup.astro"; -import { validateSurveyFile } from "@/lib/validators/survey-schema"; +import { SurveyApp } from "./survey-app"; // Validate each YAML file to ensure data integrity const profileQuestions = validateSurveyFile( diff --git a/src/components/survey/question.tsx b/src/components/survey/question.tsx index f71687cc..16813578 100644 --- a/src/components/survey/question.tsx +++ b/src/components/survey/question.tsx @@ -1,9 +1,8 @@ +import type { ChangeEvent } from "react"; +import type { SurveyQuestion } from "@/lib/validators/survey-schema"; import { useCallback, - useEffect, - useMemo, - useState, - type ChangeEvent + useMemo } from "react"; import { Choice } from "./choice"; @@ -11,7 +10,7 @@ const GRID_LAYOUT_THRESHOLD = 10; type AnswerValue = number | number[] | null; -type QuestionProps = { +interface QuestionProps { question: SurveyQuestion; index: number; sectionId: string; @@ -20,9 +19,9 @@ type QuestionProps = { othersValue: string | undefined; onAnswerChange: (value: AnswerValue) => void; onOthersChange: (value: string) => void; -}; +} -export const Question = ({ +export function Question({ question, index, sectionId, @@ -31,10 +30,9 @@ export const Question = ({ othersValue, onAnswerChange, onOthersChange -}: QuestionProps) => { +}: QuestionProps) { const { label, choices } = question; const fitContent = choices.length > GRID_LAYOUT_THRESHOLD; - const [showOtherInput, setShowOtherInput] = useState(false); // Check if "other" options exist const othersIndices = useMemo( @@ -46,32 +44,20 @@ export const Question = ({ [choices] ); - // Update showOtherInput based on current value - const updateShowOtherInput = useCallback( - (currentValue: AnswerValue | undefined) => { - if (othersIndices.length === 0) { - setShowOtherInput(false); - return; - } + // Derive showOtherInput from value instead of using useEffect + const showOtherInput = useMemo(() => { + if (othersIndices.length === 0) { + return false; + } - const valuesArray = Array.isArray(currentValue) - ? currentValue - : currentValue !== null && currentValue !== undefined - ? [currentValue as number] - : []; + const valuesArray = Array.isArray(value) + ? value + : value !== null && value !== undefined + ? [value] + : []; - const hasOtherSelected = othersIndices.some((idx) => - valuesArray.includes(idx) - ); - setShowOtherInput(hasOtherSelected); - }, - [othersIndices] - ); - - // Update showOtherInput when value changes - useEffect(() => { - updateShowOtherInput(value); - }, [value, updateShowOtherInput]); + return othersIndices.some(idx => valuesArray.includes(idx)); + }, [value, othersIndices]); const handleChoiceChange = useCallback( (choiceIndex: number, checked: boolean) => { @@ -80,16 +66,15 @@ export const Question = ({ const currentArray = Array.isArray(value) ? value : []; const newValue = checked ? [...currentArray, choiceIndex] - : currentArray.filter((v) => v !== choiceIndex); + : currentArray.filter(v => v !== choiceIndex); onAnswerChange(newValue); - updateShowOtherInput(newValue); - } else { + } + else { // Single choice: set value onAnswerChange(choiceIndex); - updateShowOtherInput(choiceIndex); } }, - [question.multiple, value, onAnswerChange, updateShowOtherInput] + [question.multiple, value, onAnswerChange] ); const handleOthersChange = useCallback( @@ -122,7 +107,9 @@ export const Question = ({ > - {`${index + 1}. ${label}`} + {`${index + 1}. ${label}`} + {" "} + @@ -140,7 +127,7 @@ export const Question = ({ > {choices.map((c, i) => ( ); -}; +} diff --git a/src/components/survey/steps.tsx b/src/components/survey/steps.tsx index 9fe51996..bb4b62b6 100644 --- a/src/components/survey/steps.tsx +++ b/src/components/survey/steps.tsx @@ -1,20 +1,23 @@ -type StepProps = { +import { useMemo } from "react"; +import { SurveyMachineContext } from "./survey-context"; + +interface StepProps { label: string; selectedIndex: number; index: number; totalSections: number; onClick?: (index: number) => void; -}; +} -const Step = ({ +function Step({ label, selectedIndex, index, totalSections, onClick -}: StepProps) => { - const color = - index > selectedIndex ? "text-muted-foreground" : "text-primary"; +}: StepProps) { + const color + = index > selectedIndex ? "text-muted-foreground" : "text-primary"; const isCompleted = index < selectedIndex; const isClickable = isCompleted && onClick; @@ -43,30 +46,32 @@ const Step = ({ role={isClickable ? "button" : undefined} aria-label={isClickable ? `Go to ${label} section` : undefined} > - {index + 1 > selectedIndex ? ( - - {index + 1} - - ) : ( - - - - - - )} + {index + 1 > selectedIndex + ? ( + + {index + 1} + + ) + : ( + + + + + + )} - {index < totalSections - 1 && - (index + 1 > selectedIndex ? ( - - ) : ( - - ))} + {index < totalSections - 1 + && (index + 1 > selectedIndex + ? ( + + + ) + : ( + + ))} > ); -}; +} -type StepsProps = { - selectedIndex: number; - sections: string[]; - onStepClick?: (index: number) => void; -}; +export function Steps() { + const actorRef = SurveyMachineContext.useActorRef(); + const context = SurveyMachineContext.useSelector(state => state.context); + const visibleSections = useMemo( + () => + context.visibleSectionIndices.map(idx => ({ + label: context.sections[idx].label, + originalIdx: idx + })), + [context.sections, context.visibleSectionIndices] + ); + + const sectionsLabels = useMemo( + () => visibleSections.map(s => s.label), + [visibleSections] + ); + + const currentVisibleSectionIdx = useMemo( + () => context.visibleSectionIndices.indexOf(context.currentSectionIdx), + [context.visibleSectionIndices, context.currentSectionIdx] + ); + + const handleStepClick = (visibleIdx: number) => { + const originalIdx = visibleSections[visibleIdx]?.originalIdx; + if (originalIdx != null) { + actorRef.send({ type: "GO_TO_SECTION", sectionIdx: originalIdx }); + } + }; -export const Steps = ({ - selectedIndex = 0, - sections, - onStepClick -}: StepsProps) => { return ( - {sections.map((section, index) => { + {sectionsLabels.map((section, index) => { return ( ); })} ); -}; +} diff --git a/src/components/survey/survey-app.tsx b/src/components/survey/survey-app.tsx index 2f9a8ffb..9a04dd96 100644 --- a/src/components/survey/survey-app.tsx +++ b/src/components/survey/survey-app.tsx @@ -1,14 +1,15 @@ +import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; import { SurveyProvider } from "./survey-context"; import { SurveyForm } from "./survey-form"; -type Props = { +interface Props { questions: SurveyQuestionsYamlFile[]; -}; +} -export const SurveyApp = ({ questions }: Props) => { +export function SurveyApp({ questions }: Props) { return ( ); -}; +} diff --git a/src/components/survey/survey-context.tsx b/src/components/survey/survey-context.tsx index 4f190893..1faf8f63 100644 --- a/src/components/survey/survey-context.tsx +++ b/src/components/survey/survey-context.tsx @@ -1,24 +1,28 @@ +import type { ReactNode } from "react"; +import type { SurveyContext } from "./survey-machine"; +import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; import { createActorContext } from "@xstate/react"; -import { useEffect, type ReactNode } from "react"; -import { surveyMachine, type SurveyContext } from "./survey-machine"; +import { useEffect } from "react"; import { createSurveyInspector } from "./survey-inspector"; +import { surveyMachine } from "./survey-machine"; const STORAGE_KEY = "survey-state"; const STORAGE_VERSION = 1; -type PersistedState = { +interface PersistedState { version: number; currentSectionIdx: number; currentQuestionIdx: number; answers: Record; -}; +} -const loadPersistedState = (): Partial | null => { +function loadPersistedState(): Partial | null { try { const stored = localStorage.getItem(STORAGE_KEY); - if (!stored) return null; + if (!stored) + return null; - const parsed: PersistedState = JSON.parse(stored); + const parsed = JSON.parse(stored) as PersistedState; // Version check - clear if outdated if (parsed.version !== STORAGE_VERSION) { @@ -31,14 +35,15 @@ const loadPersistedState = (): Partial | null => { currentQuestionIdx: parsed.currentQuestionIdx, answers: parsed.answers }; - } catch (error) { + } + catch (error) { console.error("Failed to load persisted survey state:", error); localStorage.removeItem(STORAGE_KEY); return null; } -}; +} -const persistState = (context: SurveyContext) => { +function persistState(context: SurveyContext) { try { const toPersist: PersistedState = { version: STORAGE_VERSION, @@ -47,28 +52,30 @@ const persistState = (context: SurveyContext) => { answers: context.answers }; localStorage.setItem(STORAGE_KEY, JSON.stringify(toPersist)); - } catch (error) { + } + catch (error) { console.error("Failed to persist survey state:", error); } -}; +} -export const clearPersistedState = () => { +function clearPersistedState() { try { localStorage.removeItem(STORAGE_KEY); - } catch (error) { + } + catch (error) { console.error("Failed to clear persisted survey state:", error); } -}; +} // Create the actor context export const SurveyMachineContext = createActorContext(surveyMachine); -type SurveyProviderProps = { +interface SurveyProviderProps { sections: SurveyQuestionsYamlFile[]; children: ReactNode; -}; +} -export const SurveyProvider = ({ sections, children }: SurveyProviderProps) => { +export function SurveyProvider({ sections, children }: SurveyProviderProps) { const persisted = loadPersistedState(); return ( @@ -78,27 +85,31 @@ export const SurveyProvider = ({ sections, children }: SurveyProviderProps) => { sections, persisted: persisted || undefined }, - inspect: import.meta.env.DEV ? createSurveyInspector() : undefined + inspect: + import.meta.env.DEV && import.meta.env.MODE !== "test" + ? createSurveyInspector() + : undefined }} > {children} ); -}; +} // Internal component to handle persistence side effects -const PersistenceHandler = () => { - const context = SurveyMachineContext.useSelector((state) => state.context); - const value = SurveyMachineContext.useSelector((state) => state.value); +function PersistenceHandler() { + const context = SurveyMachineContext.useSelector(state => state.context); + const value = SurveyMachineContext.useSelector(state => state.value); useEffect(() => { if (value === "complete") { clearPersistedState(); - } else { + } + else { persistState(context); } }, [context, value]); return null; -}; +} diff --git a/src/components/survey/survey-controls.tsx b/src/components/survey/survey-controls.tsx index bf44dd84..2146f7b5 100644 --- a/src/components/survey/survey-controls.tsx +++ b/src/components/survey/survey-controls.tsx @@ -1,10 +1,14 @@ -type ErrorMessageProps = { +import { hasPrevVisibleQuestion } from "@/lib/conditions"; +import { SurveyMachineContext } from "./survey-context"; + +interface ErrorMessageProps { error: string | null; onClose: () => void; -}; +} -export const ErrorMessage = ({ error, onClose }: ErrorMessageProps) => { - if (!error) return null; +export function ErrorMessage({ error, onClose }: ErrorMessageProps) { + if (!error) + return null; return ( { {error} { ); -}; +} -type BackButtonProps = { +interface BackButtonProps { onClick: () => void; -}; +} -export const BackButton = ({ onClick }: BackButtonProps) => ( - - - + + + + Back + + + ); +} + +export function SurveyActions() { + const actorRef = SurveyMachineContext.useActorRef(); + + const context = SurveyMachineContext.useSelector(state => state.context); + const isSubmitting = SurveyMachineContext.useSelector(state => + state.matches("submitting") + ); + + const currentSection = context.sections[context.currentSectionIdx]; + const currentQuestion = currentSection?.questions[context.currentQuestionIdx]; + + const isRequired = !!currentQuestion?.required; + const canGoBack + = context.visibleSectionIndices.indexOf(context.currentSectionIdx) > 0 + || hasPrevVisibleQuestion( + currentSection.questions, + context.currentQuestionIdx, + context.answers + ); + + const questionId = `${currentSection.label}-q-${context.currentQuestionIdx}`; + + const handleNext = () => { + const currentAnswer = context.answers[questionId]; + if (currentAnswer == null && currentQuestion.multiple) { + actorRef.send({ + type: "ANSWER_CHANGE", + questionId, + value: [] + }); + } + actorRef.send({ type: "NEXT" }); + }; + + const handleSkip = () => { + const value = currentQuestion.multiple ? [] : null; + actorRef.send({ + type: "ANSWER_CHANGE", + questionId, + value + }); + actorRef.send({ type: "SKIP" }); + }; + + const handleBack = () => { + actorRef.send({ type: "BACK" }); + }; + + return ( + + actorRef.send({ type: "CLEAR_ERROR" })} /> - - - Back - - -); + {canGoBack && } + + {!isRequired && ( + + Skip + + )} + + {isSubmitting ? "Loading..." : "Next"} + + + + ); +} diff --git a/src/components/survey/survey-form.test.tsx b/src/components/survey/survey-form.test.tsx index fb4bee5a..d27da0e0 100644 --- a/src/components/survey/survey-form.test.tsx +++ b/src/components/survey/survey-form.test.tsx @@ -1,10 +1,11 @@ -import { render, screen, waitFor, cleanup } from "@testing-library/react"; +import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { SurveyForm } from "./survey-form"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { SurveyProvider } from "./survey-context"; -import * as utils from "./utils"; +import { SurveyForm } from "./survey-form"; import { ERRORS } from "./survey-machine"; +import * as utils from "./utils"; // Mock utils module to prevent jsdom navigation errors vi.mock("./utils", async () => { @@ -12,7 +13,7 @@ vi.mock("./utils", async () => { return { ...actual, goToThanksPage: vi.fn(), - submitAnswers: vi.fn(() => + submitAnswers: vi.fn(async () => Promise.resolve({ data: undefined, error: undefined }) ) }; @@ -42,7 +43,7 @@ function setup(questions: SurveyQuestionsYamlFile[]) { const submitAnswersSpy = vi.mocked(utils.submitAnswers); const goToThanksPageSpy = vi.mocked(utils.goToThanksPage); -const longText = Array(300).fill("a").join(""); +const longText = Array.from({ length: 300 }).fill("a").join(""); // Mock questions based on the SurveyQuestionsYamlFile type const mockQuestions: SurveyQuestionsYamlFile[] = [ @@ -104,8 +105,8 @@ beforeEach(() => { cleanup(); }); -describe("SurveyForm", () => { - describe("Rendering", () => { +describe("surveyForm", () => { + describe("rendering", () => { it("renders the first section of questions correctly", () => { setup(mockQuestions); expect(screen.getByText(/Question 1.1/i)).toBeInTheDocument(); @@ -116,7 +117,7 @@ describe("SurveyForm", () => { }); }); - describe("Navigation", () => { + describe("navigation", () => { it("should navigate to the next question without error", async () => { const { user } = setup(mockQuestions); expect(screen.getByTestId("profile-q-0")).toBeInTheDocument(); @@ -150,7 +151,7 @@ describe("SurveyForm", () => { }); }); - describe("Error Handling", () => { + describe("error Handling", () => { it("should show error message when required question is not answered", async () => { const { user } = setup(mockQuestions); @@ -179,7 +180,7 @@ describe("SurveyForm", () => { vi.useRealTimers(); }); - it("Show error message when submitAnswers fails", async () => { + it("show error message when submitAnswers fails", async () => { const { user } = setup(mockQuestions); // Mock submitAnswers to return an error submitAnswersSpy.mockResolvedValue({ @@ -211,8 +212,8 @@ describe("SurveyForm", () => { }); }); - describe("Answer Submission", () => { - it("In the last question, it should call submitAnswers with correct answers", async () => { + describe("answer Submission", () => { + it("in the last question, it should call submitAnswers with correct answers", async () => { // mock submitAnswers to return a successful response submitAnswersSpy.mockResolvedValue({ data: undefined, @@ -268,7 +269,7 @@ describe("SurveyForm", () => { }); }); - it("Should redirect to thanks page when all questions are answered", async () => { + it("should redirect to thanks page when all questions are answered", async () => { const { user } = setup(mockQuestions); await user.click(screen.getByTestId("profile-q-0-0")); await user.click(screen.getByTestId("next-button")); @@ -299,7 +300,7 @@ describe("SurveyForm", () => { }); }); - describe("Other Input Handling", () => { + describe("other Input Handling", () => { it("should show the text area when the user selects the 'other' option for a single choice question", async () => { const { user } = setup(mockQuestions); @@ -327,7 +328,7 @@ describe("SurveyForm", () => { expect(screen.getByTestId("profile-q-2-others")).toBeInTheDocument(); }); // click multiple choice again - user.click(screen.getByTestId("profile-q-2-4")); + await user.click(screen.getByTestId("profile-q-2-4")); await waitFor(() => { expect(screen.getByTestId("profile-q-2-others")).toBeInTheDocument(); }); @@ -377,8 +378,8 @@ describe("SurveyForm", () => { }); }); - describe("Multiple Choice Handling", () => { - it("Allow multiple answers for questions with multiple: true", async () => { + describe("multiple Choice Handling", () => { + it("allow multiple answers for questions with multiple: true", async () => { const { user } = setup(mockQuestions); expect(screen.queryByTestId("back-button")).not.toBeInTheDocument(); await user.click(screen.getByTestId("profile-q-0-0")); // select the first option of the first question @@ -397,7 +398,7 @@ describe("SurveyForm", () => { }); }); - it("Toggling selection of an option for questions should work as expected with multiple: true", async () => { + it("toggling selection of an option for questions should work as expected with multiple: true", async () => { const { user } = setup(mockQuestions); expect(screen.queryByTestId("back-button")).not.toBeInTheDocument(); await user.click(screen.getByTestId("profile-q-0-0")); // select the first option of the first question @@ -437,7 +438,7 @@ describe("SurveyForm", () => { }); }); - describe("Skipping Questions", () => { + describe("skipping Questions", () => { it("on skip question, the selected answer should be null", async () => { const { user } = setup(mockQuestions); await user.click(screen.getByTestId("profile-q-0-0")); diff --git a/src/components/survey/survey-form.tsx b/src/components/survey/survey-form.tsx index d799ee32..77720e4b 100644 --- a/src/components/survey/survey-form.tsx +++ b/src/components/survey/survey-form.tsx @@ -1,33 +1,20 @@ -import { useMemo } from "react"; -import { Steps } from "./steps"; import { Question } from "./question"; +import { Steps } from "./steps"; import { SurveyMachineContext } from "./survey-context"; -import { ErrorMessage, BackButton } from "./survey-controls"; +import { SurveyActions } from "./survey-controls"; const QUESTION_CONTAINER_MIN_HEIGHT = "300px"; -export const SurveyForm = () => { +export function SurveyForm() { const actorRef = SurveyMachineContext.useActorRef(); // Select all needed state - const context = SurveyMachineContext.useSelector((state) => state.context); - const isSubmitting = SurveyMachineContext.useSelector((state) => - state.matches("submitting") - ); + const context = SurveyMachineContext.useSelector(state => state.context); // Compute derived state const currentSection = context.sections[context.currentSectionIdx]; const currentQuestion = currentSection?.questions[context.currentQuestionIdx]; - const sectionsLabels = useMemo( - () => context.sections.map((q) => q.label), - [context.sections] - ); - - const isRequired = !!currentQuestion?.required; - const canGoBack = - context.currentSectionIdx > 0 || context.currentQuestionIdx > 0; - const questionId = `${currentSection.label}-q-${context.currentQuestionIdx}`; const handleAnswerChange = (value: number | number[] | null | string) => { @@ -46,49 +33,13 @@ export const SurveyForm = () => { }); }; - const handleNext = () => { - // If no answer selected for multiple choice, set to empty array - const currentAnswer = context.answers[questionId]; - if (currentAnswer === undefined && currentQuestion.multiple) { - actorRef.send({ - type: "ANSWER_CHANGE", - questionId, - value: [] - }); - } - actorRef.send({ type: "NEXT" }); - }; - - const handleSkip = () => { - // Set answer to null for single choice or [] for multiple choice when skipping - const value = currentQuestion.multiple ? [] : null; - actorRef.send({ - type: "ANSWER_CHANGE", - questionId, - value - }); - actorRef.send({ type: "SKIP" }); - }; - - const handleBack = () => { - actorRef.send({ type: "BACK" }); - }; - - const handleSectionClick = (sectionIdx: number) => { - actorRef.send({ type: "GO_TO_SECTION", sectionIdx }); - }; - if (!currentSection || !currentQuestion) { return null; } return ( - + { sectionId={currentSection.label} value={ context.answers[questionId] as - | number - | number[] - | null - | undefined + | number + | number[] + | null + | undefined } othersValue={ context.answers[`${questionId}-others`] as string | undefined @@ -121,36 +72,9 @@ export const SurveyForm = () => { /> - - actorRef.send({ type: "CLEAR_ERROR" })} - /> - {canGoBack && } - - {!isRequired && ( - - Skip - - )} - - {isSubmitting ? "Loading..." : "Next"} - - - + ); -}; +} diff --git a/src/components/survey/survey-inspector.ts b/src/components/survey/survey-inspector.ts index d76a8b0e..2e4c23ab 100644 --- a/src/components/survey/survey-inspector.ts +++ b/src/components/survey/survey-inspector.ts @@ -1,6 +1,7 @@ +/* eslint-disable no-console */ import type { InspectionEvent } from "xstate"; -export const createSurveyInspector = () => { +export function createSurveyInspector() { return (inspectionEvent: InspectionEvent) => { const timestamp = new Date().toISOString().split("T")[1].split(".")[0]; @@ -23,7 +24,9 @@ export const createSurveyInspector = () => { console.group(`🔄 [${timestamp}] State Transition`); console.log("Triggered by:", event); + // eslint-disable-next-line ts/no-unsafe-member-access console.log("State:", (snapshot as any).value); + // eslint-disable-next-line ts/no-unsafe-member-access console.log("Context:", (snapshot as any).context); console.groupEnd(); } @@ -32,9 +35,10 @@ export const createSurveyInspector = () => { const { actorRef } = inspectionEvent; console.log( `🎭 [${timestamp}] Actor:`, + // eslint-disable-next-line ts/no-unsafe-member-access (actorRef as any).id, inspectionEvent ); } }; -}; +} diff --git a/src/components/survey/survey-machine.test.ts b/src/components/survey/survey-machine.test.ts index 6396b1e2..6cbaa2b6 100644 --- a/src/components/survey/survey-machine.test.ts +++ b/src/components/survey/survey-machine.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createActor } from "xstate"; -import { surveyMachine, ERRORS } from "./survey-machine"; +import { ERRORS, surveyMachine } from "./survey-machine"; import * as utils from "./utils"; // Mock submitAnswers @@ -88,7 +89,7 @@ const mockSections: SurveyQuestionsYamlFile[] = [ } ]; -describe("Survey State Machine", () => { +describe("survey State Machine", () => { beforeEach(() => { vi.clearAllMocks(); localStorageMock.clear(); @@ -99,7 +100,7 @@ describe("Survey State Machine", () => { vi.useRealTimers(); }); - describe("Initial State & Context", () => { + describe("initial State & Context", () => { it("should start in answering state with initial context", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -113,7 +114,8 @@ describe("Survey State Machine", () => { currentSectionIdx: 0, currentQuestionIdx: 0, answers: {}, - error: null + error: null, + visibleSectionIndices: [0, 1] }); }); @@ -139,7 +141,7 @@ describe("Survey State Machine", () => { }); }); - describe("Answer Updates (ANSWER_CHANGE)", () => { + describe("answer Updates (ANSWER_CHANGE)", () => { it("should update context with single choice answer", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -211,7 +213,7 @@ describe("Survey State Machine", () => { }); }); - describe("Forward Navigation (NEXT)", () => { + describe("forward Navigation (NEXT)", () => { it("should show error when required question is empty", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -300,7 +302,7 @@ describe("Survey State Machine", () => { }); }); - describe("Skip Navigation (SKIP)", () => { + describe("skip Navigation (SKIP)", () => { it("should skip non-required question without validation", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -356,7 +358,7 @@ describe("Survey State Machine", () => { }); }); - describe("Backward Navigation (BACK)", () => { + describe("backward Navigation (BACK)", () => { it("should decrement question within section", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -428,7 +430,7 @@ describe("Survey State Machine", () => { }); }); - describe("Section Submission", () => { + describe("section Submission", () => { it("should transition to next section on successful submission", async () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -458,8 +460,8 @@ describe("Survey State Machine", () => { () => { const snapshot = actor.getSnapshot(); return ( - snapshot.context.currentSectionIdx === 1 && - snapshot.value === "answering" + snapshot.context.currentSectionIdx === 1 + && snapshot.value === "answering" ); }, { timeout: 1000 } @@ -499,8 +501,8 @@ describe("Survey State Machine", () => { await vi.waitFor(() => { const snapshot = actor.getSnapshot(); return ( - snapshot.context.currentSectionIdx === 1 && - snapshot.value === "answering" + snapshot.context.currentSectionIdx === 1 + && snapshot.value === "answering" ); }); @@ -563,8 +565,8 @@ describe("Survey State Machine", () => { () => { const snapshot = actor.getSnapshot(); return ( - snapshot.context.error === ERRORS.submission && - snapshot.value === "answering" + snapshot.context.error === ERRORS.submission + && snapshot.value === "answering" ); }, { timeout: 1000 } @@ -618,7 +620,7 @@ describe("Survey State Machine", () => { "profile-q-0": 1, "profile-q-1": [0, 3], "profile-q-1-others": "Custom language" - }) + }) as Record }); }); @@ -659,7 +661,7 @@ describe("Survey State Machine", () => { }); }); - describe("Error Auto-Clear", () => { + describe("error Auto-Clear", () => { it("should auto-clear required error after 3000ms", async () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -716,7 +718,7 @@ describe("Survey State Machine", () => { }); }); - describe("Direct Navigation", () => { + describe("direct Navigation", () => { it("should jump to specific section with GO_TO_SECTION", () => { const actor = createActor(surveyMachine, { input: { sections: mockSections } @@ -746,7 +748,7 @@ describe("Survey State Machine", () => { }); }); - describe("Edge Cases", () => { + describe("edge Cases", () => { it("should handle single question section", () => { const singleQuestionSections: SurveyQuestionsYamlFile[] = [ { @@ -790,5 +792,365 @@ describe("Survey State Machine", () => { const snapshot = actor.getSnapshot(); expect(snapshot.context.answers["profile-q-2"]).toBe(null); }); + + it("should handle empty array for required multiple choice", () => { + const actor = createActor(surveyMachine, { + input: { sections: mockSections } + }); + actor.start(); + + // Answer first question to move to second (multiple choice) + actor.send({ + type: "ANSWER_CHANGE", + questionId: "profile-q-0", + value: 0 + }); + actor.send({ type: "NEXT" }); + + // Set empty array and try to go next (q-1 is not required, so move to q-2) + actor.send({ + type: "ANSWER_CHANGE", + questionId: "profile-q-1", + value: [] + }); + actor.send({ type: "NEXT" }); + + // Should move forward since q-1 is not required + expect(actor.getSnapshot().context.currentQuestionIdx).toBe(2); + }); + + it("should normalize boolean answers to empty array", async () => { + const actor = createActor(surveyMachine, { + input: { sections: mockSections } + }); + actor.start(); + + const submitSpy = vi.spyOn(utils, "submitAnswers").mockResolvedValue({ + data: undefined, + error: undefined + }); + + // Answer with boolean (simulating skipped multiple choice) + actor.send({ + type: "ANSWER_CHANGE", + questionId: "profile-q-0", + value: 0 + }); + actor.send({ type: "NEXT" }); + + actor.send({ + type: "ANSWER_CHANGE", + questionId: "profile-q-1", + value: [] + }); + actor.send({ type: "NEXT" }); + actor.send({ type: "NEXT" }); + + await vi.waitFor(() => { + return submitSpy.mock.calls.length > 0; + }); + + const call = submitSpy.mock.calls[0][0]; + expect(call.answers["profile-q-1"]).toEqual([]); + }); + }); + + describe("cLEAR_ERROR Event", () => { + it("should manually clear error with CLEAR_ERROR event", () => { + const actor = createActor(surveyMachine, { + input: { sections: mockSections } + }); + actor.start(); + + // Trigger error + actor.send({ type: "NEXT" }); + expect(actor.getSnapshot().context.error).toBe(ERRORS.required); + + // Manually clear + actor.send({ type: "CLEAR_ERROR" }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.error).toBe(null); + }); + }); + + describe("conditional Visibility (showIf)", () => { + const conditionalSections: SurveyQuestionsYamlFile[] = [ + { + title: "Basic Info", + label: "basic", + position: 1, + questions: [ + { + label: "Are you employed?", + choices: ["Yes", "No"], + multiple: false, + required: true + }, + { + label: "What is your job title?", + choices: ["Developer", "Designer", "Manager", "Other"], + multiple: false, + required: false, + showIf: { question: "basic-q-0", equals: 0 } + }, + { + label: "Are you looking for work?", + choices: ["Yes", "No"], + multiple: false, + required: false, + showIf: { question: "basic-q-0", equals: 1 } + } + ] + }, + { + title: "Employment Details", + label: "employment", + position: 2, + showIf: { question: "basic-q-0", equals: 0 }, + questions: [ + { + label: "Years of experience?", + choices: ["0-2", "3-5", "6+"], + multiple: false, + required: true + } + ] + } + ]; + + it("should skip hidden questions when navigating forward", () => { + const actor = createActor(surveyMachine, { + input: { sections: conditionalSections } + }); + actor.start(); + + // Answer "No" to employment - should skip q-1 and show q-2 + actor.send({ + type: "ANSWER_CHANGE", + questionId: "basic-q-0", + value: 1 + }); + actor.send({ type: "NEXT" }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.currentQuestionIdx).toBe(2); // Skipped q-1 + }); + + it("should skip hidden questions when navigating backward", () => { + const actor = createActor(surveyMachine, { + input: { sections: conditionalSections } + }); + actor.start(); + + // Answer "No" to employment + actor.send({ + type: "ANSWER_CHANGE", + questionId: "basic-q-0", + value: 1 + }); + actor.send({ type: "NEXT" }); + + // Now on q-2, go back should skip q-1 + actor.send({ type: "BACK" }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.currentQuestionIdx).toBe(0); // Back to q-0 + }); + + it("should update visibleSectionIndices when answer changes", () => { + const actor = createActor(surveyMachine, { + input: { sections: conditionalSections } + }); + actor.start(); + + // Initially employment section should be hidden + expect(actor.getSnapshot().context.visibleSectionIndices).toEqual([0]); + + // Answer "Yes" to employment - employment section becomes visible + actor.send({ + type: "ANSWER_CHANGE", + questionId: "basic-q-0", + value: 0 + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.visibleSectionIndices).toEqual([0, 1]); + }); + + it("should auto-submit when all remaining questions are hidden", async () => { + const sectionsWithHiddenEnd: SurveyQuestionsYamlFile[] = [ + { + title: "Survey", + label: "survey", + position: 1, + questions: [ + { + label: "Do you code?", + choices: ["Yes", "No"], + multiple: false, + required: true + }, + { + label: "What languages?", + choices: ["JS", "Python"], + multiple: true, + required: false, + showIf: { question: "survey-q-0", equals: 0 } + }, + { + label: "Why not?", + choices: ["No interest", "No time"], + multiple: false, + required: false, + showIf: { question: "survey-q-0", equals: 1 } + } + ] + } + ]; + + const actor = createActor(surveyMachine, { + input: { sections: sectionsWithHiddenEnd } + }); + actor.start(); + + vi.spyOn(utils, "submitAnswers").mockResolvedValue({ + data: undefined, + error: undefined + }); + + // Answer "Yes" - this hides q-2, leaving q-1 as only remaining question + actor.send({ + type: "ANSWER_CHANGE", + questionId: "survey-q-0", + value: 0 + }); + actor.send({ type: "NEXT" }); + + // Now on q-1. Answer it. + actor.send({ + type: "ANSWER_CHANGE", + questionId: "survey-q-1", + value: [0] + }); + + // NEXT should trigger submit since q-2 is hidden (no more visible questions) + actor.send({ type: "NEXT" }); + + await vi.waitFor(() => { + const snapshot = actor.getSnapshot(); + return snapshot.value === "submitting" || snapshot.value === "complete"; + }); + + expect(utils.submitAnswers).toHaveBeenCalled(); + }); + + it("should skip section if all questions are hidden", async () => { + const sectionsWithHiddenSection: SurveyQuestionsYamlFile[] = [ + { + title: "Profile", + label: "profile", + position: 1, + questions: [ + { + label: "Are you a developer?", + choices: ["Yes", "No"], + multiple: false, + required: true + } + ] + }, + { + title: "Developer Questions", + label: "dev", + position: 2, + questions: [ + { + label: "What's your role?", + choices: ["Frontend", "Backend"], + multiple: false, + required: false, + showIf: { question: "profile-q-0", equals: 0 } + }, + { + label: "Years of experience?", + choices: ["0-2", "3+"], + multiple: false, + required: false, + showIf: { question: "profile-q-0", equals: 0 } + } + ] + }, + { + title: "Final", + label: "final", + position: 3, + questions: [ + { + label: "Any feedback?", + choices: ["Yes", "No"], + multiple: false, + required: false + } + ] + } + ]; + + const actor = createActor(surveyMachine, { + input: { sections: sectionsWithHiddenSection } + }); + actor.start(); + + vi.spyOn(utils, "submitAnswers").mockResolvedValue({ + data: undefined, + error: undefined + }); + + // Answer "No" - this hides all questions in dev section + actor.send({ + type: "ANSWER_CHANGE", + questionId: "profile-q-0", + value: 1 + }); + actor.send({ type: "NEXT" }); + + // Should submit first section + await vi.waitFor(() => { + return actor.getSnapshot().value === "submitting"; + }); + + await vi.runAllTimersAsync(); + + // Should skip dev section and go to final section + await vi.waitFor(() => { + const snapshot = actor.getSnapshot(); + return ( + snapshot.value === "answering" + && snapshot.context.currentSectionIdx === 2 + ); + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.currentSectionIdx).toBe(2); // Skipped section 1 + expect(snapshot.context.currentQuestionIdx).toBe(0); + }); + + it("should handle section showIf condition hiding entire section", () => { + const actor = createActor(surveyMachine, { + input: { sections: conditionalSections } + }); + actor.start(); + + // Answer "No" - employment section should be hidden + actor.send({ + type: "ANSWER_CHANGE", + questionId: "basic-q-0", + value: 1 + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.visibleSectionIndices).toEqual([0]); + expect(snapshot.context.visibleSectionIndices).not.toContain(1); + }); }); }); diff --git a/src/components/survey/survey-machine.ts b/src/components/survey/survey-machine.ts index ebd78aa6..403f1792 100644 --- a/src/components/survey/survey-machine.ts +++ b/src/components/survey/survey-machine.ts @@ -1,28 +1,39 @@ -import { setup, assign, fromPromise } from "xstate"; -import { submitAnswers, goToThanksPage } from "./utils"; +import type { SurveyQuestionsYamlFile } from "@/lib/validators/survey-schema"; +import { assign, fromPromise, setup } from "xstate"; +import { + getFirstVisibleQuestionIndex, + getLastVisibleQuestionIndex, + getNextVisibleQuestionIndex, + getPrevVisibleQuestionIndex, + getVisibleSectionIndices, + hasNextVisibleQuestion, + hasPrevVisibleQuestion +} from "@/lib/conditions"; +import { goToThanksPage, submitAnswers } from "./utils"; const ERROR_TIMEOUT_MS = 3000; -export type SurveyContext = { +export interface SurveyContext { sections: SurveyQuestionsYamlFile[]; currentSectionIdx: number; currentQuestionIdx: number; answers: Record; error: string | null; -}; + visibleSectionIndices: number[]; +} -export type SurveyEvents = - | { type: "NEXT" } - | { type: "BACK" } - | { type: "SKIP" } - | { +export type SurveyEvents + = | { type: "NEXT" } + | { type: "BACK" } + | { type: "SKIP" } + | { type: "ANSWER_CHANGE"; questionId: string; value: number | number[] | null | string; } - | { type: "GO_TO_SECTION"; sectionIdx: number } - | { type: "GO_TO_QUESTION"; sectionIdx: number; questionIdx: number } - | { type: "CLEAR_ERROR" }; + | { type: "GO_TO_SECTION"; sectionIdx: number } + | { type: "GO_TO_QUESTION"; sectionIdx: number; questionIdx: number } + | { type: "CLEAR_ERROR" }; export const ERRORS = { none: null, @@ -31,37 +42,39 @@ export const ERRORS = { } as const; // Normalize answers for backend submission -const normalizeAnswers = ( - answers: Record -) => { - const convertedAnswers: Record = - {}; +function normalizeAnswers(answers: Record) { + const convertedAnswers: Record + = {}; for (const [key, value] of Object.entries(answers)) { if (key.endsWith("others")) { - convertedAnswers[key] = - typeof value === "string" ? value.slice(0, 200) : ""; - } else if (value === null) { + convertedAnswers[key] + = typeof value === "string" ? value.slice(0, 200) : ""; + } + else if (value === null) { convertedAnswers[key] = null; - } else if (Array.isArray(value)) { + } + else if (Array.isArray(value)) { convertedAnswers[key] = value; - } else if (typeof value === "boolean") { + } + else if (typeof value === "boolean") { // skipping a question with multiple choices will return boolean convertedAnswers[key] = []; - } else { + } + else { convertedAnswers[key] = value; } } return convertedAnswers; -}; +} // Scroll to section on mobile -const scrollToSection = (selector: string) => { +function scrollToSection(selector: string) { if (document.body.clientWidth < 600) { document.querySelector(selector)?.scrollIntoView?.({ behavior: "smooth" }); } -}; +} export const surveyMachine = setup({ types: { @@ -76,37 +89,71 @@ export const surveyMachine = setup({ isRequiredAndEmpty: ({ context }) => { const section = context.sections[context.currentSectionIdx]; const question = section.questions[context.currentQuestionIdx]; - if (!question.required) return false; + if (!question.required) + return false; const questionId = `${section.label}-q-${context.currentQuestionIdx}`; const value = context.answers[questionId]; - return value === null || value === undefined; + return value == null || (Array.isArray(value) && !value.length); }, isLastQuestion: ({ context }) => { const section = context.sections[context.currentSectionIdx]; - return context.currentQuestionIdx === section.questions.length - 1; + return !hasNextVisibleQuestion( + section.questions, + context.currentQuestionIdx, + context.answers + ); }, isNotLastQuestion: ({ context }) => { const section = context.sections[context.currentSectionIdx]; - return context.currentQuestionIdx < section.questions.length - 1; + return hasNextVisibleQuestion( + section.questions, + context.currentQuestionIdx, + context.answers + ); }, isLastSection: ({ context }) => { - return context.currentSectionIdx === context.sections.length - 1; + return ( + context.currentSectionIdx + === context.visibleSectionIndices[context.visibleSectionIndices.length - 1] + ); }, isNotLastSection: ({ context }) => { - return context.currentSectionIdx < context.sections.length - 1; + return ( + context.currentSectionIdx + !== context.visibleSectionIndices[context.visibleSectionIndices.length - 1] + ); }, canGoBackInSection: ({ context }) => { - return context.currentQuestionIdx > 0; + const section = context.sections[context.currentSectionIdx]; + return hasPrevVisibleQuestion( + section.questions, + context.currentQuestionIdx, + context.answers + ); }, canGoBackToSection: ({ context }) => { - return context.currentQuestionIdx === 0 && context.currentSectionIdx > 0; + const section = context.sections[context.currentSectionIdx]; + const hasPrevInSection = hasPrevVisibleQuestion( + section.questions, + context.currentQuestionIdx, + context.answers + ); + const currentVisibleSectionIdx = context.visibleSectionIndices.indexOf( + context.currentSectionIdx + ); + return !hasPrevInSection && currentVisibleSectionIdx > 0; } }, actions: { + computeVisibleIndices: assign({ + visibleSectionIndices: ({ context }) => + getVisibleSectionIndices(context.sections, context.answers) + }), updateAnswer: assign({ answers: ({ context, event }) => { - if (event.type !== "ANSWER_CHANGE") return context.answers; + if (event.type !== "ANSWER_CHANGE") + return context.answers; return { ...context.answers, [event.questionId]: event.value @@ -114,20 +161,73 @@ export const surveyMachine = setup({ } }), incrementQuestion: assign({ - currentQuestionIdx: ({ context }) => context.currentQuestionIdx + 1 + currentQuestionIdx: ({ context }) => { + const section = context.sections[context.currentSectionIdx]; + const next = getNextVisibleQuestionIndex( + section.questions, + context.currentQuestionIdx, + context.answers + ); + return next ?? context.currentQuestionIdx; + } }), decrementQuestion: assign({ - currentQuestionIdx: ({ context }) => context.currentQuestionIdx - 1 + currentQuestionIdx: ({ context }) => { + const section = context.sections[context.currentSectionIdx]; + const prev = getPrevVisibleQuestionIndex( + section.questions, + context.currentQuestionIdx, + context.answers + ); + return prev ?? context.currentQuestionIdx; + } }), incrementSection: assign({ - currentSectionIdx: ({ context }) => context.currentSectionIdx + 1, - currentQuestionIdx: 0 + currentSectionIdx: ({ context }) => { + const currentIdx = context.visibleSectionIndices.indexOf( + context.currentSectionIdx + ); + const nextVisibleIdx = context.visibleSectionIndices[currentIdx + 1]; + return nextVisibleIdx ?? context.currentSectionIdx; + }, + currentQuestionIdx: ({ context }) => { + const currentIdx = context.visibleSectionIndices.indexOf( + context.currentSectionIdx + ); + const nextSectionIdx = context.visibleSectionIndices[currentIdx + 1]; + if (nextSectionIdx != null) { + const nextSection = context.sections[nextSectionIdx]; + const firstVisible = getFirstVisibleQuestionIndex( + nextSection.questions, + context.answers + ); + return firstVisible ?? 0; + } + return 0; + } }), goToLastQuestionInPreviousSection: assign({ - currentSectionIdx: ({ context }) => context.currentSectionIdx - 1, + currentSectionIdx: ({ context }) => { + const currentIdx = context.visibleSectionIndices.indexOf( + context.currentSectionIdx + ); + const prevVisibleIdx = context.visibleSectionIndices[currentIdx - 1]; + return prevVisibleIdx ?? context.currentSectionIdx; + }, currentQuestionIdx: ({ context }) => { - const prevSection = context.sections[context.currentSectionIdx - 1]; - return prevSection.questions.length - 1; + const currentIdx = context.visibleSectionIndices.indexOf( + context.currentSectionIdx + ); + const prevSectionIdx = context.visibleSectionIndices[currentIdx - 1]; + if (prevSectionIdx != null) { + const prevSection = context.sections[prevSectionIdx]; + const lastVisible = getLastVisibleQuestionIndex( + prevSection.questions, + context.answers + ); + return lastVisible ?? 0; + } + return 0; } }), setRequiredError: assign({ @@ -145,18 +245,21 @@ export const surveyMachine = setup({ goToThanksPage, goToSection: assign({ currentSectionIdx: ({ event }) => { - if (event.type !== "GO_TO_SECTION") return 0; + if (event.type !== "GO_TO_SECTION") + return 0; return event.sectionIdx; }, currentQuestionIdx: 0 }), goToQuestion: assign({ currentSectionIdx: ({ event }) => { - if (event.type !== "GO_TO_QUESTION") return 0; + if (event.type !== "GO_TO_QUESTION") + return 0; return event.sectionIdx; }, currentQuestionIdx: ({ event }) => { - if (event.type !== "GO_TO_QUESTION") return 0; + if (event.type !== "GO_TO_QUESTION") + return 0; return event.questionIdx; } }) @@ -183,18 +286,25 @@ export const surveyMachine = setup({ }).createMachine({ id: "survey", initial: "answering", - context: ({ input }) => ({ - sections: input.sections, - currentSectionIdx: input.persisted?.currentSectionIdx ?? 0, - currentQuestionIdx: input.persisted?.currentQuestionIdx ?? 0, - answers: input.persisted?.answers ?? {}, - error: null - }), + context: ({ input }) => { + const sections = input.sections; + const answers = input.persisted?.answers ?? {}; + + return { + sections, + currentSectionIdx: input.persisted?.currentSectionIdx ?? 0, + currentQuestionIdx: input.persisted?.currentQuestionIdx ?? 0, + answers, + error: null, + visibleSectionIndices: getVisibleSectionIndices(sections, answers) + }; + }, states: { answering: { + entry: ["computeVisibleIndices"], after: { ERROR_TIMEOUT: { - guard: ({ context }) => context.error !== null, + guard: ({ context }) => !!context.error, actions: ["clearError"] } }, @@ -203,7 +313,7 @@ export const surveyMachine = setup({ actions: ["clearError"] }, ANSWER_CHANGE: { - actions: ["updateAnswer", "clearError"] + actions: ["updateAnswer", "computeVisibleIndices", "clearError"] }, NEXT: [ { diff --git a/src/components/survey/utils.ts b/src/components/survey/utils.ts index 1c739646..13100a5c 100644 --- a/src/components/survey/utils.ts +++ b/src/components/survey/utils.ts @@ -1,11 +1,9 @@ import { actions } from "astro:actions"; -export const submitAnswers = ( - data: Parameters[0] -) => { +export async function submitAnswers(data: Parameters[0]) { return actions.submitAnswers(data); -}; +} -export const goToThanksPage = () => { +export function goToThanksPage() { window.location.href = "/thanks"; -}; +} diff --git a/src/components/theme-toggle.astro b/src/components/theme-toggle.astro new file mode 100644 index 00000000..9f197525 --- /dev/null +++ b/src/components/theme-toggle.astro @@ -0,0 +1,67 @@ +--- + +--- + + + + + + + + + + + + + + + diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx deleted file mode 100644 index 3c08348f..00000000 --- a/src/components/theme-toggle.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useState } from "react"; - -export const ThemeToggle = () => { - const [theme, setTheme] = useState<"light" | "dark">("dark"); - const [mounted, setMounted] = useState(false); - - // Load theme from localStorage on mount - useEffect(() => { - setMounted(true); - const storedTheme = localStorage.getItem("theme") as - | "light" - | "dark" - | null; - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - const initialTheme = storedTheme || "dark"; - setTheme(initialTheme); - document.documentElement.classList.toggle("dark", initialTheme === "dark"); - }, []); - - const toggleTheme = () => { - const newTheme = theme === "light" ? "dark" : "light"; - setTheme(newTheme); - localStorage.setItem("theme", newTheme); - document.documentElement.classList.toggle("dark", newTheme === "dark"); - }; - - // Don't render until mounted to prevent hydration mismatch - if (!mounted) { - return ( - - - - ); - } - - return ( - - {theme === "light" ? ( - - - - ) : ( - - - - )} - - ); -}; diff --git a/src/lib/captcha.ts b/src/lib/captcha.ts index 36032a3d..e4072bbf 100644 --- a/src/lib/captcha.ts +++ b/src/lib/captcha.ts @@ -1,7 +1,14 @@ -const CLOUDFLARE_CAPTCHA_URL = - "https://challenges.cloudflare.com/turnstile/v0/siteverify"; +const CLOUDFLARE_CAPTCHA_URL + = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; -export const isCaptchaValid = async (token: string) => { +interface CaptchaResponse { + "success": boolean; + "error-codes"?: string[]; + "challenge_ts"?: string; + "hostname"?: string; +} + +export async function isCaptchaValid(token: string): Promise { try { const response = await fetch(CLOUDFLARE_CAPTCHA_URL, { method: "POST", @@ -9,15 +16,16 @@ export const isCaptchaValid = async (token: string) => { "Content-Type": "application/x-www-form-urlencoded" }, body: JSON.stringify({ - secret: import.meta.env.PUBLIC_TURNSTILE_SECRET_KEY, + secret: String(import.meta.env.PUBLIC_TURNSTILE_SECRET_KEY ?? ""), response: token }) }); - const data = await response.json(); - return data.success; - } catch (error) { + const data = await response.json() as CaptchaResponse; + return data.success ?? false; + } + catch (error) { console.error("Error validating captcha:", error); return false; } -}; +} diff --git a/src/lib/conditions.test.ts b/src/lib/conditions.test.ts new file mode 100644 index 00000000..6b21aae7 --- /dev/null +++ b/src/lib/conditions.test.ts @@ -0,0 +1,445 @@ +import type { Answers, QuestionList, SectionList } from "./conditions"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + + evaluateCondition, + getFirstVisibleQuestionIndex, + getLastVisibleQuestionIndex, + getNextVisibleQuestionIndex, + getPrevVisibleQuestionIndex, + getVisibleSectionIndices, + hasNextVisibleQuestion, + hasPrevVisibleQuestion + +} from "./conditions"; + +describe("evaluateCondition", () => { + let consoleWarnSpy: ReturnType; + + beforeEach(() => { + consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + describe("no condition", () => { + it("returns true when condition is undefined", () => { + expect(evaluateCondition(undefined, {})).toBe(true); + }); + }); + + describe("invalid question ID format", () => { + it("returns true and warns for empty question ID", () => { + const condition = { question: "", equals: 1 }; + expect(evaluateCondition(condition, {})).toBe(true); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("Invalid question ID format") + ); + }); + + it("returns true and warns for invalid format without -q-", () => { + const condition = { question: "profile-0", equals: 1 }; + expect(evaluateCondition(condition, {})).toBe(true); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + + it("returns true and warns for uppercase question ID", () => { + const condition = { question: "Profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, {})).toBe(true); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + + it("accepts valid question ID formats", () => { + const answers: Answers = { "profile-q-0": 1 }; + const condition = { question: "profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(true); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it("accepts kebab-case section labels", () => { + const answers: Answers = { "learning-education-q-5": 2 }; + const condition = { question: "learning-education-q-5", equals: 2 }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + }); + + describe("unanswered questions", () => { + it("returns false when answer is null", () => { + const answers: Answers = { "profile-q-0": null }; + const condition = { question: "profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + + it("returns false when answer is undefined (not in answers)", () => { + const answers: Answers = {}; + const condition = { question: "profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + }); + + describe("equals operator", () => { + it("returns true when answer equals expected value", () => { + const answers: Answers = { "profile-q-0": 2 }; + const condition = { question: "profile-q-0", equals: 2 }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + + it("returns false when answer does not equal expected value", () => { + const answers: Answers = { "profile-q-0": 1 }; + const condition = { question: "profile-q-0", equals: 2 }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + + it("returns false and warns when answer is an array", () => { + const answers: Answers = { "profile-q-0": [1, 2] }; + const condition = { question: "profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(false); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("equals operator used on non-number answer") + ); + }); + + it("returns false and warns when answer is a string", () => { + const answers: Answers = { "profile-q-0": "text" }; + const condition = { question: "profile-q-0", equals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(false); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + + it("handles equals: 0 correctly", () => { + const answers: Answers = { "profile-q-0": 0 }; + const condition = { question: "profile-q-0", equals: 0 }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + }); + + describe("notEquals operator", () => { + it("returns true when answer does not equal expected value", () => { + const answers: Answers = { "profile-q-0": 1 }; + const condition = { question: "profile-q-0", notEquals: 2 }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + + it("returns false when answer equals expected value", () => { + const answers: Answers = { "profile-q-0": 2 }; + const condition = { question: "profile-q-0", notEquals: 2 }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + + it("returns false and warns when answer is an array", () => { + const answers: Answers = { "profile-q-0": [1, 2] }; + const condition = { question: "profile-q-0", notEquals: 1 }; + expect(evaluateCondition(condition, answers)).toBe(false); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("notEquals operator used on non-number answer") + ); + }); + }); + + describe("in operator", () => { + it("returns true when answer is in the expected array", () => { + const answers: Answers = { "profile-q-0": 2 }; + const condition = { question: "profile-q-0", in: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + + it("returns false when answer is not in the expected array", () => { + const answers: Answers = { "profile-q-0": 5 }; + const condition = { question: "profile-q-0", in: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + + it("returns true when answer is the only value in array", () => { + const answers: Answers = { "profile-q-0": 1 }; + const condition = { question: "profile-q-0", in: [1] }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + + it("returns false and warns when answer is an array", () => { + const answers: Answers = { "profile-q-0": [1, 2] }; + const condition = { question: "profile-q-0", in: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(false); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("in operator used on non-number answer") + ); + }); + }); + + describe("notIn operator", () => { + it("returns true when answer is not in the expected array", () => { + const answers: Answers = { "profile-q-0": 5 }; + const condition = { question: "profile-q-0", notIn: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(true); + }); + + it("returns false when answer is in the expected array", () => { + const answers: Answers = { "profile-q-0": 2 }; + const condition = { question: "profile-q-0", notIn: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(false); + }); + + it("returns false and warns when answer is an array", () => { + const answers: Answers = { "profile-q-0": [4, 5] }; + const condition = { question: "profile-q-0", notIn: [1, 2, 3] }; + expect(evaluateCondition(condition, answers)).toBe(false); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("notIn operator used on non-number answer") + ); + }); + }); + + describe("no valid operator", () => { + it("returns true and warns when no operator is provided", () => { + const answers: Answers = { "profile-q-0": 1 }; + const condition = { question: "profile-q-0" } as { + question: string; + equals?: number; + }; + expect(evaluateCondition(condition, answers)).toBe(true); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("No valid operator found") + ); + }); + }); +}); + +describe("getVisibleSectionIndices", () => { + it("returns all indices when no conditions exist", () => { + const sections = [ + { questions: [{}] }, + { questions: [{}] }, + { questions: [{}] } + ] as SectionList; + const answers: Answers = {}; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0, 1, 2]); + }); + + it("filters out sections with failing showIf conditions", () => { + const sections = [ + { showIf: undefined, questions: [{}] }, + { showIf: { question: "profile-q-0", equals: 1 }, questions: [{}] }, + { showIf: undefined, questions: [{}] } + ] as SectionList; + const answers: Answers = { "profile-q-0": 0 }; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0, 2]); + }); + + it("includes sections when showIf condition passes", () => { + const sections = [ + { showIf: undefined, questions: [{}] }, + { showIf: { question: "profile-q-0", equals: 1 }, questions: [{}] } + ] as SectionList; + const answers: Answers = { "profile-q-0": 1 }; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0, 1]); + }); + + it("filters out sections with no questions", () => { + const sections = [ + { questions: [{}] }, + { questions: [] }, + { questions: [{}] } + ] as SectionList; + const answers: Answers = {}; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0, 2]); + }); + + it("filters out sections where all questions are hidden", () => { + const sections = [ + { questions: [{}] }, + { + questions: [ + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } } + ] + } + ] as SectionList; + const answers: Answers = { "profile-q-0": 0 }; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0]); + }); + + it("includes sections with at least one visible question", () => { + const sections = [ + { + questions: [ + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: undefined } + ] + } + ] as SectionList; + const answers: Answers = { "profile-q-0": 0 }; + expect(getVisibleSectionIndices(sections, answers)).toEqual([0]); + }); +}); + +describe("getNextVisibleQuestionIndex", () => { + it("returns next index when no conditions exist", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getNextVisibleQuestionIndex(questions, 0, {})).toBe(1); + expect(getNextVisibleQuestionIndex(questions, 1, {})).toBe(2); + }); + + it("returns undefined when at last question", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getNextVisibleQuestionIndex(questions, 2, {})).toBeUndefined(); + }); + + it("skips hidden questions", () => { + const questions: QuestionList = [ + {}, + { showIf: { question: "profile-q-0", equals: 1 } }, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getNextVisibleQuestionIndex(questions, 0, answers)).toBe(2); + }); + + it("returns undefined when all remaining questions are hidden", () => { + const questions: QuestionList = [ + {}, + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } } + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getNextVisibleQuestionIndex(questions, 0, answers)).toBeUndefined(); + }); + + it("finds visible question after multiple hidden ones", () => { + const questions: QuestionList = [ + {}, + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } }, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getNextVisibleQuestionIndex(questions, 0, answers)).toBe(3); + }); +}); + +describe("getPrevVisibleQuestionIndex", () => { + it("returns previous index when no conditions exist", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getPrevVisibleQuestionIndex(questions, 2, {})).toBe(1); + expect(getPrevVisibleQuestionIndex(questions, 1, {})).toBe(0); + }); + + it("returns undefined when at first question", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getPrevVisibleQuestionIndex(questions, 0, {})).toBeUndefined(); + }); + + it("skips hidden questions", () => { + const questions: QuestionList = [ + {}, + { showIf: { question: "profile-q-0", equals: 1 } }, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getPrevVisibleQuestionIndex(questions, 2, answers)).toBe(0); + }); + + it("returns undefined when all previous questions are hidden", () => { + const questions: QuestionList = [ + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } }, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getPrevVisibleQuestionIndex(questions, 2, answers)).toBeUndefined(); + }); +}); + +describe("getFirstVisibleQuestionIndex", () => { + it("returns 0 when first question is visible", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getFirstVisibleQuestionIndex(questions, {})).toBe(0); + }); + + it("skips hidden first questions", () => { + const questions: QuestionList = [ + { showIf: { question: "profile-q-0", equals: 1 } }, + {}, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getFirstVisibleQuestionIndex(questions, answers)).toBe(1); + }); + + it("returns undefined when all questions are hidden", () => { + const questions: QuestionList = [ + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } } + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getFirstVisibleQuestionIndex(questions, answers)).toBeUndefined(); + }); +}); + +describe("getLastVisibleQuestionIndex", () => { + it("returns last index when last question is visible", () => { + const questions: QuestionList = [{}, {}, {}]; + expect(getLastVisibleQuestionIndex(questions, {})).toBe(2); + }); + + it("skips hidden last questions", () => { + const questions: QuestionList = [ + {}, + {}, + { showIf: { question: "profile-q-0", equals: 1 } } + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getLastVisibleQuestionIndex(questions, answers)).toBe(1); + }); + + it("returns undefined when all questions are hidden", () => { + const questions: QuestionList = [ + { showIf: { question: "profile-q-0", equals: 1 } }, + { showIf: { question: "profile-q-0", equals: 1 } } + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(getLastVisibleQuestionIndex(questions, answers)).toBeUndefined(); + }); +}); + +describe("hasNextVisibleQuestion", () => { + it("returns true when there is a next visible question", () => { + const questions: QuestionList = [{}, {}]; + expect(hasNextVisibleQuestion(questions, 0, {})).toBe(true); + }); + + it("returns false when at last question", () => { + const questions: QuestionList = [{}, {}]; + expect(hasNextVisibleQuestion(questions, 1, {})).toBe(false); + }); + + it("returns false when all remaining questions are hidden", () => { + const questions: QuestionList = [ + {}, + { showIf: { question: "profile-q-0", equals: 1 } } + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(hasNextVisibleQuestion(questions, 0, answers)).toBe(false); + }); +}); + +describe("hasPrevVisibleQuestion", () => { + it("returns true when there is a previous visible question", () => { + const questions: QuestionList = [{}, {}]; + expect(hasPrevVisibleQuestion(questions, 1, {})).toBe(true); + }); + + it("returns false when at first question", () => { + const questions: QuestionList = [{}, {}]; + expect(hasPrevVisibleQuestion(questions, 0, {})).toBe(false); + }); + + it("returns false when all previous questions are hidden", () => { + const questions: QuestionList = [ + { showIf: { question: "profile-q-0", equals: 1 } }, + {} + ]; + const answers: Answers = { "profile-q-0": 0 }; + expect(hasPrevVisibleQuestion(questions, 1, answers)).toBe(false); + }); +}); diff --git a/src/lib/conditions.ts b/src/lib/conditions.ts new file mode 100644 index 00000000..a7063a39 --- /dev/null +++ b/src/lib/conditions.ts @@ -0,0 +1,254 @@ +import type { ShowIfCondition } from "./validators/survey-schema"; + +export type AnswerValue = number | number[] | null | string; +export type Answers = Record; +export type QuestionList = Array<{ showIf?: ShowIfCondition }>; +export type SectionList = Array<{ + showIf?: ShowIfCondition; + questions?: QuestionList; +}>; + +/** + * Evaluates a showIf condition against current answers + * @param condition - The condition to evaluate + * @param answers - Current survey answers + * @returns true if condition is met (show question/section), false otherwise + */ +export function evaluateCondition( + condition: ShowIfCondition | undefined, + answers: Answers +): boolean { + // No condition means always show + if (!condition) + return true; + + const { + question, + equals, + notEquals, + in: inArray, + notIn: notInArray + } = condition; + + // Validate question ID format + if (!question || !/^[a-z0-9-]+-q-\d+$/.test(question)) { + console.warn( + `[Condition] Invalid question ID format: "${question}". Showing question by default.` + ); + return true; + } + + // Get answer value + const answerValue = answers[question]; + + // Handle unanswered questions - hide if condition depends on it + if (answerValue == null) { + return false; + } + + // Evaluate operators + if (equals != null) { + return evaluateEquals(answerValue, equals); + } + + if (notEquals != null) { + return evaluateNotEquals(answerValue, notEquals); + } + + if (inArray) { + return evaluateIn(answerValue, inArray); + } + + if (notInArray) { + return evaluateNotIn(answerValue, notInArray); + } + + // No valid operator found + console.warn( + `[Condition] No valid operator found in condition for question "${question}". Showing by default.` + ); + return true; +} + +/** + * Checks if answer equals expected value + */ +function evaluateEquals(answer: AnswerValue, expected: number): boolean { + if (typeof answer !== "number") { + console.warn( + `[Condition] equals operator used on non-number answer. Expected number, got ${typeof answer}` + ); + return false; + } + return answer === expected; +} + +/** + * Checks if answer does not equal expected value + */ +function evaluateNotEquals(answer: AnswerValue, expected: number): boolean { + if (typeof answer !== "number") { + console.warn( + `[Condition] notEquals operator used on non-number answer. Expected number, got ${typeof answer}` + ); + return false; + } + return answer !== expected; +} + +/** + * Checks if answer is one of expected values + */ +function evaluateIn(answer: AnswerValue, expected: number[]): boolean { + if (typeof answer !== "number") { + console.warn( + `[Condition] in operator used on non-number answer. Expected number, got ${typeof answer}` + ); + return false; + } + return expected.includes(answer); +} + +/** + * Checks if answer is not one of expected values + */ +function evaluateNotIn(answer: AnswerValue, expected: number[]): boolean { + if (typeof answer !== "number") { + console.warn( + `[Condition] notIn operator used on non-number answer. Expected number, got ${typeof answer}` + ); + return false; + } + return !expected.includes(answer); +} + +/** + * Filters sections based on their showIf conditions and whether they have visible questions + * @param sections - All survey sections + * @param answers - Current survey answers + * @returns Array of section indices that should be visible + */ +export function getVisibleSectionIndices( + sections: SectionList, + answers: Answers +): number[] { + return sections + .map((section, index) => { + const sectionVisible = evaluateCondition(section.showIf, answers); + if (!sectionVisible) + return { index, visible: false }; + + // Check if section has at least one visible question + const questions = section.questions; + if (!questions?.length) { + return { index, visible: false }; + } + + const hasVisibleQuestion = questions.some(q => + evaluateCondition(q.showIf, answers) + ); + + return { + index, + visible: hasVisibleQuestion + }; + }) + .filter(item => item.visible) + .map(item => item.index); +} + +/** + * Finds the next visible question after the given index + * @param questions - All questions in a section + * @param fromIdx - Current question index + * @param answers - Current survey answers + * @returns Index of next visible question, or undefined if none + */ +export function getNextVisibleQuestionIndex( + questions: QuestionList, + fromIdx: number, + answers: Answers +): number | undefined { + for (let i = fromIdx + 1; i < questions.length; i++) { + if (evaluateCondition(questions[i].showIf, answers)) { + return i; + } + } + return undefined; +} + +/** + * Finds the previous visible question before the given index + * @param questions - All questions in a section + * @param fromIdx - Current question index + * @param answers - Current survey answers + * @returns Index of previous visible question, or undefined if none + */ +export function getPrevVisibleQuestionIndex( + questions: QuestionList, + fromIdx: number, + answers: Answers +): number | undefined { + for (let i = fromIdx - 1; i >= 0; i--) { + if (evaluateCondition(questions[i].showIf, answers)) { + return i; + } + } + return undefined; +} + +/** + * Finds the first visible question in a section + * @param questions - All questions in a section + * @param answers - Current survey answers + * @returns Index of first visible question, or undefined if none + */ +export function getFirstVisibleQuestionIndex( + questions: QuestionList, + answers: Answers +): number | undefined { + return getNextVisibleQuestionIndex(questions, -1, answers); +} + +/** + * Finds the last visible question in a section + * @param questions - All questions in a section + * @param answers - Current survey answers + * @returns Index of last visible question, or undefined if none + */ +export function getLastVisibleQuestionIndex( + questions: QuestionList, + answers: Answers +): number | undefined { + return getPrevVisibleQuestionIndex(questions, questions.length, answers); +} + +/** + * Checks if there's a visible question after the given index + * @param questions - All questions in a section + * @param fromIdx - Current question index + * @param answers - Current survey answers + * @returns true if there's a next visible question + */ +export function hasNextVisibleQuestion( + questions: QuestionList, + fromIdx: number, + answers: Answers +): boolean { + return getNextVisibleQuestionIndex(questions, fromIdx, answers) != null; +} + +/** + * Checks if there's a visible question before the given index + * @param questions - All questions in a section + * @param fromIdx - Current question index + * @param answers - Current survey answers + * @returns true if there's a previous visible question + */ +export function hasPrevVisibleQuestion( + questions: QuestionList, + fromIdx: number, + answers: Answers +): boolean { + return getPrevVisibleQuestionIndex(questions, fromIdx, answers) != null; +} diff --git a/src/lib/firebase/client.ts b/src/lib/firebase/client.ts index abdab9e9..35f293c4 100644 --- a/src/lib/firebase/client.ts +++ b/src/lib/firebase/client.ts @@ -1,14 +1,15 @@ +import type { FirebaseOptions } from "firebase/app"; // Client-side Firebase initialization import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; -const firebaseConfig = { - apiKey: import.meta.env.PUBLIC_FIREBASE_API_KEY, - authDomain: import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: import.meta.env.PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: import.meta.env.PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: import.meta.env.PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: import.meta.env.PUBLIC_FIREBASE_APP_ID +const firebaseConfig: FirebaseOptions = { + apiKey: String(import.meta.env.PUBLIC_FIREBASE_API_KEY ?? ""), + authDomain: String(import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN ?? ""), + projectId: String(import.meta.env.PUBLIC_FIREBASE_PROJECT_ID ?? ""), + storageBucket: String(import.meta.env.PUBLIC_FIREBASE_STORAGE_BUCKET ?? ""), + messagingSenderId: String(import.meta.env.PUBLIC_FIREBASE_MESSAGING_SENDER_ID ?? ""), + appId: String(import.meta.env.PUBLIC_FIREBASE_APP_ID ?? "") }; export const app = initializeApp(firebaseConfig); diff --git a/src/lib/firebase/database.ts b/src/lib/firebase/database.ts index 2c287745..9580dc92 100644 --- a/src/lib/firebase/database.ts +++ b/src/lib/firebase/database.ts @@ -1,39 +1,39 @@ +import type { UserRecord } from "firebase-admin/auth"; import { getFirestore } from "firebase-admin/firestore"; import { getActiveApp } from "./server"; -import type { UserRecord } from "firebase-admin/auth"; const db = () => getFirestore(getActiveApp()); const getResults = () => db().collection("results"); -type Answers = { +interface Answers { [key: string]: number | string | number[] | null; -}; +} -type ExportedResult = { +interface ExportedResult { results: (Answers & { userId: string })[]; -}; +} -export const exportResults = async () => { +export async function exportResults() { const results = await getResults().get(); return { - results: results.docs.map((doc) => ({ ...doc.data(), userId: doc.id })) + results: results.docs.map(doc => ({ ...doc.data(), userId: doc.id })) } as ExportedResult; -}; +} -export const saveAnswers = (userId: string, data: Answers) => { +export async function saveAnswers(userId: string, data: Answers) { const updatedData = { ...data, lastUpdated: new Date().toISOString() }; return getResults().doc(userId).set(updatedData, { merge: true }); -}; +} -export const initUserSubmission = (user: UserRecord) => { +export async function initUserSubmission(user: UserRecord) { const userData = { creationTime: user.metadata.creationTime, lastSignInTime: user.metadata.lastSignInTime }; return getResults().doc(user.uid).set(userData, { merge: true }); -}; +} diff --git a/src/lib/firebase/server.ts b/src/lib/firebase/server.ts index 3f10ba14..6f6ede10 100644 --- a/src/lib/firebase/server.ts +++ b/src/lib/firebase/server.ts @@ -1,5 +1,5 @@ import type { ServiceAccount } from "firebase-admin"; -import { initializeApp, cert, getApps } from "firebase-admin/app"; +import { cert, getApps, initializeApp } from "firebase-admin/app"; const activeApps = getApps(); let serviceAccount = {}; @@ -7,6 +7,7 @@ let serviceAccount = {}; // we are using multiple methods to load the service account // because import.meta.env is not available while running the export-results script // and process.env is not available with Vite on the server +/* eslint-disable node/prefer-global/process */ if (import.meta.env?.FIREBASE_PROJECT_ID) { serviceAccount = { type: "service_account", @@ -20,7 +21,8 @@ if (import.meta.env?.FIREBASE_PROJECT_ID) { auth_provider_x509_cert_url: import.meta.env.FIREBASE_AUTH_CERT_URL, client_x509_cert_url: import.meta.env.FIREBASE_CLIENT_CERT_URL }; -} else if (process.env?.FIREBASE_PROJECT_ID) { +} +else if (process.env?.FIREBASE_PROJECT_ID) { serviceAccount = { type: "service_account", project_id: process.env.FIREBASE_PROJECT_ID, @@ -33,15 +35,16 @@ if (import.meta.env?.FIREBASE_PROJECT_ID) { auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_CERT_URL, client_x509_cert_url: process.env.FIREBASE_CLIENT_CERT_URL }; -} else { +} +else { throw new Error("No Firebase project ID found"); } -const initApp = () => { +function initApp() { return initializeApp({ credential: cert(serviceAccount as ServiceAccount) }); -}; +} export const app = activeApps.length === 0 ? initApp() : activeApps[0]; export const getActiveApp = () => app; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 365058ce..88283f01 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,5 @@ -import { type ClassValue, clsx } from "clsx"; +import type { ClassValue } from "clsx"; +import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { diff --git a/src/lib/validators/survey-schema.test.ts b/src/lib/validators/survey-schema.test.ts index 9666774c..29c7a56c 100644 --- a/src/lib/validators/survey-schema.test.ts +++ b/src/lib/validators/survey-schema.test.ts @@ -1,14 +1,14 @@ -import { describe, test, expect } from "vitest"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; import { - SurveyQuestionSchema, SurveyFileSchema, + SurveyQuestionSchema, validateSurveyFile, validateSurveyFileSafe } from "./survey-schema"; -import { z } from "zod"; -describe("SurveyQuestionSchema", () => { - test("validates a valid question", () => { +describe("surveyQuestionSchema", () => { + it("validates a valid question", () => { const validQuestion = { label: "What is your age?", required: true, @@ -26,7 +26,7 @@ describe("SurveyQuestionSchema", () => { } }); - test("applies default values for optional fields", () => { + it("applies default values for optional fields", () => { const minimalQuestion = { label: "Test question?", choices: ["Yes", "No"] @@ -40,7 +40,7 @@ describe("SurveyQuestionSchema", () => { } }); - test("trims whitespace from label and choices", () => { + it("trims whitespace from label and choices", () => { const question = { label: " Test question? ", choices: [" Choice 1 ", " Choice 2 "] @@ -55,7 +55,7 @@ describe("SurveyQuestionSchema", () => { } }); - test("rejects question with less than minimum choices", () => { + it("rejects question with less than minimum choices", () => { const invalidQuestion = { label: "Test?", choices: ["Only one"] @@ -64,12 +64,11 @@ describe("SurveyQuestionSchema", () => { const result = SurveyQuestionSchema.safeParse(invalidQuestion); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("at least 2 choices"); + expect(result.error.issues[0]?.message).toContain("at least 2 choices"); } }); - test("rejects question with empty label", () => { + it("rejects question with empty label", () => { const invalidQuestion = { label: "", choices: ["Yes", "No"] @@ -79,7 +78,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(false); }); - test("rejects question with whitespace-only label", () => { + it("rejects question with whitespace-only label", () => { const invalidQuestion = { label: " ", choices: ["Yes", "No"] @@ -89,7 +88,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(false); }); - test("rejects question with short label", () => { + it("rejects question with short label", () => { const invalidQuestion = { label: "ab", choices: ["Yes", "No"] @@ -99,7 +98,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(false); }); - test("rejects question with empty choice", () => { + it("rejects question with empty choice", () => { const invalidQuestion = { label: "Test?", choices: ["Valid", ""] @@ -109,7 +108,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(false); }); - test("rejects question with whitespace-only choice", () => { + it("rejects question with whitespace-only choice", () => { const invalidQuestion = { label: "Test?", choices: ["Valid", " "] @@ -119,7 +118,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(false); }); - test("rejects question with duplicate choices (case-insensitive)", () => { + it("rejects question with duplicate choices (case-insensitive)", () => { const invalidQuestion = { label: "Test?", choices: ["Male", "Female", "male"] @@ -128,12 +127,11 @@ describe("SurveyQuestionSchema", () => { const result = SurveyQuestionSchema.safeParse(invalidQuestion); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("Duplicate choices"); + expect(result.error.issues[0]?.message).toContain("Duplicate choices"); } }); - test("allows multiple 'Other' variations (handled as warnings elsewhere)", () => { + it("allows multiple 'Other' variations (handled as warnings elsewhere)", () => { const question = { label: "Test?", choices: ["Option 1", "Other", "Other option"] @@ -145,7 +143,7 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(true); }); - test("allows exactly one 'Other' option", () => { + it("allows exactly one 'Other' option", () => { const validQuestion = { label: "Test?", choices: ["Option 1", "Option 2", "Other"] @@ -155,19 +153,19 @@ describe("SurveyQuestionSchema", () => { expect(result.success).toBe(true); }); - test("warns about too many question marks", () => { - const invalidQuestion = { + it("allows multiple question marks (warning at validator level)", () => { + const question = { label: "Test?? Really??", choices: ["Yes", "No"] }; - const result = SurveyQuestionSchema.safeParse(invalidQuestion); - expect(result.success).toBe(false); + const result = SurveyQuestionSchema.safeParse(question); + expect(result.success).toBe(true); }); }); -describe("SurveyFileSchema", () => { - test("validates a complete valid survey file", () => { +describe("surveyFileSchema", () => { + it("validates a complete valid survey file", () => { const validFile = { title: "Profile", label: "profile", @@ -196,7 +194,7 @@ describe("SurveyFileSchema", () => { } }); - test("rejects file with empty title", () => { + it("rejects file with empty title", () => { const invalidFile = { title: "", label: "profile", @@ -213,7 +211,7 @@ describe("SurveyFileSchema", () => { expect(result.success).toBe(false); }); - test("rejects file with non-kebab-case label", () => { + it("rejects file with non-kebab-case label", () => { const invalidFile = { title: "Profile", label: "Profile Section", @@ -229,12 +227,11 @@ describe("SurveyFileSchema", () => { const result = SurveyFileSchema.safeParse(invalidFile); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("kebab-case"); + expect(result.error.issues[0]?.message).toContain("kebab-case"); } }); - test("accepts valid kebab-case labels", () => { + it("accepts valid kebab-case labels", () => { const validLabels = [ "profile", "work-experience", @@ -261,7 +258,7 @@ describe("SurveyFileSchema", () => { }); }); - test("rejects invalid kebab-case labels", () => { + it("rejects invalid kebab-case labels", () => { const invalidLabels = [ "Profile", "work_experience", @@ -290,7 +287,7 @@ describe("SurveyFileSchema", () => { }); }); - test("rejects file with zero position", () => { + it("rejects file with zero position", () => { const invalidFile = { title: "Profile", label: "profile", @@ -307,7 +304,7 @@ describe("SurveyFileSchema", () => { expect(result.success).toBe(false); }); - test("rejects file with negative position", () => { + it("rejects file with negative position", () => { const invalidFile = { title: "Profile", label: "profile", @@ -324,7 +321,7 @@ describe("SurveyFileSchema", () => { expect(result.success).toBe(false); }); - test("rejects file with non-integer position", () => { + it("rejects file with non-integer position", () => { const invalidFile = { title: "Profile", label: "profile", @@ -341,7 +338,7 @@ describe("SurveyFileSchema", () => { expect(result.success).toBe(false); }); - test("rejects file with no questions", () => { + it("rejects file with no questions", () => { const invalidFile = { title: "Profile", label: "profile", @@ -352,12 +349,11 @@ describe("SurveyFileSchema", () => { const result = SurveyFileSchema.safeParse(invalidFile); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("at least one question"); + expect(result.error.issues[0]?.message).toContain("at least one question"); } }); - test("rejects file with duplicate question labels", () => { + it("rejects file with duplicate question labels", () => { const invalidFile = { title: "Profile", label: "profile", @@ -377,12 +373,11 @@ describe("SurveyFileSchema", () => { const result = SurveyFileSchema.safeParse(invalidFile); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("Duplicate question labels"); + expect(result.error.issues[0]?.message).toContain("Duplicate question labels"); } }); - test("rejects file with all optional questions", () => { + it("rejects file with all optional questions", () => { const invalidFile = { title: "Profile", label: "profile", @@ -404,13 +399,12 @@ describe("SurveyFileSchema", () => { const result = SurveyFileSchema.safeParse(invalidFile); expect(result.success).toBe(false); if (!result.success) { - const issues = (result.error as any).issues || []; - expect(issues[0].message).toContain("At least one question"); - expect(issues[0].message).toContain("required"); + expect(result.error.issues[0]?.message).toContain("At least one question"); + expect(result.error.issues[0]?.message).toContain("required"); } }); - test("accepts file with at least one required question", () => { + it("accepts file with at least one required question", () => { const validFile = { title: "Profile", label: "profile", @@ -433,7 +427,7 @@ describe("SurveyFileSchema", () => { expect(result.success).toBe(true); }); - test("accepts file where required is not explicitly set (defaults to true)", () => { + it("accepts file where required is not explicitly set (defaults to true)", () => { const validFile = { title: "Profile", label: "profile", @@ -457,7 +451,7 @@ describe("SurveyFileSchema", () => { }); describe("validateSurveyFile helper", () => { - test("validates and returns typed data", () => { + it("validates and returns typed data", () => { const validData = { title: "Profile", label: "profile", @@ -476,7 +470,7 @@ describe("validateSurveyFile helper", () => { expect(result.questions).toHaveLength(1); }); - test("throws error with filename context", () => { + it("throws error with filename context", () => { const invalidData = { title: "Profile", label: "invalid label", @@ -494,7 +488,7 @@ describe("validateSurveyFile helper", () => { ); }); - test("throws error with validation details", () => { + it("throws error with validation details", () => { const invalidData = { title: "Profile", label: "invalid label", @@ -507,7 +501,7 @@ describe("validateSurveyFile helper", () => { }); describe("validateSurveyFileSafe helper", () => { - test("returns success result for valid data", () => { + it("returns success result for valid data", () => { const validData = { title: "Profile", label: "profile", @@ -527,7 +521,7 @@ describe("validateSurveyFileSafe helper", () => { } }); - test("returns error result for invalid data", () => { + it("returns error result for invalid data", () => { const invalidData = { title: "", label: "profile", @@ -539,14 +533,13 @@ describe("validateSurveyFileSafe helper", () => { expect(result.success).toBe(false); if (!result.success) { expect(result.error).toBeInstanceOf(z.ZodError); - const issues = (result.error as any).issues || []; - expect(issues.length).toBeGreaterThan(0); + expect(result.error.issues.length).toBeGreaterThan(0); } }); }); -describe("Real-world YAML structure validation", () => { - test("validates typical survey section structure", () => { +describe("real-world YAML structure validation", () => { + it("validates typical survey section structure", () => { const typicalSection = { title: "AI", label: "ai", @@ -588,9 +581,9 @@ describe("Real-world YAML structure validation", () => { }); }); -describe("Edge case tests", () => { - describe("Unicode and special characters", () => { - test("accepts Unicode characters in labels", () => { +describe("edge case tests", () => { + describe("unicode and special characters", () => { + it("accepts Unicode characters in labels", () => { const question = { label: "What programming languages do you use 🚀?", choices: ["JavaScript", "Python", "Go"] @@ -600,7 +593,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("accepts emoji in choice text", () => { + it("accepts emoji in choice text", () => { const question = { label: "What is your favorite beverage?", choices: ["Coffee ☕", "Tea 🍵", "Water 💧"] @@ -610,7 +603,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("accepts special characters in choices", () => { + it("accepts special characters in choices", () => { const question = { label: "What framework do you prefer?", choices: [ @@ -626,8 +619,8 @@ describe("Edge case tests", () => { }); }); - describe("Length validation", () => { - test("rejects label exceeding max length", () => { + describe("length validation", () => { + it("rejects label exceeding max length", () => { const veryLongLabel = "a".repeat(501); const question = { label: veryLongLabel, @@ -638,14 +631,14 @@ describe("Edge case tests", () => { expect(result.success).toBe(false); if (!result.success) { const issues = result.error.issues; - expect(issues.some((i) => i.message.includes("500 characters"))).toBe( + expect(issues.some(i => i.message.includes("500 characters"))).toBe( true ); } }); - test("accepts label at max length", () => { - const maxLengthLabel = "a".repeat(499) + "?"; // 499 chars + 1 for "?" = 500 + it("accepts label at max length", () => { + const maxLengthLabel = `${"a".repeat(499)}?`; // 499 chars + 1 for "?" = 500 const question = { label: maxLengthLabel, choices: ["Yes", "No"] @@ -655,7 +648,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("rejects choice exceeding max length", () => { + it("rejects choice exceeding max length", () => { const veryLongChoice = "b".repeat(201); const question = { label: "Test question?", @@ -666,13 +659,13 @@ describe("Edge case tests", () => { expect(result.success).toBe(false); if (!result.success) { const issues = result.error.issues; - expect(issues.some((i) => i.message.includes("200 characters"))).toBe( + expect(issues.some(i => i.message.includes("200 characters"))).toBe( true ); } }); - test("accepts choice at max length", () => { + it("accepts choice at max length", () => { const maxLengthChoice = "c".repeat(200); const question = { label: "Test question?", @@ -684,22 +677,18 @@ describe("Edge case tests", () => { }); }); - describe("Question mark placement", () => { - test("rejects question mark not at the end", () => { + describe("question mark placement", () => { + it("allows question mark not at the end (warning at validator level)", () => { const question = { label: "What? is your age", choices: ["18-24", "25-34", "35+"] }; const result = SurveyQuestionSchema.safeParse(question); - expect(result.success).toBe(false); - if (!result.success) { - const issues = result.error.issues; - expect(issues.some((i) => i.message.includes("at the end"))).toBe(true); - } + expect(result.success).toBe(true); }); - test("accepts question mark at the end", () => { + it("accepts question mark at the end", () => { const question = { label: "What is your age?", choices: ["18-24", "25-34", "35+"] @@ -709,7 +698,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("accepts label without question mark", () => { + it("accepts label without question mark", () => { const question = { label: "Select your programming languages", choices: ["JavaScript", "Python", "Go"] @@ -720,18 +709,18 @@ describe("Edge case tests", () => { }); }); - describe("Null and undefined handling", () => { - test("rejects null data", () => { + describe("null and undefined handling", () => { + it("rejects null data", () => { const result = SurveyFileSchema.safeParse(null); expect(result.success).toBe(false); }); - test("rejects undefined data", () => { + it("rejects undefined data", () => { const result = SurveyFileSchema.safeParse(undefined); expect(result.success).toBe(false); }); - test("rejects question with null label", () => { + it("rejects question with null label", () => { const question = { label: null, choices: ["Yes", "No"] @@ -741,7 +730,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(false); }); - test("rejects question with null choices", () => { + it("rejects question with null choices", () => { const question = { label: "Test?", choices: null @@ -751,7 +740,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(false); }); - test("rejects question with null choice in array", () => { + it("rejects question with null choice in array", () => { const question = { label: "Test?", choices: ["Valid", null, "Another"] @@ -762,8 +751,8 @@ describe("Edge case tests", () => { }); }); - describe("Complex real-world scenarios", () => { - test("validates question with long realistic label", () => { + describe("complex real-world scenarios", () => { + it("validates question with long realistic label", () => { const question = { label: "How often do you use AI-powered development tools like GitHub Copilot, ChatGPT, Claude, or similar assistants in your daily development workflow?", @@ -774,7 +763,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("validates choices with technical jargon", () => { + it("validates choices with technical jargon", () => { const question = { label: "What backend frameworks do you use?", choices: [ @@ -791,7 +780,7 @@ describe("Edge case tests", () => { expect(result.success).toBe(true); }); - test("validates multilingual content", () => { + it("validates multilingual content", () => { const file = { title: "التعليم والتطوير", label: "education", diff --git a/src/lib/validators/survey-schema.ts b/src/lib/validators/survey-schema.ts index 43f8b656..62cd1520 100644 --- a/src/lib/validators/survey-schema.ts +++ b/src/lib/validators/survey-schema.ts @@ -8,6 +8,36 @@ const MIN_TITLE_LENGTH = VALIDATION_THRESHOLDS.MIN_TITLE_LENGTH; const MAX_LABEL_LENGTH = VALIDATION_THRESHOLDS.MAX_LABEL_LENGTH; const MAX_CHOICE_LENGTH = VALIDATION_THRESHOLDS.MAX_CHOICE_LENGTH; +// Schema for conditional visibility (showIf) +export const ShowIfConditionSchema = z + .object({ + question: z + .string() + .regex( + /^[a-z0-9-]+-q-\d+$/, + "Question ID must be in format: {section-label}-q-{index}" + ), + equals: z.number().int().nonnegative().optional(), + notEquals: z.number().int().nonnegative().optional(), + in: z.array(z.number().int().nonnegative()).min(1).optional(), + notIn: z.array(z.number().int().nonnegative()).min(1).optional() + }) + .refine( + (data) => { + const operators = [ + data.equals !== undefined, + data.notEquals !== undefined, + data.in !== undefined, + data.notIn !== undefined + ].filter(Boolean); + return operators.length === 1; + }, + { + message: + "Exactly one operator must be specified: equals, notEquals, in, or notIn" + } + ); + export const SurveyQuestionSchema = z.object({ label: z .string() @@ -17,22 +47,9 @@ export const SurveyQuestionSchema = z.object({ MAX_LABEL_LENGTH, `Question label must not exceed ${MAX_LABEL_LENGTH} characters` ) - .refine((val) => val.trim().length > 0, { + .refine(val => val.trim().length > 0, { message: "Question label cannot be empty or whitespace only" - }) - .refine( - (val) => { - // Check for valid question mark usage: at most one, must be at the end - const questionMarks = (val.match(/\?/g) || []).length; - if (questionMarks === 0) return true; - if (questionMarks > 1) return false; - return val.trim().endsWith("?"); - }, - { - message: - "Question label should contain at most one question mark at the end" - } - ), + }), required: z.boolean().optional().default(true), @@ -48,7 +65,7 @@ export const SurveyQuestionSchema = z.object({ MAX_CHOICE_LENGTH, `Choice must not exceed ${MAX_CHOICE_LENGTH} characters` ) - .refine((val) => val.trim().length > 0, { + .refine(val => val.trim().length > 0, { message: "Choice cannot be whitespace only" }) ) @@ -56,15 +73,17 @@ export const SurveyQuestionSchema = z.object({ .refine( (choices) => { // Check for duplicate choices (case-insensitive) - const lowerCaseChoices = choices.map((c) => c.toLowerCase().trim()); + const lowerCaseChoices = choices.map(c => c.toLowerCase().trim()); const uniqueChoices = new Set(lowerCaseChoices); return uniqueChoices.size === lowerCaseChoices.length; }, { message: "Duplicate choices detected (case-insensitive comparison)" } - ) + ), // Note: Multiple "Other" variations are handled as warnings in cross-file validation + + showIf: ShowIfConditionSchema.optional() }); export const SurveyFileSchema = z @@ -73,7 +92,7 @@ export const SurveyFileSchema = z .string() .trim() .min(MIN_TITLE_LENGTH, "Section title must be at least 2 characters") - .refine((val) => val.trim().length > 0, { + .refine(val => val.trim().length > 0, { message: "Section title cannot be empty or whitespace only" }), @@ -85,7 +104,7 @@ export const SurveyFileSchema = z /^[a-z0-9]+(-[a-z0-9]+)*$/, "Section label must be in kebab-case format (lowercase, hyphens only)" ) - .refine((val) => val.trim().length > 0, { + .refine(val => val.trim().length > 0, { message: "Section label cannot be empty or whitespace only" }), @@ -101,20 +120,22 @@ export const SurveyFileSchema = z .refine( (questions) => { // Check for duplicate question labels within the section - const labels = questions.map((q) => q.label.toLowerCase().trim()); + const labels = questions.map(q => q.label.toLowerCase().trim()); const uniqueLabels = new Set(labels); return uniqueLabels.size === labels.length; }, { message: "Duplicate question labels detected within the section" } - ) + ), + + showIf: ShowIfConditionSchema.optional() }) .refine( (data) => { // Validate that at least one question is required const requiredCount = data.questions.filter( - (q) => q.required !== false + q => q.required !== false ).length; return requiredCount > 0; }, @@ -128,6 +149,7 @@ export const SurveyFileSchema = z * TypeScript types inferred from Zod schemas * These replace the types in custom-yaml.d.ts */ +export type ShowIfCondition = z.infer; export type SurveyQuestion = z.infer; export type SurveyQuestionsYamlFile = z.infer; @@ -144,7 +166,8 @@ export function validateSurveyFile( ): SurveyQuestionsYamlFile { try { return SurveyFileSchema.parse(data); - } catch (error) { + } + catch (error) { if (error instanceof z.ZodError) { const fileContext = filename ? ` in file "${filename}"` : ""; @@ -153,8 +176,8 @@ export function validateSurveyFile( const formattedErrors = issues .map((err) => { // Handle empty path array correctly - const path = - err.path && Array.isArray(err.path) && err.path.length > 0 + const path + = err.path && Array.isArray(err.path) && err.path.length > 0 ? err.path.join(".") : "root"; return ` - ${path}: ${err.message}`; @@ -167,10 +190,10 @@ export function validateSurveyFile( ); } // Re-throw non-Zod errors with preserved context - const message = `Unexpected error during validation${filename ? ` in file "${filename}"` : ""}`; + const message = `Unexpected error during validation${(filename != null) ? ` in file "${filename}"` : ""}`; throw new Error( - message + - (error instanceof Error ? `: ${error.message}` : `: ${String(error)}`), + message + + (error instanceof Error ? `: ${error.message}` : `: ${String(error)}`), { cause: error instanceof Error ? error : undefined } ); } diff --git a/src/lib/validators/survey-validator.ts b/src/lib/validators/survey-validator.ts index 8028c580..c9d2716f 100644 --- a/src/lib/validators/survey-validator.ts +++ b/src/lib/validators/survey-validator.ts @@ -1,15 +1,17 @@ -import * as fs from "fs"; -import * as path from "path"; +import type { ValidationError } from "./constants"; +import type { SurveyQuestionsYamlFile } from "./survey-schema"; +import * as fs from "node:fs"; +import * as path from "node:path"; import * as yaml from "js-yaml"; -import { - validateSurveyFile, - type SurveyQuestionsYamlFile -} from "./survey-schema"; import { VALIDATION_THRESHOLDS, - ValidationSeverity, - type ValidationError + + ValidationSeverity } from "./constants"; +import { + + validateSurveyFile +} from "./survey-schema"; export interface FileValidationResult { filename: string; @@ -37,7 +39,7 @@ export interface ValidationReport { export function validateAllSurveyFiles(surveyDir: string): ValidationReport { const files = fs .readdirSync(surveyDir) - .filter((file) => file.endsWith(".yml")) + .filter(file => file.endsWith(".yml")) .sort(); // Sort to ensure consistent ordering const fileResults: FileValidationResult[] = []; @@ -62,12 +64,12 @@ export function validateAllSurveyFiles(surveyDir: string): ValidationReport { const data = yaml.load(content); - if (!data) { + if (data == null) { throw new Error("YAML parsing returned null or undefined"); } if (typeof data !== "object") { - throw new Error("YAML file must contain an object"); + throw new TypeError("YAML file must contain an object"); } const validatedData = validateSurveyFile(data, file); @@ -83,7 +85,7 @@ export function validateAllSurveyFiles(surveyDir: string): ValidationReport { // Check for multiple "Other" options in questions (warning only) const otherWarnings: ValidationError[] = []; validatedData.questions.forEach((question, index) => { - const otherChoices = question.choices.filter((c) => + const otherChoices = question.choices.filter(c => /^other$/i.test(c.trim()) ); if (otherChoices.length > 1) { @@ -95,12 +97,43 @@ export function validateAllSurveyFiles(surveyDir: string): ValidationReport { } }); - const allWarnings = [...filenameWarnings, ...otherWarnings]; + // Check for question mark style issues (warning only) + const questionMarkWarnings: ValidationError[] = []; + validatedData.questions.forEach((question, index) => { + const questionMarks = (question.label.match(/\?/g) || []).length; + + if (questionMarks > 1) { + questionMarkWarnings.push({ + severity: ValidationSeverity.WARNING, + message: `Question ${index + 1} has multiple question marks`, + path: `questions[${index}].label`, + value: question.label + }); + } + else if ( + questionMarks === 1 + && !question.label.trim().endsWith("?") + ) { + questionMarkWarnings.push({ + severity: ValidationSeverity.WARNING, + message: `Question ${index + 1} has question mark not at end`, + path: `questions[${index}].label`, + value: question.label + }); + } + }); + + const allWarnings = [ + ...filenameWarnings, + ...otherWarnings, + ...questionMarkWarnings + ]; if (allWarnings.length > 0) { result.errors = allWarnings; // Don't mark as invalid for warnings } - } catch (error) { + } + catch (error) { result.valid = false; result.errors = [ { @@ -117,11 +150,11 @@ export function validateAllSurveyFiles(surveyDir: string): ValidationReport { const crossFileErrors = performCrossFileValidation(validFiles, files); // Generate report - const validCount = fileResults.filter((r) => r.valid).length; + const validCount = fileResults.filter(r => r.valid).length; // Only count actual errors, not warnings const actualCrossFileErrors = crossFileErrors.filter( - (e) => e.severity === ValidationSeverity.ERROR + e => e.severity === ValidationSeverity.ERROR ); const report: ValidationReport = { @@ -161,7 +194,7 @@ function validateFilename( } const [, positionStr, labelFromFilename] = match; - const positionFromFilename = parseInt(positionStr, 10); + const positionFromFilename = Number.parseInt(positionStr, 10); if (positionFromFilename !== data.position) { errors.push({ @@ -197,7 +230,7 @@ function performCrossFileValidation( const errors: ValidationError[] = []; // Check 1: Unique positions across all files - const positions = files.map((f) => f.position); + const positions = files.map(f => f.position); const duplicatePositions = findDuplicates(positions); if (duplicatePositions.length > 0) { errors.push({ @@ -224,7 +257,7 @@ function performCrossFileValidation( } // Check 3: Unique labels across all files - const labels = files.map((f) => f.label); + const labels = files.map(f => f.label); const duplicateLabels = findDuplicates(labels); if (duplicateLabels.length > 0) { errors.push({ @@ -261,7 +294,7 @@ function performCrossFileValidation( const duplicateQuestionIds = findDuplicates(allQuestionIds); if (duplicateQuestionIds.length > 0) { const duplicateDetails = duplicateQuestionIds - .map((id) => `${id} (in ${questionIdMap.get(id)})`) + .map(id => `${id} (in ${questionIdMap.get(id)})`) .join(", "); errors.push({ severity: ValidationSeverity.ERROR, @@ -274,15 +307,15 @@ function performCrossFileValidation( // Check 5: Validate reasonable question distribution files.forEach((file, index) => { const requiredCount = file.questions.filter( - (q) => q.required !== false + q => q.required !== false ).length; const optionalCount = file.questions.length - requiredCount; // Warn if section has too many optional questions (> threshold) const optionalRatio = optionalCount / file.questions.length; if ( - optionalRatio > VALIDATION_THRESHOLDS.OPTIONAL_RATIO_WARNING && - file.questions.length > VALIDATION_THRESHOLDS.MIN_QUESTIONS_WARNING + optionalRatio > VALIDATION_THRESHOLDS.OPTIONAL_RATIO_WARNING + && file.questions.length > VALIDATION_THRESHOLDS.MIN_QUESTIONS_WARNING ) { errors.push({ severity: ValidationSeverity.WARNING, @@ -335,6 +368,89 @@ function performCrossFileValidation( }); }); + // Check 7: Validate showIf cross-references + // Build position map: questionId -> { sectionPosition, questionIndex } + const questionPositionMap = new Map< + string, + { sectionPosition: number; questionIndex: number } + >(); + + files.forEach((file) => { + file.questions.forEach((_, qIndex) => { + const questionId = `${file.label}-q-${qIndex}`; + questionPositionMap.set(questionId, { + sectionPosition: file.position, + questionIndex: qIndex + }); + }); + }); + + // Validate section-level showIf references + files.forEach((file, fileIndex) => { + if (file.showIf?.question) { + const refId = file.showIf.question; + const refPosition = questionPositionMap.get(refId); + + if (!refPosition) { + errors.push({ + severity: ValidationSeverity.ERROR, + message: `Section showIf references non-existent question "${refId}"`, + path: `${filenames[fileIndex]}.showIf.question`, + value: refId + }); + } + else if (refPosition.sectionPosition >= file.position) { + errors.push({ + severity: ValidationSeverity.ERROR, + message: `Section showIf references question "${refId}" which is not in a previous section (references must point backward)`, + path: `${filenames[fileIndex]}.showIf.question`, + value: { + referenced: refId, + refSection: refPosition.sectionPosition, + currentSection: file.position + } + }); + } + } + + // Validate question-level showIf references + file.questions.forEach((question, qIndex) => { + if (question.showIf?.question) { + const refId = question.showIf.question; + const refPosition = questionPositionMap.get(refId); + const currentQuestionId = `${file.label}-q-${qIndex}`; + + if (!refPosition) { + errors.push({ + severity: ValidationSeverity.ERROR, + message: `Question showIf references non-existent question "${refId}"`, + path: `${filenames[fileIndex]}.questions[${qIndex}].showIf.question`, + value: refId + }); + } + else { + // Check if reference points backward (earlier section or earlier in same section) + const isEarlierSection = refPosition.sectionPosition < file.position; + const isSameSectionEarlierQuestion + = refPosition.sectionPosition === file.position + && refPosition.questionIndex < qIndex; + + if (!isEarlierSection && !isSameSectionEarlierQuestion) { + errors.push({ + severity: ValidationSeverity.ERROR, + message: `Question showIf references "${refId}" which does not come before "${currentQuestionId}" (references must point backward)`, + path: `${filenames[fileIndex]}.questions[${qIndex}].showIf.question`, + value: { + referenced: refId, + current: currentQuestionId + } + }); + } + } + } + }); + }); + return errors; } @@ -345,7 +461,8 @@ function findDuplicates(array: T[]): T[] { for (const item of array) { if (seen.has(item)) { duplicates.add(item); - } else { + } + else { seen.add(item); } } @@ -354,13 +471,13 @@ function findDuplicates(array: T[]): T[] { } const colors = { - reset: "\x1b[0m", - bright: "\x1b[1m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - cyan: "\x1b[36m" + reset: "\x1B[0m", + bright: "\x1B[1m", + red: "\x1B[31m", + green: "\x1B[32m", + yellow: "\x1B[33m", + blue: "\x1B[34m", + cyan: "\x1B[36m" }; /** @@ -421,12 +538,12 @@ export function formatValidationReport(report: ValidationReport): string { if (result.errors && result.errors.length > 0) { lines.push(" Issues:"); result.errors.forEach((error) => { - const errorColor = - error.severity === ValidationSeverity.ERROR + const errorColor + = error.severity === ValidationSeverity.ERROR ? colors.red : colors.yellow; - const prefix = - error.severity === ValidationSeverity.ERROR ? "✗" : "⚠"; + const prefix + = error.severity === ValidationSeverity.ERROR ? "✗" : "⚠"; const pathInfo = error.path ? ` [${error.path}]` : ""; lines.push( ` ${errorColor}${prefix}${colors.reset} ${error.message}${pathInfo}` @@ -443,8 +560,8 @@ export function formatValidationReport(report: ValidationReport): string { lines.push("-".repeat(60)); report.crossFileErrors.forEach((error) => { // Distinguish between errors and warnings by severity - const errorColor = - error.severity === ValidationSeverity.ERROR + const errorColor + = error.severity === ValidationSeverity.ERROR ? colors.red : colors.yellow; const prefix = error.severity === ValidationSeverity.ERROR ? "✗" : "⚠"; diff --git a/src/pages/2022.astro b/src/pages/2022.astro index 8525c7e4..00f18d38 100644 --- a/src/pages/2022.astro +++ b/src/pages/2022.astro @@ -2,6 +2,7 @@ import BaseLayout from "@/components/layout.astro"; import Repport from "@/components/report/index.astro"; + export const prerender = true; --- diff --git a/src/pages/api/init-session.ts b/src/pages/api/init-session.ts index 6b0a2c19..521ee048 100644 --- a/src/pages/api/init-session.ts +++ b/src/pages/api/init-session.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; -import { getActiveApp } from "@/lib/firebase/server"; import { getAuth } from "firebase-admin/auth"; +import { getActiveApp } from "@/lib/firebase/server"; // TODO: check if we really need this export const prerender = false; @@ -13,11 +13,11 @@ export const GET: APIRoute = async ({ request, cookies, redirect }) => { return new Response("No token found", { status: 401 }); } - console.log("idToken", idToken); /* Verify id token */ try { await auth.verifyIdToken(idToken); - } catch (error) { + } + catch { return new Response("Invalid token", { status: 401 }); } diff --git a/src/pages/api/remove-session.ts b/src/pages/api/remove-session.ts index f9554db3..5f7d6bdd 100644 --- a/src/pages/api/remove-session.ts +++ b/src/pages/api/remove-session.ts @@ -1,4 +1,5 @@ import type { APIRoute } from "astro"; + export const prerender = false; export const GET: APIRoute = async ({ redirect, cookies }) => { diff --git a/src/pages/before-start.astro b/src/pages/before-start.astro index 30252c94..5f56ae6e 100644 --- a/src/pages/before-start.astro +++ b/src/pages/before-start.astro @@ -67,66 +67,69 @@ export const prerender = true; + is:inline +> diff --git a/src/pages/home.astro b/src/pages/home.astro index f690b283..dfc06bf3 100644 --- a/src/pages/home.astro +++ b/src/pages/home.astro @@ -1,6 +1,6 @@ --- -import Layout from "../components/layout.astro"; import * as HomePage from "../components/home/index.astro"; +import Layout from "../components/layout.astro"; export const prerender = true; --- diff --git a/src/pages/playground.astro b/src/pages/playground.astro index 806f9628..130be3aa 100644 --- a/src/pages/playground.astro +++ b/src/pages/playground.astro @@ -1,6 +1,7 @@ --- -import { SurveyPlayground } from "@/components/playground"; import BaseLayout from "@/components/layout.astro"; +import { SurveyPlayground } from "@/components/playground"; + export const prerender = true; --- diff --git a/src/pages/survey.astro b/src/pages/survey.astro index 505eb8cd..3482f04a 100644 --- a/src/pages/survey.astro +++ b/src/pages/survey.astro @@ -1,9 +1,10 @@ --- -export const prerender = false; -import BaseLayout from "@/components/layout.astro"; -import { getActiveApp } from "@/lib/firebase/server"; import { getAuth } from "firebase-admin/auth"; +import BaseLayout from "@/components/layout.astro"; import SurveyForm from "@/components/survey/index.astro"; +import { getActiveApp } from "@/lib/firebase/server"; + +export const prerender = false; const auth = getAuth(getActiveApp()); diff --git a/src/pages/thanks.astro b/src/pages/thanks.astro index 497365db..871002c9 100644 --- a/src/pages/thanks.astro +++ b/src/pages/thanks.astro @@ -1,12 +1,13 @@ --- -export const prerender = false; -import SocialMediaCard from "@/components/social-media-card.astro"; import { Image } from "astro:assets"; +import { getAuth } from "firebase-admin/auth"; import tarbouch from "@/assets/tarbouch.png"; // Image is 1600x900 import BaseLayout from "@/components/layout.astro"; +import SocialMediaCard from "@/components/social-media-card.astro"; import { getActiveApp } from "@/lib/firebase/server"; -import { getAuth } from "firebase-admin/auth"; + +export const prerender = false; const auth = getAuth(getActiveApp()); @@ -133,7 +134,7 @@ const posts: Post[] = [ - {posts.map((post) => )} + {posts.map(post => )} @@ -141,35 +142,35 @@ const posts: Post[] = [ diff --git a/survey/1-profile.yml b/survey/1-profile.yml index 8924edea..9f7c11cd 100644 --- a/survey/1-profile.yml +++ b/survey/1-profile.yml @@ -114,6 +114,9 @@ questions: - No - label: If you are working abroad, do you have any plans to come back to Morocco? + showIf: + question: profile-q-7 + equals: 3 choices: - Yes, within the next 12 months - Yes, within the next 24 months diff --git a/survey/2-learning-and-education.yml b/survey/2-learning-and-education.yml index 61e70afc..1c8d3e85 100644 --- a/survey/2-learning-and-education.yml +++ b/survey/2-learning-and-education.yml @@ -72,7 +72,7 @@ questions: - Coding challenges (LeetCode, HackerRank, etc) - Other platforms - - label: What are the biggest challenges you find in learning new technologies? (pick 3) + - label: What are the biggest challenges you find in learning new technologies (pick 3)? required: true choices: - Lack of time diff --git a/vitest-setup.js b/vitest-setup.js index ee72373a..95e011e6 100644 --- a/vitest-setup.js +++ b/vitest-setup.js @@ -1,5 +1,5 @@ -import { afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; import "@testing-library/jest-dom/vitest"; // runs a clean after each test case (e.g. clearing jsdom) diff --git a/vitest.config.ts b/vitest.config.ts index 30fcb536..05f6b023 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,12 @@ +import react from "@vitejs/plugin-react"; /// import { getViteConfig } from "astro/config"; -import react from "@vitejs/plugin-react"; +// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-call +const reactPlugin = react(); export default getViteConfig({ - plugins: [react()], - // @ts-ignore + plugins: [reactPlugin], + // @ts-expect-error - getViteConfig doesn't include test property in types but it's supported test: { exclude: ["node_modules"], coverage: {
- {result.label} : {result.total}{" "} + {result.label} + {" "} + : + {result.total} + {" "}
+ Want to dive deeper into the survey results? Try our interactive data + playground! +
- Want to dive deeper into the survey results? Try our interactive data - playground! -
{children}
{post.user?.name ?? "Anonymous"}
{post.content}
- {`${index + 1}. ${label}`} + {`${index + 1}. ${label}`} + {" "} + @@ -140,7 +127,7 @@ export const Question = ({ > {choices.map((c, i) => (