diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aab46e1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,160 @@ +# AGENTS.md — Formify Frontend Developer & AI Agent Guide + +This document is the authoritative guide for AI coding assistants and autonomous agents working on the **Formify Frontend** codebase (`formify/`). + +--- + +## 1. Role & Mission + +You are an expert Frontend Engineer and React 19 specialist working on `formify`. Your goals: +1. Build and maintain intuitive, responsive, and robust UI components for form creation, customization, and submission. +2. Maintain feature parity and consistent behavior across both **Single Page** and **Flow** form modes. +3. Preserve strict TypeScript safety and ensure seamless schema alignment with the backend API. +4. Keep the codebase clean, adhering to React 19 standards and TailwindCSS v4 best practices. + +--- + +## 2. Technology Stack & Dependencies + +| Category | Technology | Version / Details | +| :--- | :--- | :--- | +| **Framework** | React 19 | `react@^19.2.3`, `react-dom@^19.2.3` with `babel-plugin-react-compiler` | +| **Build Tool** | Vite 7 | `vite@^7.2.4`, `@vitejs/plugin-react@^5.1.1` | +| **Language** | TypeScript | `typescript@~5.9.3` with strict typing | +| **Styling** | TailwindCSS v4 | `@tailwindcss/vite@^4.1.18` | +| **Routing** | React Router DOM v7 | `react-router-dom@^7.13.0` with lazy-loaded route components | +| **State** | Redux Toolkit | `@reduxjs/toolkit@^2.11.2`, `react-redux@^9.2.0` | +| **Drag & Drop** | `@dnd-kit` | `@dnd-kit/core@^6.3.1`, `@dnd-kit/sortable@^10.0.0` | +| **HTTP** | Axios | `axios@^1.13.4` (configured with `withCredentials: true`) | +| **Utilities** | PapaParse, Mapbox | CSV parsing and export, Mapbox location autocomplete | + +--- + +## 3. Project Structure & Key Directories + +```text +src/ +├── components/ +│ ├── BuilderCore/ # The form builder engine +│ │ ├── Flow/ # Flow (Typeform-style) canvas & card components +│ │ │ ├── FlowCard.tsx # Draggable card representing a step in Flow mode +│ │ │ ├── FlowFormBuilder.tsx # Flow builder controller +│ │ │ └── FlowPageCanvas.tsx # Canvas rendering Welcome, Step Cards, and Thank You +│ │ ├── Single/ # Single-page canvas & draggable field components +│ │ │ ├── DraggableField.tsx # Sortable field row on Single page canvas +│ │ │ ├── SinglePageCanvas.tsx # Canvas rendering Header and sortable fields +│ │ │ └── SinglePageFormBuilder.tsx # Single page builder controller +│ │ └── shared/ # Shared builder logic and UI +│ │ ├── EditorSections/ # Sub-panels in the right-hand property editor drawer: +│ │ │ ├── BasicFieldSettings.tsx # Title, subtitle, placeholder, limits +│ │ │ ├── HeaderSettings.tsx # Form title, description, banner image +│ │ │ ├── LogicEditor.tsx # Conditional rule definition +│ │ │ ├── OptionsEditor.tsx # Option list management (radio/select/checkbox) +│ │ │ ├── QuizAnswerSettings.tsx # Correct answers and points assignment +│ │ │ └── ScreenEditor.tsx # Welcome & Thank You screen customization +│ │ ├── constants.tsx # Palette item definitions (`FIELD_PALETTE_ITEMS`) +│ │ ├── FieldEditor.tsx # Master property editor panel +│ │ ├── FieldPalette.tsx # Drag-and-drop field types palette +│ │ ├── FormBuilder.tsx # Container switching between Single and Flow builders +│ │ ├── PreviewModal.tsx # Live preview dialog for both layouts +│ │ ├── quizUtils.ts # Score calculation and answer evaluation +│ │ ├── types.ts # Core TypeScript models (FormFieldConfig, Logic, Quiz) +│ │ └── useFormBuilder.ts # Core hook managing builder state, history, and actions +│ └── common/ # Reusable atomic UI components +│ ├── Button.tsx # Button with variant and size styling +│ ├── Checkbox.tsx # Form checkbox input +│ ├── DatePicker.tsx # Custom calendar picker +│ ├── FileUpload.tsx # File drag-and-drop with Cloudinary upload +│ ├── Footer.tsx # Global footer component +│ ├── FormattedAnswer.tsx # Formatted response value renderer +│ ├── GoogleSheetsModal.tsx # Google Sheets link and auto-sync modal +│ ├── icons.tsx # Shared SVG icons dictionary (`Icons.*`) +│ ├── InputField.tsx # Text/email/number input (`forwardRef`) +│ ├── Modal.tsx # Standard dialog modal wrapper +│ ├── RadioButton.tsx # Radio option button +│ ├── Rating.tsx # Star/heart/smile/thumb rating control +│ ├── Select.tsx # Dropdown selector (`forwardRef`) +│ ├── ShareModal.tsx # Share link popup with QR & copy actions +│ ├── TextArea.tsx # Multi-line text input (`forwardRef`) +│ └── TimePicker.tsx # Custom time selector component +├── layouts/ +│ ├── FlowPage.tsx # Typeform-like sequential step renderer +│ └── SinglePage.tsx # Traditional vertical form renderer with Quiz mode +├── pages/ # Application views +│ ├── AuthCallbackPage.tsx # OAuth redirect receiver +│ ├── BuilderPage.tsx # Form builder page (`/builder/:formId`) +│ ├── DashboardPage.tsx # User forms dashboard (`/dashboard`) +│ ├── FormResponsesPage.tsx # Responses table & CSV export (`/forms/:formId/responses`) +│ ├── HomePage.tsx # Public home page (`/`) +│ ├── PrivacyPage.tsx # Privacy policy page (`/privacy`) +│ ├── PublicFormPage.tsx # Responder form view (`/forms/:formId`) +│ ├── ResponsePage.tsx # Single submission detail page (`/responses/:responseId`) +│ └── TermsPage.tsx # Terms of service page (`/terms`) +├── services/ +│ ├── api.ts # Backend Axios client & API methods +│ └── apiTypes.ts # Request and response TypeScript interfaces +├── store/ +│ ├── slices/authSlice.ts # Authentication state slice +│ └── store.ts # Redux root store +└── utils/ + ├── formatters.ts # Date and time formatting utilities + └── validation.ts # Input validation functions +``` + +--- + +## 4. Domain Concepts & Implementation Details + +### 4.1 Field Types (`FieldType`) +- Standard text: `"text"`, `"textarea"`, `"email"`, `"tel"`, `"number"` +- Option selectors: `"radio"`, `"checkbox"`, `"select"` +- Date & Time: `"date"`, `"time"` +- Media & Rating: `"file"`, `"rating"` +- Layout helpers: `"page_break"` + +### 4.2 Form Builder State (`useFormBuilder`) +The form builder is driven by `useFormBuilder.ts` which manages: +- `fields`: Array of `FormFieldConfig` objects. +- `mode`: `"single" | "flow"`. +- `selectedFieldId`: Currently selected field for editing in `FieldEditor`. +- `welcomeScreen` & `thankYouScreen`: Flow mode configurations. +- `header`: Single page header configuration (`title`, `description`, `banner`). +- `isQuiz`: Boolean flag enabling Quiz Mode. + +### 4.3 Conditional Branching (Flow Mode) +- Defined per-field in `field.logic.rules`. +- Operators supported: `"equals"`, `"not_equals"`, `"contains"`, `"not_contains"`. +- Target: `targetFieldId` can be a specific field ID or `"SUBMIT"`. +- Evaluated in `FlowPage.tsx` sequentially upon answering each question. + +### 4.4 Quiz Mode (Single Page Mode) +- Configured via `correctAnswer` and `points` per field. +- Answers are evaluated by `isAnswerCorrect` in `src/components/BuilderCore/shared/quizUtils.ts`. +- Scores and question breakdowns are computed and displayed to responders upon submission. + +--- + +## 5. Coding Standards & Agent Guardrails + +1. **Idiomatic React 19**: Do not add unnecessary `useCallback` or `useMemo` hooks unless stabilizing references for external dependencies. +2. **Forward Refs**: All new input primitives placed in `src/components/common/` must wrap elements with `forwardRef` and specify `displayName`. +3. **Atomic Components**: Reuse existing primitives in `src/components/common/` to maintain visual consistency. +4. **TailwindCSS v4**: Write clean utility classes. Ensure full responsive design (`sm:`, `md:`, `lg:`). +5. **API & Types Synchronization**: Whenever modifying backend interactions, keep `src/services/api.ts` and `src/services/apiTypes.ts` fully typed and synchronized. +6. **Error Handling**: Handle network and validation errors gracefully with appropriate UI feedback (modals or notification alerts). + +--- + +## 6. Verification Checklist + +Before completing any task, always execute: +1. **Linting Check**: + ```bash + npm run lint + ``` + Must complete with zero ESLint errors or warnings. +2. **Typecheck & Production Build**: + ```bash + npm run build + ``` + Must succeed without TypeScript compilation (`tsc -b`) errors. diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a7960..703df69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ All notable changes to this project will be documented in this file. +## [Release 0.1.1] + +### Added + +- **Server-Side Quiz Integration**: Connected form submissions to the backend quiz evaluation API, rendering instant scored breakdowns (`quiz_result`) upon completion. +- **Flow Mode Quiz Support**: Enabled quiz mode configuration, points assignment, and question settings within `FlowFormBuilder` and `FlowPage`. +- **Question Breakdown Result Screen**: Added itemized quiz question breakdown displaying earned vs. max points and correct/incorrect status in both Single Page and Flow layouts. +- **Global Toast Notification System**: Added accessible, lightweight toast notifications (`Toast.tsx`, `toastContext.ts`) across dashboard actions and response deletions. + +### Changed + +- **UI Modernization**: Refreshed input controls, dropdowns, modals, and buttons across core builder and form layout components. + +### Fixed + +- **Duplicate Question Title Collision**: Keyed form submission answers by field ID in addition to titles, preventing answer overwrite when questions share identical titles. +- **Quiz Points Default Value Handling**: Fixed score computation in `FormResponsesPage.tsx` and layouts to default unedited question point values to `1` point instead of dropping them from score calculation. +- **Option Label & Case Normalization**: Enhanced `quizUtils.ts` to normalize option labels and values bidirectionally with case-insensitivity and array order invariance. +- **Preview Modal Flow Quiz Support**: Passed `isQuiz` prop to `FlowPage` within `PreviewModal`, ensuring quiz scoring functions properly in preview mode. +- **Removed Announcement Banner**: Removed the `"Question X of Y"` announcement container from `FlowPage.tsx` for a cleaner layout flow. + +### Security + +- **CSV Formula Injection Sanitization**: Sanitized respondent answer values beginning with formula trigger characters (`=`, `+`, `-`, `@`, `\t`, `\r`) with a leading single quote `'` in `FormResponsesPage.tsx` before passing data to `Papa.unparse` for CSV export. +- **Clickjacking & Security Headers**: Configured `vercel.json` with HTTP security headers, including `X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'none'` for builder and dashboard routes, along with `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin`. +- **Client-Side File Upload Validation**: Enforced a 10MB maximum file size limit in `FileUpload.tsx` and added an accessible error banner to reject oversized uploads before initiating network calls. +- **Reverse Tabnabbing Protection**: Added `'noopener,noreferrer'` features to `window.open` in `ShareModal.tsx`. +- **OAuth Callback URL Sanitization**: Added immediate `window.history.replaceState` in `AuthCallbackPage.tsx` on mount to strip sensitive OAuth parameters from the browser history and address bar. + +### Performance + +- **Pruned Unused Mapbox Dependencies**: Removed unused packages (`@mapbox/search-js-react`, `mapbox-gl`, and `react-map-gl`), eliminating 67 unused nested dependencies from `node_modules`. + ## [Release 0.1.0] ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1896a61 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,240 @@ +# CLAUDE.md — Formify Frontend + +This document provides architecture context, development guidelines, schema definitions, and command references for Claude Code when working in the `formify` frontend repository. + +--- + +## 1. Project Overview + +**Formify Frontend** is a modern form builder and submission platform built with React 19, TypeScript, and TailwindCSS v4. It allows users to design, publish, and analyze customizable forms in two distinct rendering layouts: +- **Single Page Mode**: Traditional vertical layout displaying all form questions, banner images, header titles, and automated **Quiz Mode** scoring. +- **Flow Mode**: Step-by-step Typeform-style questionnaire with custom Welcome Screen, Thank You Screen, and dynamic **Conditional Logic** jumping. +- **Integrations & Analytics**: Response capture, CSV export via PapaParse, Google Sheets automatic synchronization, and Cloudinary media uploads. + +--- + +## 2. Tech Stack + +- **Framework**: [React 19](https://react.dev/) (`react@^19.2.3`, `react-dom@^19.2.3`) +- **React Compiler**: `babel-plugin-react-compiler` enabled in Vite React plugin +- **Build Tool**: [Vite 7](https://vitejs.dev/) (`vite@^7.2.4`, `@vitejs/plugin-react@^5.1.1`) +- **Language**: [TypeScript](https://www.typescriptlang.org/) (~5.9.3) configured with strict mode (`tsconfig.app.json`) +- **Styling**: [TailwindCSS v4](https://tailwindcss.com/) (`@tailwindcss/vite@^4.1.18`) +- **Routing**: [React Router DOM v7](https://reactrouter.com/) (`react-router-dom@^7.13.0`) +- **State Management**: [Redux Toolkit](https://redux-toolkit.js.org/) (`@reduxjs/toolkit@^2.11.2`, `react-redux@^9.2.0`) +- **Drag & Drop**: [@dnd-kit/core](https://dndkit.com/) (`^6.3.1`), `@dnd-kit/sortable` (`^10.0.0`), `@dnd-kit/utilities` (`^3.2.2`) +- **HTTP Client**: [Axios](https://axios-http.com/) (`^1.13.4`) +- **Export & Maps**: `papaparse` (`^5.5.3`), `mapbox-gl` (`^3.18.1`), `react-map-gl` (`^8.1.0`), `@mapbox/search-js-react` (`^1.5.1`) +- **Code Quality**: ESLint 9 (`eslint@^9.39.1`), `typescript-eslint`, Prettier, Pre-commit + +--- + +## 3. Development Commands + +Run these commands from `formify/`: + +```bash +# Install dependencies +npm install + +# Start development server on http://localhost:5173 +npm run dev + +# Run TypeScript compilation check & build production bundle to dist/ +npm run build + +# Run ESLint across all files +npm run lint + +# Preview the production build locally +npm run preview + +# Run pre-commit checks on all files (if pre-commit is installed) +pre-commit run --all-files +``` + +--- + +## 4. Architecture & Directory Map + +```text +formify/ +├── docs/ # Project documentation +│ ├── components.md # UI and builder component reference +│ └── forms.md # Layouts, logic, and quiz mode specifications +├── public/ # Static public assets +├── src/ +│ ├── assets/ # Images and graphics +│ ├── components/ +│ │ ├── BuilderCore/ # Form builder core modules +│ │ │ ├── Flow/ # Flow builder canvas and card components +│ │ │ │ ├── FlowCard.tsx # Sortable step card in Flow canvas +│ │ │ │ ├── FlowFormBuilder.tsx # Flow mode master builder +│ │ │ │ └── FlowPageCanvas.tsx # Flow canvas with Welcome, Cards, Thank You +│ │ │ ├── Single/ # Single-page builder canvas and field components +│ │ │ │ ├── DraggableField.tsx # Draggable field item in Single canvas +│ │ │ │ ├── SinglePageCanvas.tsx # Single page canvas with header and fields +│ │ │ │ └── SinglePageFormBuilder.tsx # Single page master builder +│ │ │ └── shared/ # Shared builder components & utilities +│ │ │ ├── EditorSections/ # Sub-panels inside FieldEditor +│ │ │ │ ├── BasicFieldSettings.tsx # Title, subtitle, placeholder, limits +│ │ │ │ ├── HeaderSettings.tsx # Form title, description, banner URL +│ │ │ │ ├── LogicEditor.tsx # Conditional branching rule editor +│ │ │ │ ├── OptionsEditor.tsx # Radio/Select/Checkbox option items +│ │ │ │ ├── QuizAnswerSettings.tsx # Correct answer & points editor +│ │ │ │ └── ScreenEditor.tsx # Welcome & Thank You screen editor +│ │ │ ├── constants.tsx # FIELD_PALETTE_ITEMS definitions +│ │ │ ├── FieldEditor.tsx # Property editor drawer for fields & screens +│ │ │ ├── FieldPalette.tsx # Draggable field palette sidebar +│ │ │ ├── FormBuilder.tsx # Top-level builder container with mode toggle +│ │ │ ├── PreviewModal.tsx # Live form preview dialog +│ │ │ ├── quizUtils.ts # Quiz scoring and answer checking helpers +│ │ │ ├── types.ts # Core builder TypeScript interfaces +│ │ │ └── useFormBuilder.ts # Custom state hook for builder operations +│ │ └── common/ # Reusable atomic UI components +│ │ ├── Button.tsx # Button component with customizable colors +│ │ ├── Checkbox.tsx # Checkbox input with label +│ │ ├── DatePicker.tsx # Custom calendar dropdown picker +│ │ ├── FileUpload.tsx # Drag-and-drop file uploader (Cloudinary) +│ │ ├── Footer.tsx # App footer with copyright and legal links +│ │ ├── FormattedAnswer.tsx # Helper rendering formatted response answers +│ │ ├── GoogleSheetsModal.tsx # Modal to link/unlink Google Sheets & auto-sync +│ │ ├── icons.tsx # Shared SVG icon library (Icons.*) +│ │ ├── InputField.tsx # Text/Number/Email/Phone input (forwardRef) +│ │ ├── Modal.tsx # Reusable dialog modal wrapper +│ │ ├── RadioButton.tsx # Radio button option +│ │ ├── Rating.tsx # Star/Heart/Smile/Thumb rating component +│ │ ├── Select.tsx # Styled dropdown component (forwardRef) +│ │ ├── ShareModal.tsx # Share link dialog with copy-to-clipboard +│ │ ├── TextArea.tsx # Multiline textarea component (forwardRef) +│ │ └── TimePicker.tsx # Custom time picker component +│ ├── layouts/ # Form execution engines +│ │ ├── FlowPage.tsx # Typeform-style step-by-step questionnaire +│ │ └── SinglePage.tsx # Traditional single-page form with Quiz scoring +│ ├── pages/ # Route page components +│ │ ├── AuthCallbackPage.tsx # Handles OAuth redirect callback +│ │ ├── BuilderPage.tsx # Form builder wrapper page (/builder/:formId) +│ │ ├── DashboardPage.tsx # User dashboard (/dashboard) +│ │ ├── FormResponsesPage.tsx # Form submissions table & CSV export (/forms/:formId/responses) +│ │ ├── HomePage.tsx # Landing page (/) +│ │ ├── PrivacyPage.tsx # Privacy policy (/privacy) +│ │ ├── PublicFormPage.tsx # Public responder form view (/forms/:formId) +│ │ ├── ResponsePage.tsx # Single response inspector (/responses/:responseId) +│ │ └── TermsPage.tsx # Terms of service (/terms) +│ ├── services/ # Network services +│ │ ├── api.ts # Axios API instance and endpoints +│ │ └── apiTypes.ts # Backend request & response TypeScript types +│ ├── store/ # Global state management +│ │ ├── slices/ +│ │ │ └── authSlice.ts # Authentication state slice +│ │ └── store.ts # Redux Toolkit store setup +│ ├── utils/ # Utility helpers +│ │ ├── formatters.ts # Date & time locale formatting helpers +│ │ └── validation.ts # Form input validation logic +│ ├── App.css # Global application CSS +│ ├── App.tsx # Route configuration with React.lazy +│ ├── index.css # Tailwind CSS imports & global styles +│ └── main.tsx # App entrypoint (Redux Provider & root mount) +├── eslint.config.js # ESLint 9 flat configuration +├── package.json # NPM dependencies & scripts +├── tsconfig.app.json # Application TypeScript compiler options +├── tsconfig.json # Solution TypeScript configuration +└── vite.config.ts # Vite configuration with Tailwind & React Compiler +``` + +--- + +## 5. Domain Models & Schemas + +### 5.1 Field Types (`FieldType`) +All supported field types in `src/components/BuilderCore/shared/types.ts`: +- `"text"` — Single-line text input +- `"number"` — Numeric input +- `"email"` — Email address with validation +- `"tel"` — Telephone number input +- `"radio"` — Single-choice radio group +- `"checkbox"` — Checkbox toggle or multi-select option +- `"textarea"` — Multiline text area +- `"select"` — Dropdown selector +- `"date"` — Date picker calendar +- `"time"` — Time selector +- `"file"` — File upload input (Cloudinary integration) +- `"rating"` — Interactive rating (star, heart, smile, thumb) +- `"page_break"` — Visual section separator + +### 5.2 Form Field Configuration (`FormFieldConfig`) +```typescript +export interface FormFieldConfig { + id: string; + type: FieldType; + title: string; + subtitle?: string; + placeholder?: string; + maxLength?: number; + options?: { label: string; value: string }[]; + defaultValue?: string | boolean | number | string[]; + name?: string; + required?: boolean; + multiSelect?: boolean; + minDate?: string; + maxDate?: string; + minTime?: string; + maxTime?: string; + maxRating?: number; + ratingSymbol?: "star" | "heart" | "smile" | "thumb"; + logic?: { + rules: { + id: string; + operator: "equals" | "not_equals" | "contains" | "not_contains"; + value: string; + targetFieldId: string; // Target field ID or "SUBMIT" + }[]; + }; + correctAnswer?: string | string[]; + points?: number; +} +``` + +### 5.3 Screen Configurations +- **WelcomeScreenConfig**: `{ title: string; description: string; buttonText: string }` +- **ThankYouScreenConfig**: `{ title: string; description: string; emoji: string }` + +--- + +## 6. API Client Integration (`src/services/api.ts`) + +The Axios client communicates with the backend with `withCredentials: true` enabled for cookie session authentication. + +| Method | Endpoint | Description | +| :--- | :--- | :--- | +| `createForm(data)` | `POST /api/forms` | Create a new draft form | +| `getForms(userId)` | `GET /api/users/:userId/forms` | Fetch all forms owned by user | +| `getForm(id)` | `GET /api/forms/:id` | Fetch specific form details | +| `getPublicForm(shareUrl)` | `GET /api/forms/share/:shareUrl` | Fetch published form for responder | +| `updateForm(id, data)` | `PUT /api/forms/:id` | Update schema, settings, name, or description | +| `deleteForm(id)` | `DELETE /api/forms/:id` | Delete form | +| `publishForm(id)` | `POST /api/forms/:id/publish` | Publish form and generate share URL | +| `unpublishForm(id)` | `POST /api/forms/:id/unpublish` | Unpublish form | +| `submitResponse(formId, data, meta)` | `POST /api/forms/:formId/responses` | Submit answers to a published form | +| `uploadFile(formId, file)` | `POST /api/forms/:formId/upload` | Upload file (multipart) to Cloudinary | +| `getFormResponses(formId)` | `GET /api/forms/:formId/responses` | List all responses for a form | +| `getResponse(id)` | `GET /api/responses/:id` | Get single response details | +| `deleteResponse(id)` | `DELETE /api/responses/:id` | Delete single response | +| `createAndLinkGoogleSheet(formId, data)` | `POST /api/forms/:formId/sheets/create` | Create & link Google Spreadsheet | +| `unlinkGoogleSheet(formId)` | `DELETE /api/forms/:formId/sheets/link` | Unlink connected Google Sheet | +| `getCurrentUser()` | `GET /api/auth/me` | Fetch authenticated user profile | +| `getGoogleAuthUrl()` | `GET /api/auth/google` | OAuth redirect URL helper | + +--- + +## 7. Key Frontend Guidelines & Conventions + +1. **React 19 & Compiler Optimization**: Write idiomatic React code. Avoid unnecessary manual memoization (`useMemo`, `useCallback`) unless required for third-party library stability. +2. **Forward Refs**: Form input components in `src/components/common/` (`InputField`, `TextArea`, `Select`) must use `forwardRef` to support keyboard focus and scrolling. +3. **Reusable Primitives**: Always reuse components in `src/components/common/` rather than re-creating ad-hoc form inputs. +4. **TailwindCSS v4 Utility Usage**: Use standard Tailwind utilities. Maintain responsive design for mobile and desktop screens. +5. **State Management**: + - Form builder local state is managed through `useFormBuilder.ts`. + - Global user session state is stored in Redux (`authSlice.ts`). +6. **Environment Variables**: + - `VITE_API_URL`: Backend server base URL (e.g. `http://localhost:1323`). diff --git a/package-lock.json b/package-lock.json index 9359476..0d45dfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,23 @@ { "name": "formify", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "formify", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@mapbox/search-js-react": "^1.5.1", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.4", "eslint-plugin-react": "^7.37.5", - "mapbox-gl": "^3.18.1", "papaparse": "^5.5.3", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-map-gl": "^8.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.13.0" }, @@ -938,21 +935,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", - "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==", - "license": "MIT" - }, - "node_modules/@floating-ui/dom": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", - "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^0.7.3" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1046,138 +1028,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@mapbox/mapbox-gl-supported": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", - "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==", - "license": "BSD-3-Clause" - }, - "node_modules/@mapbox/point-geometry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", - "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", - "license": "ISC" - }, - "node_modules/@mapbox/search-js-core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mapbox/search-js-core/-/search-js-core-1.5.1.tgz", - "integrity": "sha512-aXv9LoN8FvTkMhOOWxIEwFsh8aZFMid1xZfGD71nxWGEWG+TufjCdMCExOI/q7XI49xR1+kEc3ONI21X484uow==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@types/geojson": "^7946.0.8" - }, - "engines": { - "node": ">=12.20.1" - } - }, - "node_modules/@mapbox/search-js-react": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mapbox/search-js-react/-/search-js-react-1.5.1.tgz", - "integrity": "sha512-+5t7WkAOhpVvqWF9A+BZYEjkFaZ6i5lvLwkXqV5a/UrpsHCCDpcg2Ii5TKvtCJ1GW7VVnCocDH2aF49tIvtM2g==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@mapbox/search-js-core": "^1.5.1", - "@mapbox/search-js-web": "^1.5.1", - "@types/geojson": "^7946.0.8" - }, - "engines": { - "node": ">=12.20.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@mapbox/search-js-web": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mapbox/search-js-web/-/search-js-web-1.5.1.tgz", - "integrity": "sha512-hhPbcbamv8Mzitzwgzzxac7GA1TUW5S4aKGDdDkOgbgTyoudunSm4Jem364yxv4bJi4dPFk+WSyhNdF1m5VCkw==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@floating-ui/dom": "^0.5.2", - "@mapbox/search-js-core": "^1.5.1", - "@mapbox/sphericalmercator": "^1.2.0", - "focus-trap": "^6.7.3", - "no-scroll": "^2.1.1", - "subtag": "^0.5.0" - }, - "engines": { - "node": ">=12.20.1" - }, - "peerDependencies": { - "mapbox-gl": ">=2.7.0" - } - }, - "node_modules/@mapbox/sphericalmercator": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.2.0.tgz", - "integrity": "sha512-ZTOuuwGuMOJN+HEmG/68bSEw15HHaMWmQ5gdTsWdWsjDe56K1kGvLOK6bOSC8gWgIvEO0w6un/2Gvv1q5hJSkQ==", - "bin": { - "bbox": "bin/bbox.js", - "to4326": "bin/to4326.js", - "to900913": "bin/to900913.js", - "xyz": "bin/xyz.js" - } - }, - "node_modules/@mapbox/tiny-sdf": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", - "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/vector-tile": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", - "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/point-geometry": "~1.1.0", - "@types/geojson": "^7946.0.16", - "pbf": "^4.0.1" - } - }, - "node_modules/@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "19.3.3", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", - "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", - "license": "ISC", - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", - "@mapbox/unitbezier": "^0.0.1", - "json-stringify-pretty-compact": "^3.0.0", - "minimist": "^1.2.8", - "rw": "^1.3.3", - "sort-object": "^3.0.3" - }, - "bin": { - "gl-style-format": "dist/gl-style-format.mjs", - "gl-style-migrate": "dist/gl-style-migrate.mjs", - "gl-style-validate": "dist/gl-style-validate.mjs" - } - }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -1856,33 +1706,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/geojson-vt": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", - "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, - "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", @@ -1903,12 +1732,6 @@ "@types/node": "*" } }, - "node_modules/@types/pbf": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", - "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", @@ -1929,15 +1752,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/supercluster": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", - "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -2213,41 +2027,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vis.gl/react-mapbox": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.1.0.tgz", - "integrity": "sha512-FwvH822oxEjWYOr+pP2L8hpv+7cZB2UsQbHHHT0ryrkvvqzmTgt7qHDhamv0EobKw86e1I+B4ojENdJ5G5BkyQ==", - "license": "MIT", - "peerDependencies": { - "mapbox-gl": ">=3.5.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "mapbox-gl": { - "optional": true - } - } - }, - "node_modules/@vis.gl/react-maplibre": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.1.0.tgz", - "integrity": "sha512-PkAK/gp3mUfhCLhUuc+4gc3PN9zCtVGxTF2hB6R5R5yYUw+hdg84OZ770U5MU4tPMTCG6fbduExuIW6RRKN6qQ==", - "license": "MIT", - "dependencies": { - "@maplibre/maplibre-gl-style-spec": "^19.2.1" - }, - "peerDependencies": { - "maplibre-gl": ">=4.0.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "maplibre-gl": { - "optional": true - } - } - }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -2327,15 +2106,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -2467,15 +2237,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2587,25 +2348,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bytewise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", - "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", - "license": "MIT", - "dependencies": { - "bytewise-core": "^1.2.2", - "typewise": "^1.0.3" - } - }, - "node_modules/bytewise-core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", - "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", - "license": "MIT", - "dependencies": { - "typewise-core": "^1.2" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2699,12 +2441,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/cheap-ruler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", - "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", - "license": "ISC" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2775,12 +2511,6 @@ "node": ">= 8" } }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2940,12 +2670,6 @@ "node": ">= 0.4" } }, - "node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" - }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -3407,18 +3131,6 @@ "node": ">=0.10.0" } }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3501,15 +3213,6 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "license": "ISC" }, - "node_modules/focus-trap": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz", - "integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==", - "license": "MIT", - "dependencies": { - "tabbable": "^5.3.3" - } - }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -3632,12 +3335,6 @@ "node": ">=6.9.0" } }, - "node_modules/geojson-vt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "license": "ISC" - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3692,21 +3389,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gl-matrix": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", - "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", - "license": "MIT" - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3766,12 +3448,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/grid-index": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", - "license": "ISC" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4061,15 +3737,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4165,18 +3832,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4325,15 +3980,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -4409,12 +4055,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, - "node_modules/json-stringify-pretty-compact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4443,12 +4083,6 @@ "node": ">=4.0" } }, - "node_modules/kdbush": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", - "license": "ISC" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4772,57 +4406,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/mapbox-gl": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.18.1.tgz", - "integrity": "sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw==", - "license": "SEE LICENSE IN LICENSE.txt", - "workspaces": [ - "src/style-spec", - "test/build/vite", - "test/build/webpack", - "test/build/typings" - ], - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^3.0.0", - "@mapbox/point-geometry": "^1.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^2.0.4", - "@mapbox/whoots-js": "^3.1.0", - "@types/geojson": "^7946.0.16", - "@types/geojson-vt": "^3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "cheap-ruler": "^4.0.0", - "csscolorparser": "~1.0.3", - "earcut": "^3.0.1", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.4", - "grid-index": "^1.1.0", - "kdbush": "^4.0.2", - "martinez-polygon-clipping": "^0.8.1", - "murmurhash-js": "^1.0.0", - "pbf": "^4.0.1", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0" - } - }, - "node_modules/martinez-polygon-clipping": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.8.1.tgz", - "integrity": "sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==", - "license": "MIT", - "dependencies": { - "robust-predicates": "^2.0.4", - "splaytree": "^0.1.4", - "tinyqueue": "3.0.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4865,27 +4448,12 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/murmurhash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4910,12 +4478,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, - "node_modules/no-scroll": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/no-scroll/-/no-scroll-2.1.1.tgz", - "integrity": "sha512-YTzGAJOo/B6hkodeT5SKKHpOhAzjMfkUCCXjLJwjWk2F4/InIg+HbdH9kmT7bKpleDuqLZDTRy2OdNtAj0IVyQ==", - "license": "MIT" - }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5148,18 +4710,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/pbf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", - "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", - "license": "BSD-3-Clause", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5215,12 +4765,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/potpack": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", - "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", - "license": "ISC" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5241,12 +4785,6 @@ "react-is": "^16.13.1" } }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", - "license": "MIT" - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5262,12 +4800,6 @@ "node": ">=6" } }, - "node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" - }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -5295,30 +4827,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/react-map-gl": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.0.tgz", - "integrity": "sha512-vDx/QXR3Tb+8/ap/z6gdMjJQ8ZEyaZf6+uMSPz7jhWF5VZeIsKsGfPvwHVPPwGF43Ryn+YR4bd09uEFNR5OPdg==", - "license": "MIT", - "dependencies": { - "@vis.gl/react-mapbox": "8.1.0", - "@vis.gl/react-maplibre": "8.1.0" - }, - "peerDependencies": { - "mapbox-gl": ">=1.13.0", - "maplibre-gl": ">=1.13.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "mapbox-gl": { - "optional": true - }, - "maplibre-gl": { - "optional": true - } - } - }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -5485,21 +4993,6 @@ "node": ">=4" } }, - "node_modules/resolve-protobuf-schema": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "license": "MIT", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, - "node_modules/robust-predicates": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", - "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==", - "license": "Unlicense" - }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", @@ -5544,12 +5037,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -5669,21 +5156,6 @@ "node": ">= 0.4" } }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5777,41 +5249,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sort-asc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", - "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-desc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", - "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sort-object": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", - "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", - "license": "MIT", - "dependencies": { - "bytewise": "^1.1.0", - "get-value": "^2.0.2", - "is-extendable": "^0.1.1", - "sort-asc": "^0.2.0", - "sort-desc": "^0.2.0", - "union-value": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5821,49 +5258,6 @@ "node": ">=0.10.0" } }, - "node_modules/splaytree": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz", - "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==", - "license": "MIT" - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5982,21 +5376,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/subtag": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz", - "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==", - "license": "ISC" - }, - "node_modules/supercluster": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", - "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", - "license": "ISC", - "dependencies": { - "kdbush": "^4.0.2" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6021,12 +5400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tabbable": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", - "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -6062,12 +5435,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "license": "ISC" - }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -6211,21 +5578,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/typewise": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", - "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", - "license": "MIT", - "dependencies": { - "typewise-core": "^1.2.0" - } - }, - "node_modules/typewise-core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", - "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==", - "license": "MIT" - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6251,21 +5603,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/package.json b/package.json index d7395fb..2b3df90 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,13 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@mapbox/search-js-react": "^1.5.1", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.4", "eslint-plugin-react": "^7.37.5", - "mapbox-gl": "^3.18.1", "papaparse": "^5.5.3", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-map-gl": "^8.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.13.0" }, diff --git a/src/App.tsx b/src/App.tsx index 7625488..b708667 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { ToastProvider } from "./components/common/Toast"; import "./App.css"; const HomePage = lazy(() => import("./pages/HomePage")); @@ -15,22 +16,24 @@ const PublicFormPage = lazy(() => import("./pages/PublicFormPage")); function App() { return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - - + + + + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + + + ); } diff --git a/src/components/BuilderCore/Flow/FlowFormBuilder.tsx b/src/components/BuilderCore/Flow/FlowFormBuilder.tsx index f27d93a..f1db20a 100644 --- a/src/components/BuilderCore/Flow/FlowFormBuilder.tsx +++ b/src/components/BuilderCore/Flow/FlowFormBuilder.tsx @@ -26,6 +26,7 @@ interface FlowFormBuilderProps { initialThankYou?: ThankYouScreenConfig; initialIsPublished?: boolean; initialShareUrl?: string | null; + initialIsQuiz?: boolean; } export default function FlowFormBuilder({ @@ -45,6 +46,7 @@ export default function FlowFormBuilder({ }, initialIsPublished = false, initialShareUrl, + initialIsQuiz = false, }: FlowFormBuilderProps) { const navigate = useNavigate(); const [welcomeScreen, setWelcomeScreen] = @@ -52,6 +54,7 @@ export default function FlowFormBuilder({ const [thankYouScreen, setThankYouScreen] = useState(initialThankYou); + const [isQuiz, setIsQuiz] = useState(initialIsQuiz); const onSave = useCallback( async (fields: FormFieldConfig[]) => { @@ -61,6 +64,7 @@ export default function FlowFormBuilder({ fields, welcomeScreen, thankYouScreen, + isQuiz, }; await updateForm(formId, { name: welcomeScreen.title, @@ -69,7 +73,7 @@ export default function FlowFormBuilder({ }); return true; }, - [formId, welcomeScreen, thankYouScreen], + [formId, welcomeScreen, thankYouScreen, isQuiz], ); const { @@ -255,6 +259,8 @@ export default function FlowFormBuilder({ formMetadata={{ title: "", description: "", banner: "" }} onUpdateMetadata={() => {}} allFields={fields} + isQuiz={isQuiz} + onToggleQuiz={setIsQuiz} /> @@ -268,6 +274,7 @@ export default function FlowFormBuilder({ formTitle={welcomeScreen.title} formDescription={welcomeScreen.description} formBanner="" + isQuiz={isQuiz} /> {formId && ( ) : ( {}} + isQuiz={isQuiz} /> ) : (
diff --git a/src/components/BuilderCore/shared/quizUtils.ts b/src/components/BuilderCore/shared/quizUtils.ts index 6e660f9..6c04183 100644 --- a/src/components/BuilderCore/shared/quizUtils.ts +++ b/src/components/BuilderCore/shared/quizUtils.ts @@ -1,3 +1,5 @@ +import type { FieldOption } from "./types"; + const QUIZ_SUPPORTED_TYPES = ["radio", "select"] as const; type QuizSupportedType = (typeof QUIZ_SUPPORTED_TYPES)[number]; @@ -9,17 +11,51 @@ export function arraysEqual(a: string[], b: string[]): boolean { if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); - return sortedA.every((v, i) => v === sortedB[i]); + return sortedA.every( + (v, i) => v.trim().toLowerCase() === sortedB[i].trim().toLowerCase(), + ); +} + +function normalizeOptionValue(val: unknown, options?: FieldOption[]): string { + const str = String(val ?? "").trim(); + if (!options || options.length === 0) return str; + const found = options.find( + (o) => + o.label.trim().toLowerCase() === str.toLowerCase() || + o.value.trim().toLowerCase() === str.toLowerCase(), + ); + return found ? found.value : str; } export function isAnswerCorrect( userAnswer: unknown, correctAnswer: string | string[] | undefined, + options?: FieldOption[], ): boolean { - if (correctAnswer === undefined) return false; + if ( + correctAnswer === undefined || + correctAnswer === null || + correctAnswer === "" + ) { + return false; + } + if (Array.isArray(correctAnswer)) { - const user = Array.isArray(userAnswer) ? userAnswer : []; - return arraysEqual(user, correctAnswer); + const rawUserList = Array.isArray(userAnswer) + ? userAnswer + : userAnswer !== undefined && userAnswer !== null && userAnswer !== "" + ? [userAnswer] + : []; + const normalizedUser = rawUserList.map((v) => + normalizeOptionValue(v, options), + ); + const normalizedCorrect = correctAnswer.map((v) => + normalizeOptionValue(v, options), + ); + return arraysEqual(normalizedUser, normalizedCorrect); } - return userAnswer === correctAnswer; + + const normUser = normalizeOptionValue(userAnswer, options); + const normCorrect = normalizeOptionValue(correctAnswer, options); + return normUser.toLowerCase() === normCorrect.toLowerCase(); } diff --git a/src/components/common/FileUpload.tsx b/src/components/common/FileUpload.tsx index 86137a5..adadddd 100644 --- a/src/components/common/FileUpload.tsx +++ b/src/components/common/FileUpload.tsx @@ -1,6 +1,7 @@ -import { useRef } from "react"; +import { useRef, useState, useId } from "react"; interface FileUploadProps { + id?: string; label: string; name?: string; accept?: string; @@ -10,9 +11,11 @@ interface FileUploadProps { isUploading?: boolean; subtitle?: string; value?: string; + maxSize?: number; } function FileUpload({ + id, label, name, accept, @@ -22,8 +25,13 @@ function FileUpload({ isUploading = false, subtitle, value = "", + maxSize = 10 * 1024 * 1024, }: FileUploadProps) { const inputRef = useRef(null); + const [validationError, setValidationError] = useState(null); + const generatedId = useId(); + const inputId = id || (name ? `field-${name}` : generatedId); + const errorId = `${inputId}-error`; const handleClick = () => { if (!disabled && inputRef.current) { @@ -31,10 +39,28 @@ function FileUpload({ } }; + const handleChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (file.size > maxSize) { + setValidationError( + `File size exceeds ${(maxSize / (1024 * 1024)).toFixed(0)}MB limit.`, + ); + if (inputRef.current) inputRef.current.value = ""; + return; + } + setValidationError(null); + } + onChange?.(e); + }; + return (
{label && ( -
); } diff --git a/src/components/common/InputField.tsx b/src/components/common/InputField.tsx index 3094815..28588a9 100644 --- a/src/components/common/InputField.tsx +++ b/src/components/common/InputField.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react"; +import { forwardRef, useId } from "react"; interface InputFieldProps { title: string; @@ -6,11 +6,24 @@ interface InputFieldProps { placeholder?: string; maxLength?: number; name?: string; + id?: string; value?: string | number; onChange?: (e: React.ChangeEvent) => void; autoFocus?: boolean; subtitle?: string; disabled?: boolean; + required?: boolean; + autoComplete?: string; + inputMode?: + | "search" + | "text" + | "email" + | "tel" + | "url" + | "numeric" + | "decimal" + | "none"; + error?: string; } const InputField = forwardRef( @@ -21,32 +34,88 @@ const InputField = forwardRef( placeholder, maxLength, name, + id, value, onChange, autoFocus, subtitle, disabled, + required, + autoComplete, + inputMode, + error, }, ref, ) => { + const generatedId = useId(); + const inputId = id || name || generatedId; + const descId = subtitle ? `${inputId}-desc` : undefined; + const errorId = error ? `${inputId}-error` : undefined; + return (
-
); }, diff --git a/src/components/common/Modal.tsx b/src/components/common/Modal.tsx index 1a6b94e..128a2a2 100644 --- a/src/components/common/Modal.tsx +++ b/src/components/common/Modal.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { createPortal } from "react-dom"; +import React, { useEffect, useRef, useId } from "react"; interface ModalProps { isOpen: boolean; @@ -9,20 +8,65 @@ interface ModalProps { } const Modal: React.FC = ({ isOpen, onClose, title, children }) => { - if (!isOpen) return null; - - return createPortal( -
-
-
-
-

{title}

+ const dialogRef = useRef(null); + const titleId = useId(); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (isOpen) { + if (!dialog.open) { + dialog.showModal(); + } + } else { + if (dialog.open) { + dialog.close(); + } + } + }, [isOpen]); + + const handleBackdropClick = (e: React.MouseEvent) => { + const dialog = dialogRef.current; + if (!dialog || e.target !== dialog) return; + + const rect = dialog.getBoundingClientRect(); + const isInside = + rect.top <= e.clientY && + e.clientY <= rect.top + rect.height && + rect.left <= e.clientX && + e.clientX <= rect.left + rect.width; + + if (!isInside) { + onClose(); + } + }; + + const handleCancel = (e: React.SyntheticEvent) => { + e.preventDefault(); + onClose(); + }; + + return ( + +
+
+ {title && ( +

+ {title} +

+ )}
-
, - document.body, +
); }; diff --git a/src/components/common/Select.tsx b/src/components/common/Select.tsx index be00f17..10d0cef 100644 --- a/src/components/common/Select.tsx +++ b/src/components/common/Select.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react"; +import { forwardRef, useId } from "react"; interface Option { label: string; @@ -9,11 +9,14 @@ interface SelectProps { title: string; options: Option[]; name?: string; + id?: string; value?: string; onChange?: (e: React.ChangeEvent) => void; placeholder?: string; subtitle?: string; disabled?: boolean; + required?: boolean; + error?: string; } const Select = forwardRef( @@ -22,28 +25,60 @@ const Select = forwardRef( title, options, name, + id, value, onChange, placeholder = "Select an option", subtitle, disabled, + required, + error, }, ref, ) => { + const generatedId = useId(); + const selectId = id || name || generatedId; + const descId = subtitle ? `${selectId}-desc` : undefined; + const errorId = error ? `${selectId}-error` : undefined; + return (
-