From 07ac6c422f5bc1f6770cce7de2e772608bca6cc7 Mon Sep 17 00:00:00 2001 From: Vitrixxl Date: Tue, 14 Oct 2025 16:11:46 +0200 Subject: [PATCH] init --- backend/.env | 7 + backend/api_summary.json | 486 +++++++ backend/app.py | 723 +++++++++ backend/requirements.txt | 4 + backend/search_schema.json | 353 +++++ frontend/.gitignore | 24 + frontend/README.md | 75 + frontend/api-schema.json | 1289 +++++++++++++++++ frontend/bun.lock | 680 +++++++++ frontend/components.json | 22 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/package.json | 47 + frontend/public/vite.svg | 1 + frontend/scraper-schema.json | 353 +++++ frontend/src/App.tsx | 91 ++ frontend/src/ScraperPage.tsx | 26 + frontend/src/assets/react.svg | 1 + frontend/src/components/json-fields.tsx | 392 +++++ .../schema-builder/SchemaAwareJsonField.tsx | 129 ++ .../schema-builder/SchemaBuilder.tsx | 43 + .../schema-builder/SchemaFieldLabel.tsx | 25 + .../schema-builder/SchemaFieldRenderer.tsx | 342 +++++ .../src/components/schema-builder/index.ts | 3 + .../schema-builder/useSchemaValidation.ts | 181 +++ frontend/src/components/ui/button.tsx | 60 + frontend/src/components/ui/card.tsx | 92 ++ frontend/src/components/ui/checkbox.tsx | 32 + frontend/src/components/ui/input.tsx | 21 + frontend/src/components/ui/select.tsx | 185 +++ frontend/src/components/ui/tabs.tsx | 64 + frontend/src/components/ui/textarea.tsx | 19 + .../src/features/profiles/ProfilesTab.tsx | 464 ++++++ .../src/features/scrapers/ScrapersTab.tsx | 603 ++++++++ frontend/src/index.css | 120 ++ frontend/src/lib/api.ts | 148 ++ frontend/src/lib/utils.ts | 6 + frontend/src/main.tsx | 16 + frontend/src/schemas/llm-json-guide.md | 321 ++++ frontend/src/schemas/loader.ts | 21 + frontend/src/schemas/normalize.ts | 183 +++ frontend/src/schemas/profile-schema.ts | 375 +++++ frontend/src/schemas/types.ts | 58 + frontend/src/schemas/utils.ts | 136 ++ frontend/tsconfig.app.json | 32 + frontend/tsconfig.json | 13 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 14 + 48 files changed, 8342 insertions(+) create mode 100644 backend/.env create mode 100644 backend/api_summary.json create mode 100644 backend/app.py create mode 100644 backend/requirements.txt create mode 100644 backend/search_schema.json create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/api-schema.json create mode 100644 frontend/bun.lock create mode 100644 frontend/components.json create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/scraper-schema.json create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/ScraperPage.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/json-fields.tsx create mode 100644 frontend/src/components/schema-builder/SchemaAwareJsonField.tsx create mode 100644 frontend/src/components/schema-builder/SchemaBuilder.tsx create mode 100644 frontend/src/components/schema-builder/SchemaFieldLabel.tsx create mode 100644 frontend/src/components/schema-builder/SchemaFieldRenderer.tsx create mode 100644 frontend/src/components/schema-builder/index.ts create mode 100644 frontend/src/components/schema-builder/useSchemaValidation.ts create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/components/ui/textarea.tsx create mode 100644 frontend/src/features/profiles/ProfilesTab.tsx create mode 100644 frontend/src/features/scrapers/ScrapersTab.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/schemas/llm-json-guide.md create mode 100644 frontend/src/schemas/loader.ts create mode 100644 frontend/src/schemas/normalize.ts create mode 100644 frontend/src/schemas/profile-schema.ts create mode 100644 frontend/src/schemas/types.ts create mode 100644 frontend/src/schemas/utils.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..def8a16 --- /dev/null +++ b/backend/.env @@ -0,0 +1,7 @@ +DB_NAME=immonator_test +DB_PORT=5433 +DB_HOST=34.135.132.141 +DB_USERNAME=django +DB_PASSWORD=T17HE59a7Xlu2lBgPUQ9rdCPTe4159UNedGhcOvrTu8= +API_TOKEN=B0uKZQN+qIsLc0yNR/t9xCOkgP6Keg0oarLUiZkO2Mo= + diff --git a/backend/api_summary.json b/backend/api_summary.json new file mode 100644 index 0000000..f82338d --- /dev/null +++ b/backend/api_summary.json @@ -0,0 +1,486 @@ +{ + "api_meta": { + "name": "Managinator Backend API", + "overview": "Bearer-protected Flask service for managing investment profiles, users, and scrapers.", + "base_url_example": "http://localhost:8000", + "default_headers": { + "Content-Type": "application/json" + }, + "cors": { + "allow_origin": "*", + "allow_credentials": true, + "notes": "Server responds to OPTIONS preflight without authentication and accepts credentialed requests from any origin." + } + }, + "authentication": { + "strategy": "bearer_token", + "header": "Authorization", + "format": "Bearer ", + "env_variable": "API_TOKEN", + "notes": [ + "Every request (except OPTIONS and Flask static assets) must carry the bearer token in the Authorization header.", + "There is no login endpoint; the expected token value is configured server-side via the API_TOKEN environment variable.", + "Missing, malformed, or incorrect bearer tokens return HTTP 401." + ] + }, + "conventions": { + "datetime_format": "Use ISO 8601 strings with timezone. Responses normalize to UTC with trailing Z.", + "boolean_input": "Send JSON booleans true/false. The backend also accepts 1/0 and common string equivalents, but clients should prefer booleans.", + "integer_input": "Integer fields accept JSON numbers. Numeric strings will be coerced server-side but should be avoided.", + "null_handling": "Fields documented as nullable accept JSON null and will persist as SQL NULL.", + "error_responses": { + "typical_status_codes": [400, 401, 404, 409, 503], + "body_format": "Flask default HTTP error responses; body may be plain text or HTML containing the description. Rely on HTTP status and message." + } + }, + "schemas": { + "Profile": { + "description": "Investment profile descriptor persisted in users_investmentprofile.", + "fields": [ + {"name": "profile_id", "type": "uuid", "nullable": false, "read_only": false, "description": "Primary key. Optional on create; autogenerated when omitted."}, + {"name": "name", "type": "string", "nullable": false, "read_only": false, "description": "Human-readable profile label."}, + {"name": "description", "type": "string", "nullable": true, "read_only": false, "description": "Optional free-form description. Empty string preserved."}, + {"name": "criteria", "type": "json", "nullable": false, "read_only": false, "description": "Arbitrary JSON object/array describing selection rules."}, + {"name": "created_at", "type": "datetime", "nullable": false, "read_only": false, "description": "Creation timestamp. Defaults to current UTC when omitted."}, + {"name": "is_active", "type": "boolean", "nullable": false, "read_only": false, "description": "Activation flag. Defaults to true on create."} + ] + }, + "User": { + "description": "Auth user record persisted in auth_user.", + "fields": [ + {"name": "id", "type": "integer", "nullable": false, "read_only": true, "description": "Database primary key."}, + {"name": "username", "type": "string", "nullable": false, "read_only": false, "description": "Unique login name."}, + {"name": "password", "type": "string", "nullable": false, "read_only": false, "description": "Raw password value inserted as-is. Not returned in responses."}, + {"name": "first_name", "type": "string", "nullable": false, "read_only": false, "description": "User first name."}, + {"name": "last_name", "type": "string", "nullable": false, "read_only": false, "description": "User last name."}, + {"name": "email", "type": "string", "nullable": false, "read_only": false, "description": "User email address."}, + {"name": "is_superuser", "type": "boolean", "nullable": false, "read_only": false, "description": "Administrative superuser flag."}, + {"name": "is_staff", "type": "boolean", "nullable": false, "read_only": false, "description": "Staff membership flag."}, + {"name": "is_active", "type": "boolean", "nullable": false, "read_only": false, "description": "Account enabled flag."}, + {"name": "date_joined", "type": "datetime", "nullable": false, "read_only": false, "description": "Account creation timestamp. Defaults to current UTC when omitted."}, + {"name": "last_login", "type": "datetime", "nullable": true, "read_only": false, "description": "Most recent login timestamp."} + ] + }, + "Scraper": { + "description": "Scraper configuration persisted in scraper table.", + "fields": [ + {"name": "id", "type": "string", "nullable": false, "read_only": false, "description": "Primary key identifier."}, + {"name": "params", "type": "string", "nullable": true, "read_only": false, "description": "Serialized scraper parameters."}, + {"name": "last_seen_days", "type": "integer", "nullable": true, "read_only": false, "description": "Number of days since listings were last seen."}, + {"name": "first_seen_days", "type": "integer", "nullable": true, "read_only": false, "description": "Number of days since listings were first seen."}, + {"name": "frequency", "type": "string", "nullable": true, "read_only": false, "description": "Cron or human-readable frequency descriptor."}, + {"name": "task_name", "type": "string", "nullable": true, "read_only": false, "description": "Associated task identifier."}, + {"name": "enabled", "type": "integer", "nullable": true, "read_only": false, "description": "1/0 flag for activation. Stored as integer."}, + {"name": "property_types", "type": "string", "nullable": true, "read_only": false, "description": "Comma-separated property type filters."}, + {"name": "page_size", "type": "integer", "nullable": true, "read_only": false, "description": "Listings fetched per page."}, + {"name": "max_pages", "type": "integer", "nullable": true, "read_only": false, "description": "Maximum number of pages to crawl."}, + {"name": "enrich_llm", "type": "integer", "nullable": true, "read_only": false, "description": "Toggle for LLM enrichment (1 enabled, 0 disabled)."}, + {"name": "only_match", "type": "integer", "nullable": true, "read_only": false, "description": "Toggle for strict matching (1 enabled, 0 disabled)."} + ] + } + }, + "endpoints": [ + { + "operation_id": "listProfiles", + "method": "GET", + "path": "/profiles", + "authenticated": true, + "description": "List all investment profiles ordered by most recent creation.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + } + }, + "responses": { + "200": {"description": "Array of profiles.", "body": {"type": "array", "items": {"$ref": "#/schemas/Profile"}}}, + "401": {"description": "Missing or invalid bearer token."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "getProfile", + "method": "GET", + "path": "/profiles/", + "authenticated": true, + "description": "Retrieve a single profile by UUID.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "profile_id", "type": "uuid", "required": true, "description": "Profile identifier."} + ] + }, + "responses": { + "200": {"description": "Profile payload.", "body": {"$ref": "#/schemas/Profile"}}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Profile not found."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "createProfile", + "method": "POST", + "path": "/profiles", + "authenticated": true, + "description": "Create a new investment profile.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "body": { + "type": "object", + "required_fields": ["name", "criteria"], + "fields": { + "profile_id": {"type": "uuid", "nullable": false, "description": "Optional explicit UUID. Auto-generated when omitted."}, + "name": {"type": "string", "nullable": false, "description": "Profile name."}, + "description": {"type": "string", "nullable": true, "description": "Optional description. Use null to clear."}, + "criteria": {"type": "json", "nullable": false, "description": "Required JSON criteria payload."}, + "created_at": {"type": "datetime", "nullable": false, "description": "Optional creation timestamp. Defaults to now (UTC)."}, + "is_active": {"type": "boolean", "nullable": false, "description": "Defaults to true."} + } + } + }, + "responses": { + "201": {"description": "Profile created.", "body": {"$ref": "#/schemas/Profile"}}, + "400": {"description": "Validation failure (missing criteria, invalid field formats, or empty request)."}, + "401": {"description": "Missing or invalid bearer token."}, + "409": {"description": "Constraint violation (e.g., duplicate profile_id)."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "updateProfile", + "method": "PUT", + "path": "/profiles/", + "authenticated": true, + "description": "Update mutable fields on an existing profile.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "profile_id", "type": "uuid", "required": true, "description": "Profile identifier."} + ], + "body": { + "type": "object", + "allowed_fields": ["name", "description", "criteria", "created_at", "is_active"], + "min_fields": 1, + "fields": { + "name": {"type": "string", "nullable": false, "description": "New profile name."}, + "description": {"type": "string", "nullable": true, "description": "Use null to clear description."}, + "criteria": {"type": "json", "nullable": false, "description": "Updated criteria JSON."}, + "created_at": {"type": "datetime", "nullable": false, "description": "New creation timestamp."}, + "is_active": {"type": "boolean", "nullable": false, "description": "Activation flag."} + } + } + }, + "responses": { + "200": {"description": "Profile updated.", "body": {"$ref": "#/schemas/Profile"}}, + "400": {"description": "No updatable fields provided or invalid data."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Profile not found."}, + "409": {"description": "Constraint violation."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "deleteProfile", + "method": "DELETE", + "path": "/profiles/", + "authenticated": true, + "description": "Remove a profile by UUID.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "profile_id", "type": "uuid", "required": true, "description": "Profile identifier."} + ] + }, + "responses": { + "204": {"description": "Profile deleted. Empty body."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Profile not found."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "listUsers", + "method": "GET", + "path": "/users", + "authenticated": true, + "description": "List all users ordered by ascending id.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + } + }, + "responses": { + "200": {"description": "Array of users.", "body": {"type": "array", "items": {"$ref": "#/schemas/User"}}}, + "401": {"description": "Missing or invalid bearer token."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "getUser", + "method": "GET", + "path": "/users/", + "authenticated": true, + "description": "Retrieve a user by numeric id.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "user_id", "type": "integer", "required": true, "description": "User primary key."} + ] + }, + "responses": { + "200": {"description": "User payload.", "body": {"$ref": "#/schemas/User"}}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "User not found."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "createUser", + "method": "POST", + "path": "/users", + "authenticated": true, + "description": "Create a new user record.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "body": { + "type": "object", + "required_fields": ["password", "username", "first_name", "last_name", "email"], + "fields": { + "password": {"type": "string", "nullable": false, "description": "Password to store. No hashing performed."}, + "username": {"type": "string", "nullable": false, "description": "Unique username."}, + "first_name": {"type": "string", "nullable": false, "description": "First name."}, + "last_name": {"type": "string", "nullable": false, "description": "Last name."}, + "email": {"type": "string", "nullable": false, "description": "Email address."}, + "is_superuser": {"type": "boolean", "nullable": false, "description": "Defaults to false when omitted."}, + "is_staff": {"type": "boolean", "nullable": false, "description": "Defaults to false when omitted."}, + "is_active": {"type": "boolean", "nullable": false, "description": "Defaults to true when omitted."}, + "date_joined": {"type": "datetime", "nullable": true, "description": "Optional. Defaults to current UTC when omitted or null."}, + "last_login": {"type": "datetime", "nullable": true, "description": "Optional last login timestamp."} + } + } + }, + "responses": { + "201": {"description": "User created.", "body": {"$ref": "#/schemas/User"}}, + "400": {"description": "Validation failure."}, + "401": {"description": "Missing or invalid bearer token."}, + "409": {"description": "Constraint violation (e.g., duplicate username or email)."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "updateUser", + "method": "PUT", + "path": "/users/", + "authenticated": true, + "description": "Update mutable fields on a user.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "user_id", "type": "integer", "required": true, "description": "User primary key."} + ], + "body": { + "type": "object", + "allowed_fields": ["password", "username", "first_name", "last_name", "email", "is_superuser", "is_staff", "is_active", "date_joined", "last_login"], + "min_fields": 1, + "fields": { + "password": {"type": "string", "nullable": false, "description": "New raw password."}, + "username": {"type": "string", "nullable": false, "description": "New username."}, + "first_name": {"type": "string", "nullable": false, "description": "New first name."}, + "last_name": {"type": "string", "nullable": false, "description": "New last name."}, + "email": {"type": "string", "nullable": false, "description": "New email."}, + "is_superuser": {"type": "boolean", "nullable": false, "description": "Updated admin flag."}, + "is_staff": {"type": "boolean", "nullable": false, "description": "Updated staff flag."}, + "is_active": {"type": "boolean", "nullable": false, "description": "Updated active flag."}, + "date_joined": {"type": "datetime", "nullable": false, "description": "Updated join timestamp."}, + "last_login": {"type": "datetime", "nullable": true, "description": "Updated last login timestamp."} + } + } + }, + "responses": { + "200": {"description": "User updated.", "body": {"$ref": "#/schemas/User"}}, + "400": {"description": "No updatable fields provided or invalid data."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "User not found."}, + "409": {"description": "Constraint violation."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "deleteUser", + "method": "DELETE", + "path": "/users/", + "authenticated": true, + "description": "Delete a user by id.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "user_id", "type": "integer", "required": true, "description": "User primary key."} + ] + }, + "responses": { + "204": {"description": "User deleted. Empty body."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "User not found."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "listScrapers", + "method": "GET", + "path": "/scrapers", + "authenticated": true, + "description": "List all scraper configurations ordered by id.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + } + }, + "responses": { + "200": {"description": "Array of scrapers.", "body": {"type": "array", "items": {"$ref": "#/schemas/Scraper"}}}, + "401": {"description": "Missing or invalid bearer token."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "getScraper", + "method": "GET", + "path": "/scrapers/", + "authenticated": true, + "description": "Retrieve a single scraper by text id.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "scraper_id", "type": "string", "required": true, "description": "Scraper identifier."} + ] + }, + "responses": { + "200": {"description": "Scraper payload.", "body": {"$ref": "#/schemas/Scraper"}}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Scraper not found."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "createScraper", + "method": "POST", + "path": "/scrapers", + "authenticated": true, + "description": "Create a scraper configuration.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "body": { + "type": "object", + "required_fields": ["id"], + "fields": { + "id": {"type": "string", "nullable": false, "description": "Scraper primary key."}, + "params": {"type": "string", "nullable": true, "description": "Serialized parameter payload."}, + "frequency": {"type": "string", "nullable": true, "description": "Execution cadence descriptor."}, + "task_name": {"type": "string", "nullable": true, "description": "Linked task name."}, + "property_types": {"type": "string", "nullable": true, "description": "Comma-separated property types."}, + "last_seen_days": {"type": "integer", "nullable": true, "description": "Days since listing last seen."}, + "first_seen_days": {"type": "integer", "nullable": true, "description": "Days since listing first seen."}, + "page_size": {"type": "integer", "nullable": true, "description": "Fetch size per page."}, + "max_pages": {"type": "integer", "nullable": true, "description": "Maximum crawl pages."}, + "enabled": {"type": "integer", "nullable": true, "description": "Activation flag (1 or 0)."}, + "enrich_llm": {"type": "integer", "nullable": true, "description": "LLM enrichment toggle (1 or 0)."}, + "only_match": {"type": "integer", "nullable": true, "description": "Strict matching toggle (1 or 0)."} + } + } + }, + "responses": { + "201": {"description": "Scraper created.", "body": {"$ref": "#/schemas/Scraper"}}, + "400": {"description": "Validation failure."}, + "401": {"description": "Missing or invalid bearer token."}, + "409": {"description": "Constraint violation (e.g., duplicate id)."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "updateScraper", + "method": "PUT", + "path": "/scrapers/", + "authenticated": true, + "description": "Update mutable fields on a scraper configuration.", + "request": { + "headers": { + "Content-Type": {"type": "string", "required": true, "example": "application/json"}, + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "scraper_id", "type": "string", "required": true, "description": "Scraper identifier."} + ], + "body": { + "type": "object", + "allowed_fields": ["params", "frequency", "task_name", "property_types", "last_seen_days", "first_seen_days", "page_size", "max_pages", "enabled", "enrich_llm", "only_match"], + "min_fields": 1, + "fields": { + "params": {"type": "string", "nullable": true, "description": "Updated serialized parameters."}, + "frequency": {"type": "string", "nullable": true, "description": "Updated cadence descriptor."}, + "task_name": {"type": "string", "nullable": true, "description": "Updated task name."}, + "property_types": {"type": "string", "nullable": true, "description": "Updated property type list."}, + "last_seen_days": {"type": "integer", "nullable": true, "description": "Updated last seen days."}, + "first_seen_days": {"type": "integer", "nullable": true, "description": "Updated first seen days."}, + "page_size": {"type": "integer", "nullable": true, "description": "Updated page size."}, + "max_pages": {"type": "integer", "nullable": true, "description": "Updated max pages."}, + "enabled": {"type": "integer", "nullable": true, "description": "Updated enabled flag."}, + "enrich_llm": {"type": "integer", "nullable": true, "description": "Updated enrichment flag."}, + "only_match": {"type": "integer", "nullable": true, "description": "Updated only match flag."} + } + } + }, + "responses": { + "200": {"description": "Scraper updated.", "body": {"$ref": "#/schemas/Scraper"}}, + "400": {"description": "No updatable fields provided or invalid data."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Scraper not found."}, + "409": {"description": "Constraint violation."}, + "503": {"description": "Database unavailable."} + } + }, + { + "operation_id": "deleteScraper", + "method": "DELETE", + "path": "/scrapers/", + "authenticated": true, + "description": "Delete a scraper configuration.", + "request": { + "headers": { + "Authorization": {"type": "string", "required": true, "description": "Bearer "} + }, + "path_params": [ + {"name": "scraper_id", "type": "string", "required": true, "description": "Scraper identifier."} + ] + }, + "responses": { + "204": {"description": "Scraper deleted. Empty body."}, + "401": {"description": "Missing or invalid bearer token."}, + "404": {"description": "Scraper not found."}, + "503": {"description": "Database unavailable."} + } + } + ] +} diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..7877656 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,723 @@ +"""Minimal Flask API with token-based authentication middleware.""" + +from __future__ import annotations + +import os +from datetime import datetime, timezone +from typing import Any, Dict, Iterable, List, Mapping, MutableMapping +from uuid import UUID, uuid4 + +import psycopg +from psycopg import sql +from psycopg.rows import dict_row +from dotenv import load_dotenv +from flask import Flask, abort, jsonify, request, g +from flask_cors import CORS + +load_dotenv() + +API_TOKEN = os.getenv("API_TOKEN") + + +REQUIRED_DB_SETTINGS = { + "DB_NAME": os.getenv("DB_NAME", ""), + "DB_HOST": os.getenv("DB_HOST", ""), + "DB_PORT": os.getenv("DB_PORT", ""), + "DB_USERNAME": os.getenv("DB_USERNAME", ""), + "DB_PASSWORD": os.getenv("DB_PASSWORD", ""), +} + +missing_db_settings = [ + name for name, value in REQUIRED_DB_SETTINGS.items() if not value +] +if missing_db_settings: + missing = ", ".join(missing_db_settings) + raise RuntimeError( + f"Database configuration missing for: {missing}. Did you configure the .env file?" + ) + +DB_NAME = REQUIRED_DB_SETTINGS["DB_NAME"] +DB_HOST = REQUIRED_DB_SETTINGS["DB_HOST"] +DB_USERNAME = REQUIRED_DB_SETTINGS["DB_USERNAME"] +DB_PASSWORD = REQUIRED_DB_SETTINGS["DB_PASSWORD"] + +try: + DB_PORT = int(REQUIRED_DB_SETTINGS["DB_PORT"]) +except ValueError as exc: + raise RuntimeError("DB_PORT must be an integer") from exc + +USER_TABLE = os.getenv("DB_TABLE_USERS", "auth_user") +INVESTMENT_PROFILE_TABLE = os.getenv( + "DB_TABLE_INVESTMENT_PROFILES", "users_investmentprofile" +) +SCRAPER_TABLE = os.getenv("DB_TABLE_SCRAPERS", "scraper") + +if not API_TOKEN: + raise RuntimeError( + "API_TOKEN missing from environment. Did you configure the .env file?" + ) + +app = Flask(__name__) +CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) + + +def get_db_connection() -> psycopg.Connection: + connection = psycopg.connect( + dbname=DB_NAME, + user=DB_USERNAME, + password=DB_PASSWORD, + host=DB_HOST, + port=DB_PORT, + row_factory=dict_row, + ) + connection.autocommit = True + return connection + + +def get_db() -> psycopg.Connection: + if "db_connection" not in g: + try: + g.db_connection = get_db_connection() + except psycopg.OperationalError: + abort(503, description="Database connection failed") + return g.db_connection + + +@app.teardown_appcontext +def close_db_connection(_: BaseException | None) -> None: + db_connection = g.pop("db_connection", None) + if db_connection is not None: + db_connection.close() + + +def _get_json_body() -> MutableMapping[str, Any]: + payload = request.get_json(silent=True) + if not isinstance(payload, MutableMapping): + abort(400, description="Request body must be a JSON object") + return payload + + +def _parse_bool(value: Any, field_name: str) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"true", "1", "yes", "y"}: + return True + if lowered in {"false", "0", "no", "n"}: + return False + if isinstance(value, (int, float)): + if value in {0, 1}: + return bool(value) + abort(400, description=f"Field '{field_name}' must be a boolean value") + + +def _parse_datetime(value: Any, field_name: str) -> datetime | None: + if value is None: + return None + if isinstance(value, datetime): + dt = value + elif isinstance(value, str): + try: + normalized = value.replace("Z", "+00:00") + dt = datetime.fromisoformat(normalized) + except ValueError: + abort( + 400, + description=f"Field '{field_name}' must be a valid ISO 8601 datetime", + ) + else: + abort( + 400, description=f"Field '{field_name}' must be a valid ISO 8601 datetime" + ) + + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + else: + dt = dt.astimezone(timezone.utc) + return dt + + +def _parse_string(value: Any, field_name: str, *, allow_empty: bool = False) -> str: + if not isinstance(value, str): + abort(400, description=f"Field '{field_name}' must be a string") + stripped = value.strip() + if not allow_empty and not stripped: + abort(400, description=f"Field '{field_name}' cannot be empty") + return stripped if not allow_empty else value + + +def _isoformat(dt: datetime | None) -> str | None: + if dt is None: + return None + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _ensure_json_compatible(value: Any, field_name: str) -> Any: + if isinstance(value, (dict, list, str, int, float, bool)) or value is None: + return value + abort(400, description=f"Field '{field_name}' must be valid JSON data") + + +def _parse_int(value: Any, field_name: str) -> int: + if isinstance(value, int): + return value + if isinstance(value, str): + try: + return int(value, 10) + except ValueError: + abort(400, description=f"Field '{field_name}' must be an integer") + abort(400, description=f"Field '{field_name}' must be an integer") + + +def _parse_uuid(value: Any, field_name: str) -> UUID: + if isinstance(value, UUID): + return value + if isinstance(value, str): + try: + return UUID(value) + except ValueError: + abort(400, description=f"Field '{field_name}' must be a valid UUID") + abort(400, description=f"Field '{field_name}' must be a valid UUID") + + +def _require_bearer_token(header_value: str | None) -> str: + if not header_value: + abort(401, description="Missing bearer token") + parts = header_value.strip().split() + if len(parts) != 2 or parts[0].lower() != "bearer": + abort(401, description="Authorization header must be 'Bearer '") + token = parts[1].strip() + if not token: + abort(401, description="Authorization header must include a token") + return token + + +def _serialize_row( + row: Mapping[str, Any], *, datetime_fields: Iterable[str] | None = None +) -> Dict[str, Any]: + result: Dict[str, Any] = dict(row) + for field in datetime_fields or (): + result[field] = _isoformat(result.get(field)) + for field, value in list(result.items()): + if isinstance(value, UUID): + result[field] = str(value) + return result + + +def _fetch_one( + query: sql.Composed, params: Mapping[str, Any] +) -> Mapping[str, Any] | None: + conn = get_db() + with conn.cursor() as cur: + cur.execute(query, params) + return cur.fetchone() + + +def _fetch_all( + query: sql.Composed, params: Mapping[str, Any] | None = None +) -> List[Mapping[str, Any]]: + conn = get_db() + with conn.cursor() as cur: + cur.execute(query, params or {}) + return cur.fetchall() + + +def _execute(query: sql.Composed, params: Mapping[str, Any]) -> None: + conn = get_db() + with conn.cursor() as cur: + cur.execute(query, params) + + +def _columns_sql(columns: Iterable[str]) -> sql.Composed: + return sql.SQL(", ").join(sql.Identifier(column) for column in columns) + + +def _placeholders(columns: Iterable[str]) -> sql.Composed: + return sql.SQL(", ").join(sql.Placeholder(column) for column in columns) + + +def _insert_row( + table: str, data: Mapping[str, Any], returning: Iterable[str] +) -> Mapping[str, Any]: + if not data: + raise ValueError("Cannot insert without data") + query = sql.SQL( + "INSERT INTO {table} ({columns}) VALUES ({values}) RETURNING {returning}" + ).format( + table=sql.Identifier(table), + columns=_columns_sql(data.keys()), + values=_placeholders(data.keys()), + returning=_columns_sql(returning), + ) + row = _fetch_one(query, data) + if row is None: + raise RuntimeError("Insert statement did not return a row") + return row + + +def _update_row( + table: str, + identifier_column: str, + identifier_value: Any, + data: Mapping[str, Any], + returning: Iterable[str], +) -> Mapping[str, Any] | None: + if not data: + raise ValueError("Cannot update without data") + assignments = sql.SQL(", ").join( + sql.SQL("{column} = {placeholder}").format( + column=sql.Identifier(column), + placeholder=sql.Placeholder(column), + ) + for column in data.keys() + ) + query = sql.SQL( + "UPDATE {table} SET {assignments} WHERE {identifier_column} = {identifier} " + "RETURNING {returning}" + ).format( + table=sql.Identifier(table), + assignments=assignments, + identifier_column=sql.Identifier(identifier_column), + identifier=sql.Placeholder("identifier"), + returning=_columns_sql(returning), + ) + params: Dict[str, Any] = dict(data) + params["identifier"] = identifier_value + return _fetch_one(query, params) + + +def _delete_row(table: str, identifier_column: str, identifier_value: Any) -> bool: + query = sql.SQL( + "DELETE FROM {table} WHERE {identifier_column} = {identifier}" + ).format( + table=sql.Identifier(table), + identifier_column=sql.Identifier(identifier_column), + identifier=sql.Placeholder("identifier"), + ) + conn = get_db() + with conn.cursor() as cur: + cur.execute(query, {"identifier": identifier_value}) + return cur.rowcount > 0 + + +def _abort_for_integrity_error(exc: psycopg.IntegrityError) -> None: + detail = getattr(getattr(exc, "diag", None), "message_detail", None) + abort(409, description=detail or "Database constraint violation") + + +USER_RESPONSE_FIELDS = ( + "id", + "username", + "first_name", + "last_name", + "email", + "is_superuser", + "is_staff", + "is_active", + "date_joined", + "last_login", +) + +USER_DATETIME_FIELDS = ("date_joined", "last_login") + +USER_BOOL_FIELDS = ("is_superuser", "is_staff", "is_active") + +PROFILE_RESPONSE_FIELDS = ( + "profile_id", + "name", + "description", + "criteria", + "created_at", + "is_active", +) + +PROFILE_DATETIME_FIELDS = ("created_at",) + +SCRAPER_RESPONSE_FIELDS = ( + "id", + "params", + "last_seen_days", + "first_seen_days", + "frequency", + "task_name", + "enabled", + "property_types", + "page_size", + "max_pages", + "enrich_llm", + "only_match", +) + +SCRAPER_INT_FIELDS = ( + "last_seen_days", + "first_seen_days", + "page_size", + "max_pages", + "enabled", + "enrich_llm", + "only_match", +) + + +@app.before_request +def enforce_bearer_token() -> None: + if request.method == "OPTIONS": + return + + # Allow Flask internals without auth. + if request.endpoint == "static": + return + + provided_token = _require_bearer_token(request.headers.get("Authorization")) + if provided_token != API_TOKEN: + abort(401, description="Invalid bearer token") + + +@app.get("/profiles") +def get_profiles(): + rows = _fetch_all( + sql.SQL("SELECT {columns} FROM {table} ORDER BY created_at DESC").format( + columns=_columns_sql(PROFILE_RESPONSE_FIELDS), + table=sql.Identifier(INVESTMENT_PROFILE_TABLE), + ) + ) + payload = [ + _serialize_row(row, datetime_fields=PROFILE_DATETIME_FIELDS) for row in rows + ] + return jsonify(payload) + + +@app.get("/profiles/") +def get_profile(profile_id: str): + profile_uuid = _parse_uuid(profile_id, "profile_id") + row = _fetch_one( + sql.SQL("SELECT {columns} FROM {table} WHERE profile_id = {identifier}").format( + columns=_columns_sql(PROFILE_RESPONSE_FIELDS), + table=sql.Identifier(INVESTMENT_PROFILE_TABLE), + identifier=sql.Placeholder("profile_id"), + ), + {"profile_id": profile_uuid}, + ) + if row is None: + abort(404, description="Profile not found") + return jsonify(_serialize_row(row, datetime_fields=PROFILE_DATETIME_FIELDS)) + + +@app.post("/profiles") +def create_profile(): + payload = _get_json_body() + + profile_identifier = payload.get("profile_id") + profile_uuid = ( + _parse_uuid(profile_identifier, "profile_id") if profile_identifier else uuid4() + ) + + name = _parse_string(payload.get("name"), "name") + description_value = payload.get("description") + description = ( + None + if description_value is None + else _parse_string(description_value, "description", allow_empty=True) + ) + criteria_raw = payload.get("criteria") + if criteria_raw is None: + abort(400, description="Field 'criteria' is required") + criteria = _ensure_json_compatible(criteria_raw, "criteria") + + created_at_value = payload.get("created_at") + created_at = ( + datetime.now(timezone.utc) + if created_at_value is None + else _parse_datetime(created_at_value, "created_at") + ) + + is_active = _parse_bool(payload.get("is_active", True), "is_active") + + data = { + "profile_id": profile_uuid, + "name": name, + "description": description, + "criteria": criteria, + "created_at": created_at, + "is_active": is_active, + } + + try: + row = _insert_row(INVESTMENT_PROFILE_TABLE, data, PROFILE_RESPONSE_FIELDS) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + return ( + jsonify(_serialize_row(row, datetime_fields=PROFILE_DATETIME_FIELDS)), + 201, + ) + + +@app.put("/profiles/") +def update_profile(profile_id: str): + profile_uuid = _parse_uuid(profile_id, "profile_id") + payload = _get_json_body() + + updates: Dict[str, Any] = {} + + if "name" in payload: + updates["name"] = _parse_string(payload["name"], "name") + if "description" in payload: + description_value = payload["description"] + updates["description"] = ( + None + if description_value is None + else _parse_string(description_value, "description", allow_empty=True) + ) + if "criteria" in payload: + updates["criteria"] = _ensure_json_compatible(payload["criteria"], "criteria") + if "created_at" in payload: + updates["created_at"] = _parse_datetime(payload["created_at"], "created_at") + if "is_active" in payload: + updates["is_active"] = _parse_bool(payload["is_active"], "is_active") + + if not updates: + abort(400, description="No updatable fields provided") + + try: + row = _update_row( + INVESTMENT_PROFILE_TABLE, + "profile_id", + profile_uuid, + updates, + PROFILE_RESPONSE_FIELDS, + ) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + if row is None: + abort(404, description="Profile not found") + + return jsonify(_serialize_row(row, datetime_fields=PROFILE_DATETIME_FIELDS)) + + +@app.delete("/profiles/") +def delete_profile(profile_id: str): + profile_uuid = _parse_uuid(profile_id, "profile_id") + deleted = _delete_row(INVESTMENT_PROFILE_TABLE, "profile_id", profile_uuid) + if not deleted: + abort(404, description="Profile not found") + return "", 204 + + +@app.get("/users") +def get_users(): + rows = _fetch_all( + sql.SQL("SELECT {columns} FROM {table} ORDER BY id").format( + columns=_columns_sql(USER_RESPONSE_FIELDS), + table=sql.Identifier(USER_TABLE), + ) + ) + payload = [ + _serialize_row(row, datetime_fields=USER_DATETIME_FIELDS) for row in rows + ] + return jsonify(payload) + + +@app.get("/users/") +def get_user(user_id: int): + row = _fetch_one( + sql.SQL("SELECT {columns} FROM {table} WHERE id = {identifier}").format( + columns=_columns_sql(USER_RESPONSE_FIELDS), + table=sql.Identifier(USER_TABLE), + identifier=sql.Placeholder("user_id"), + ), + {"user_id": user_id}, + ) + if row is None: + abort(404, description="User not found") + return jsonify(_serialize_row(row, datetime_fields=USER_DATETIME_FIELDS)) + + +@app.post("/users") +def create_user(): + payload = _get_json_body() + + user_data: Dict[str, Any] = {} + user_data["password"] = _parse_string(payload.get("password"), "password") + user_data["username"] = _parse_string(payload.get("username"), "username") + user_data["first_name"] = _parse_string(payload.get("first_name"), "first_name") + user_data["last_name"] = _parse_string(payload.get("last_name"), "last_name") + user_data["email"] = _parse_string(payload.get("email"), "email") + + user_data["is_superuser"] = _parse_bool( + payload.get("is_superuser", False), "is_superuser" + ) + user_data["is_staff"] = _parse_bool(payload.get("is_staff", False), "is_staff") + user_data["is_active"] = _parse_bool(payload.get("is_active", True), "is_active") + + user_data["date_joined"] = _parse_datetime( + payload.get("date_joined"), "date_joined" + ) + if user_data["date_joined"] is None: + user_data["date_joined"] = datetime.now(timezone.utc) + + user_data["last_login"] = _parse_datetime(payload.get("last_login"), "last_login") + + try: + row = _insert_row(USER_TABLE, user_data, USER_RESPONSE_FIELDS) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + return ( + jsonify(_serialize_row(row, datetime_fields=USER_DATETIME_FIELDS)), + 201, + ) + + +@app.put("/users/") +def update_user(user_id: int): + payload = _get_json_body() + + updates: Dict[str, Any] = {} + if "password" in payload: + updates["password"] = _parse_string(payload["password"], "password") + if "username" in payload: + updates["username"] = _parse_string(payload["username"], "username") + if "first_name" in payload: + updates["first_name"] = _parse_string(payload["first_name"], "first_name") + if "last_name" in payload: + updates["last_name"] = _parse_string(payload["last_name"], "last_name") + if "email" in payload: + updates["email"] = _parse_string(payload["email"], "email") + + for field in USER_BOOL_FIELDS: + if field in payload: + updates[field] = _parse_bool(payload[field], field) + + if "date_joined" in payload: + updates["date_joined"] = _parse_datetime(payload["date_joined"], "date_joined") + if "last_login" in payload: + updates["last_login"] = _parse_datetime(payload["last_login"], "last_login") + + if not updates: + abort(400, description="No updatable fields provided") + + try: + row = _update_row(USER_TABLE, "id", user_id, updates, USER_RESPONSE_FIELDS) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + if row is None: + abort(404, description="User not found") + + return jsonify(_serialize_row(row, datetime_fields=USER_DATETIME_FIELDS)) + + +@app.delete("/users/") +def delete_user(user_id: int): + deleted = _delete_row(USER_TABLE, "id", user_id) + if not deleted: + abort(404, description="User not found") + return "", 204 + + +@app.get("/scrapers") +def get_scrapers(): + rows = _fetch_all( + sql.SQL("SELECT {columns} FROM {table} ORDER BY id").format( + columns=_columns_sql(SCRAPER_RESPONSE_FIELDS), + table=sql.Identifier(SCRAPER_TABLE), + ) + ) + return jsonify([dict(row) for row in rows]) + + +@app.get("/scrapers/") +def get_scraper(scraper_id: str): + row = _fetch_one( + sql.SQL("SELECT {columns} FROM {table} WHERE id = {identifier}").format( + columns=_columns_sql(SCRAPER_RESPONSE_FIELDS), + table=sql.Identifier(SCRAPER_TABLE), + identifier=sql.Placeholder("scraper_id"), + ), + {"scraper_id": _parse_string(scraper_id, "id")}, + ) + if row is None: + abort(404, description="Scraper not found") + return jsonify(dict(row)) + + +@app.post("/scrapers") +def create_scraper(): + payload = _get_json_body() + + scraper_id = _parse_string(payload.get("id"), "id") + + data: Dict[str, Any] = {"id": scraper_id} + + for field in ("params", "frequency", "task_name", "property_types"): + if field in payload: + value = payload[field] + data[field] = ( + None if value is None else _parse_string(value, field, allow_empty=True) + ) + + for field in SCRAPER_INT_FIELDS: + if field in payload: + value = payload[field] + data[field] = None if value is None else _parse_int(value, field) + + try: + row = _insert_row(SCRAPER_TABLE, data, SCRAPER_RESPONSE_FIELDS) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + return jsonify(dict(row)), 201 + + +@app.put("/scrapers/") +def update_scraper(scraper_id: str): + payload = _get_json_body() + updates: Dict[str, Any] = {} + + for field in ("params", "frequency", "task_name", "property_types"): + if field in payload: + value = payload[field] + updates[field] = ( + None if value is None else _parse_string(value, field, allow_empty=True) + ) + + for field in SCRAPER_INT_FIELDS: + if field in payload: + value = payload[field] + updates[field] = None if value is None else _parse_int(value, field) + + if not updates: + abort(400, description="No updatable fields provided") + + try: + row = _update_row( + SCRAPER_TABLE, + "id", + _parse_string(scraper_id, "id"), + updates, + SCRAPER_RESPONSE_FIELDS, + ) + except psycopg.IntegrityError as exc: + _abort_for_integrity_error(exc) + + if row is None: + abort(404, description="Scraper not found") + + return jsonify(dict(row)) + + +@app.delete("/scrapers/") +def delete_scraper(scraper_id: str): + deleted = _delete_row(SCRAPER_TABLE, "id", _parse_string(scraper_id, "id")) + if not deleted: + abort(404, description="Scraper not found") + return "", 204 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=int(os.getenv("PORT", "8000")), debug=False) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..49dcb5f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +Flask>=3.0.0,<4.0.0 +python-dotenv>=1.0.0,<2.0.0 +psycopg[binary]>=3.1,<4.0 +Flask-Cors>=4.0.0,<5.0.0 diff --git a/backend/search_schema.json b/backend/search_schema.json new file mode 100644 index 0000000..4e3439b --- /dev/null +++ b/backend/search_schema.json @@ -0,0 +1,353 @@ +{ + "adverts": [ + { + "firstSeenAt": { "max": "date", "min": "date" }, + "flxId": ["string"], + "isOnline": "boolean", + "lastSeenAt": { "max": "date", "min": "date" }, + "location": { + "city": "string", + "cityCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "department": "string", + "inseeCode": "string", + "irisCode": "string", + "locationCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "postalCode": "string" + }, + "price": { + "currency": ["CURRENCY_EUR", "CURRENCY_USD"], + "initial": { + "source": { + "flxId": "string", + "url": "string", + "website": "string" + }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "isAuction": "boolean", + "latest": { + "source": { + "flxId": "string", + "url": "string", + "website": "string" + }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "scope": ["PRICING_ONE_OFF", "PRICING_MENSUAL"], + "warrantyDeposit": { "max": "number", "min": "number" }, + "variation": [ + { + "sinceLastModified": { "max": "number", "min": "number" }, + "sincePublished": { "max": "number", "min": "number" } + } + ] + }, + "source": { "flxId": "string", "url": "string", "website": "string" }, + "isPro": "boolean", + "seller": [ + { + "flxId": "string", + "name": "string", + "siren": "string", + "type": "SELLER_TYPE_UNKNOWN | SELLER_TYPE_AGENCY | SELLER_TYPE_NETWORK" + } + ], + "hasAnomaly": "boolean", + "isExclusive": "boolean" + } + ], + "habitation": { + "bathroomCount": { "max": "number", "min": "number" }, + "bedroomCount": { "max": "number", "min": "number" }, + "characteristics": { + "hasAlarm": "boolean", + "hasBalcony": "boolean", + "hasCellar": "boolean", + "hasConcierge": "boolean", + "hasDigicode": "boolean", + "hasFireplace": "boolean", + "hasGarage": "boolean", + "hasGarden": "boolean", + "hasGrenier": "boolean", + "hasInterphone": "boolean", + "hasJacuzzi": "boolean", + "hasLand": "boolean", + "hasLift": "boolean", + "hasMezzanine": "boolean", + "hasParking": "boolean", + "hasPool": "boolean", + "hasTerrace": "boolean", + "hasVisAVis": "boolean", + "isPeaceful": "boolean" + }, + "climate": { + "epcClimate": [ + "GREENHOUSE_CLASSIFICATION_UNKNOWN", + "GREENHOUSE_CLASSIFICATION_G", + "GREENHOUSE_CLASSIFICATION_F", + "GREENHOUSE_CLASSIFICATION_E", + "GREENHOUSE_CLASSIFICATION_D", + "GREENHOUSE_CLASSIFICATION_C", + "GREENHOUSE_CLASSIFICATION_B", + "GREENHOUSE_CLASSIFICATION_A", + "GREENHOUSE_CLASSIFICATION_NC" + ], + "epcClimateScore": { "max": "number", "min": "number" }, + "epcEnergy": [ + "ENERGY_CLASSIFICATION_UNKNOWN", + "ENERGY_CLASSIFICATION_G", + "ENERGY_CLASSIFICATION_F", + "ENERGY_CLASSIFICATION_E", + "ENERGY_CLASSIFICATION_D", + "ENERGY_CLASSIFICATION_C", + "ENERGY_CLASSIFICATION_B", + "ENERGY_CLASSIFICATION_A", + "ENERGY_CLASSIFICATION_NC" + ], + "epcEnergyScore": { "max": "number", "min": "number" }, + "epcClimateDate": { "max": "date", "min": "date" }, + "epcEnergyDate": { "max": "date", "min": "date" } + }, + "features": { + "exposure": [ + "EXPOSURE_UNKNOWN", + "EXPOSURE_NORTH", + "EXPOSURE_NORTH_EAST", + "EXPOSURE_EAST", + "EXPOSURE_SOUTH_EAST", + "EXPOSURE_SOUTH", + "EXPOSURE_SOUTH_WEST", + "EXPOSURE_WEST", + "EXPOSURE_NORTH_WEST" + ], + "furniture": [ + "UNKNOWN_FURNITURE", + "UNFURNISHED", + "PARTIALLY_FURNISHED", + "FULLY_FURNISHED" + ], + "propertyFloor": { "max": "number", "min": "number" }, + "propertyTotalFloor": { "max": "number", "min": "number" }, + "constructionMaterials": ["string"], + "glazingTypes": ["string"], + "hasThroughExposure": "boolean", + "viewOns": ["string"], + "viewTypes": ["string"] + }, + "heatTypes": [ + "HEAT_TYPE_UNKNOWN", + "HEAT_TYPE_INDIVIDUAL", + "HEAT_TYPE_SHARED", + "HEAT_TYPE_MIX", + "HEAT_TYPE_CITY", + "HEAT_TYPE_CISTERN" + ], + "heatTypeDetails": [ + "HEAT_DETAIL_UNKNOWN", + "HEAT_DETAIL_FLOOR", + "HEAT_DETAIL_CEILING", + "HEAT_DETAIL_FIREPLACE", + "HEAT_DETAIL_INSERT", + "HEAT_DETAIL_AIR_CON", + "HEAT_DETAIL_REVERSIBLE_AIR_CON", + "HEAT_DETAIL_RADIANTS_TUBES", + "HEAT_DETAIL_RADIATOR", + "HEAT_DETAIL_SHEAHS", + "HEAT_DETAIL_CONVECTOR" + ], + "heatings": [ + "HEATING_UNKNOWN", + "HEATING_FUEL_OIL", + "HEATING_ELECTRICAL", + "HEATING_GAS", + "HEATING_BIOMASS", + "HEATING_SOLAR", + "HEATING_GEOTHERMAL", + "HEATING_WOOD", + "HEATING_HEAT_PUMP", + "HEATING_PELLET", + "HEATING_CANADIAN_WELL", + "HEATING_COAL" + ], + "propertyCondition": [ + { + "constructionYear": "number", + "interiorCondition": [ + "INTERIOR_CONDITION_UNKNOWN", + "INTERIOR_CONDITION_EXCELLENT", + "INTERIOR_CONDITION_TO_REFRESH", + "INTERIOR_CONDITION_SMALL_WORKS_TO_BE_PLANNED", + "INTERIOR_CONDITION_MAJOR_WORKS_TO_BE_PALLNED", + "INTERIOR_CONDITION_BRAND_NEW", + "INTERIOR_CONDITION_GOOD_CONDITION", + "INTERIOR_CONDITION_TO_BE_RENOVATED" + ], + "renovationYear": "number", + "generalConditions": ["string"] + } + ], + "roomCount": { "max": "number", "min": "number" }, + "surface": { + "balconies": { "max": "number", "min": "number" }, + "floorSpace": { "max": "number", "min": "number" }, + "gardens": { "max": "number", "min": "number" }, + "groundFloor": { "max": "number", "min": "number" }, + "kitchen": { "max": "number", "min": "number" }, + "livingSpace": { "max": "number", "min": "number" }, + "livingroom": { "max": "number", "min": "number" }, + "terraces": { "max": "number", "min": "number" }, + "total": { "max": "number", "min": "number" } + }, + "type": [ + "PROPERTY_TYPE_UNKNOWN", + "PROPERTY_TYPE_STUDIO", + "PROPERTY_TYPE_T1", + "PROPERTY_TYPE_T1_T2", + "PROPERTY_TYPE_T2", + "PROPERTY_TYPE_T2_T3", + "PROPERTY_TYPE_T3", + "PROPERTY_TYPE_T3_4", + "PROPERTY_TYPE_T4", + "PROPERTY_TYPE_T4_5", + "PROPERTY_TYPE_T5_MORE", + "PROPERTY_TYPE_LOFT", + "PROPERTY_TYPE_DUPLEX", + "PROPERTY_TYPE_OTHER_APARTMENT_TYPE", + "PROPERTY_TYPE_INDIVIDUAL", + "PROPERTY_TYPE_ONE_SIDE_TERRACED", + "PROPERTY_TYPE_TWO_SIDE_TERRACED", + "PROPERTY_TYPE_SINGLE_STOREY", + "PROPERTY_TYPE_TRADITIONAL", + "PROPERTY_TYPE_CONTEMPORARY", + "PROPERTY_TYPE_BOURGEOIS", + "PROPERTY_TYPE_VILLA", + "PROPERTY_TYPE_MANOR", + "PROPERTY_TYPE_CASTLE", + "PROPERTY_TYPE_FARM", + "PROPERTY_TYPE_MAS", + "PROPERTY_TYPE_BASTIDE", + "PROPERTY_TYPE_CHALET", + "PROPERTY_TYPE_ANCIENT", + "PROPERTY_TYPE_HERITAGE_LISTED", + "PROPERTY_TYPE_BUNGALOW" + ], + "wcCount": { "max": "number", "min": "number" } + }, + "isUrgent": "boolean", + "land": { + "canConstruct": "boolean", + "isServiced": "boolean", + "surface": { "max": "number", "min": "number" }, + "surfaceConstructable": { "max": "number", "min": "number" }, + "type": [ + "LAND_UNKNOWN", + "LAND_BUILDING_PLOT", + "LAND_AGRICULTURAL", + "LAND_VINEYARD", + "LAND_INDUSTRIAL", + "LAND_POND", + "LAND_FOREST" + ], + "haveBuildingPermit": "boolean", + "haveElectricity": "boolean", + "haveTelecom": "boolean", + "haveWater": "boolean" + }, + "location": [ + { + "city": "string", + "cityCoordinate": { "location": { "lon": "number", "lat": "number" } }, + "department": "string", + "inseeCode": "string", + "irisCode": "string", + "locationCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "postalCode": "string" + } + ], + "meta": { + "firstSeenAt": { "max": "date", "min": "date" }, + "isTotallyOffline": "boolean", + "lastPublishedAt": { "max": "date", "min": "date" }, + "lastSeenAt": { "max": "date", "min": "date" }, + "lastUpdatedAt": { "max": "date", "min": "date" } + }, + "parking": { + "count": { "max": "number", "min": "number" }, + "numberOfCars": { "max": "number", "min": "number" }, + "surface": { "max": "number", "min": "number" }, + "type": ["PARKING_UNKNOWN", "PARKING_GARAGE", "PARKING_PARKING"] + }, + "price": { + "currency": ["CURRENCY_EUR", "CURRENCY_USD"], + "initial": { + "source": { "flxId": "string", "url": "string", "website": "string" }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "isAuction": "boolean", + "latest": { + "source": { "flxId": "string", "url": "string", "website": "string" }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "scope": ["PRICING_ONE_OFF", "PRICING_MENSUAL"], + "warrantyDeposit": { "max": "number", "min": "number" }, + "variation": [ + { + "sinceLastModified": { "max": "number", "min": "number" }, + "sincePublished": { "max": "number", "min": "number" } + } + ] + }, + "process": [ + "PROCESS_UNKNOWN", + "PROCESS_AVAILABLE_ON_MARKET", + "PROCESS_UNDER_COMPROMISE", + "PROCESS_RENTED_SOLD", + "PROCESS_REMOVED", + "PROCESS_RESERVED", + "PROCESS_ARCHIVED" + ], + "tags": ["string"], + "type": [ + "CLASS_UNKNOWN", + "CLASS_HOUSE", + "CLASS_FLAT", + "CLASS_PROGRAM", + "CLASS_SHOP", + "CLASS_PREMISES", + "CLASS_OFFICE", + "CLASS_LAND", + "CLASS_BUILDING", + "CLASS_PARKING" + ], + "hasAnomaly": "boolean", + "offer": [ + { + "isCurrentlyOccupied": "boolean", + "renting": { + "isColocation": "boolean", + "isLongTerm": "boolean", + "isShortTerm": "boolean", + "isSubLease": "boolean" + }, + "type": [ + "OFFER_UNKNOWN", + "OFFER_BUY", + "OFFER_RENT", + "OFFER_BUSINESS_TAKE_OVER", + "OFFER_LEASE_BACK", + "OFFER_LIFE_ANNUITY_SALE", + "OFFER_HOLIDAYS" + ] + } + ] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..86b2b11 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/api-schema.json b/frontend/api-schema.json new file mode 100644 index 0000000..a97b59a --- /dev/null +++ b/frontend/api-schema.json @@ -0,0 +1,1289 @@ +{ + "api_meta": { + "name": "Managinator Backend API", + "overview": "Token-gated Flask service for managing investment profiles, users, and scrapers.", + "base_url_example": "http://localhost:8000", + "default_headers": { + "Content-Type": "application/json" + }, + "cors": { + "allow_origin": "*", + "allow_credentials": true, + "notes": "Server responds to OPTIONS preflight without authentication." + } + }, + "authentication": { + "strategy": "session_token", + "login_endpoint": "/auth/login", + "session_duration_seconds": 86400, + "header": "X-Session-Token", + "credential_source": "Clients must send the server-side AUTH_TOKEN value in the login body to receive a session token.", + "notes": [ + "All endpoints except POST /auth/login require a valid X-Session-Token header.", + "Session tokens expire 24 hours after issuance; expired tokens trigger 401 responses and must be refreshed via /auth/login." + ] + }, + "conventions": { + "datetime_format": "Use ISO 8601 strings with timezone. Responses normalize to UTC with trailing Z.", + "boolean_input": "Send JSON booleans true/false. The backend also accepts 1/0 and common string equivalents, but clients should prefer booleans.", + "integer_input": "Integer fields accept JSON numbers. Numeric strings will be coerced server-side but should be avoided.", + "null_handling": "Fields documented as nullable accept JSON null and will persist as SQL NULL.", + "error_responses": { + "typical_status_codes": [400, 401, 404, 409, 503], + "body_format": "Flask default HTTP error responses; body may be plain text or HTML containing the description. Rely on HTTP status and message." + } + }, + "schemas": { + "Session": { + "description": "Session token issued after successful login.", + "fields": [ + { + "name": "session_token", + "type": "string", + "nullable": false, + "read_only": true, + "description": "Opaque token to supply via X-Session-Token header." + }, + { + "name": "expires_at", + "type": "datetime", + "nullable": false, + "read_only": true, + "description": "UTC expiry timestamp in ISO 8601 format with trailing Z." + } + ] + }, + "Profile": { + "description": "Investment profile descriptor persisted in users_investmentprofile.", + "fields": [ + { + "name": "profile_id", + "type": "uuid", + "nullable": false, + "read_only": false, + "description": "Primary key. Optional on create; autogenerated when omitted." + }, + { + "name": "name", + "type": "string", + "nullable": false, + "read_only": false, + "description": "Human-readable profile label." + }, + { + "name": "description", + "type": "string", + "nullable": true, + "read_only": false, + "description": "Optional free-form description. Empty string preserved." + }, + { + "name": "criteria", + "type": "json", + "nullable": false, + "read_only": false, + "description": "Arbitrary JSON object/array describing selection rules." + }, + { + "name": "created_at", + "type": "datetime", + "nullable": false, + "read_only": false, + "description": "Creation timestamp. Defaults to current UTC when omitted." + }, + { + "name": "is_active", + "type": "boolean", + "nullable": false, + "read_only": false, + "description": "Activation flag. Defaults to true on create." + } + ] + }, + "User": { + "description": "Auth user record persisted in auth_user.", + "fields": [ + { + "name": "id", + "type": "integer", + "nullable": false, + "read_only": true, + "description": "Database primary key." + }, + { + "name": "username", + "type": "string", + "nullable": false, + "read_only": false, + "description": "Unique login name." + }, + { + "name": "password", + "type": "string", + "nullable": false, + "read_only": false, + "description": "Raw password value inserted as-is. Not returned in responses." + }, + { + "name": "first_name", + "type": "string", + "nullable": false, + "read_only": false, + "description": "User first name." + }, + { + "name": "last_name", + "type": "string", + "nullable": false, + "read_only": false, + "description": "User last name." + }, + { + "name": "email", + "type": "string", + "nullable": false, + "read_only": false, + "description": "User email address." + }, + { + "name": "is_superuser", + "type": "boolean", + "nullable": false, + "read_only": false, + "description": "Administrative superuser flag." + }, + { + "name": "is_staff", + "type": "boolean", + "nullable": false, + "read_only": false, + "description": "Staff membership flag." + }, + { + "name": "is_active", + "type": "boolean", + "nullable": false, + "read_only": false, + "description": "Account enabled flag." + }, + { + "name": "date_joined", + "type": "datetime", + "nullable": false, + "read_only": false, + "description": "Account creation timestamp. Defaults to current UTC when omitted." + }, + { + "name": "last_login", + "type": "datetime", + "nullable": true, + "read_only": false, + "description": "Most recent login timestamp." + } + ] + }, + "Scraper": { + "description": "Scraper configuration persisted in scraper table.", + "fields": [ + { + "name": "id", + "type": "string", + "nullable": false, + "read_only": false, + "description": "Primary key identifier." + }, + { + "name": "params", + "type": "string", + "nullable": true, + "read_only": false, + "description": "Serialized scraper parameters." + }, + { + "name": "last_seen_days", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Number of days since listings were last seen." + }, + { + "name": "first_seen_days", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Number of days since listings were first seen." + }, + { + "name": "frequency", + "type": "string", + "nullable": true, + "read_only": false, + "description": "Cron or human-readable frequency descriptor." + }, + { + "name": "task_name", + "type": "string", + "nullable": true, + "read_only": false, + "description": "Associated task identifier." + }, + { + "name": "enabled", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "1/0 flag for activation. Stored as integer." + }, + { + "name": "property_types", + "type": "string", + "nullable": true, + "read_only": false, + "description": "Comma-separated property type filters." + }, + { + "name": "page_size", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Listings fetched per page." + }, + { + "name": "max_pages", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Maximum number of pages to crawl." + }, + { + "name": "enrich_llm", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Toggle for LLM enrichment (1 enabled, 0 disabled)." + }, + { + "name": "only_match", + "type": "integer", + "nullable": true, + "read_only": false, + "description": "Toggle for strict matching (1 enabled, 0 disabled)." + } + ] + } + }, + "endpoints": [ + { + "operation_id": "login", + "method": "POST", + "path": "/auth/login", + "authenticated": false, + "description": "Exchange the static AUTH_TOKEN for a time-limited session token.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + } + }, + "body": { + "type": "object", + "required_fields": ["token"], + "fields": { + "token": { + "type": "string", + "nullable": false, + "description": "Server-configured AUTH_TOKEN value." + } + } + } + }, + "responses": { + "201": { + "description": "Session token issued.", + "body": { + "$ref": "#/schemas/Session" + } + }, + "400": { + "description": "Token missing." + }, + "401": { + "description": "Submitted token invalid." + } + } + }, + { + "operation_id": "listProfiles", + "method": "GET", + "path": "/profiles", + "authenticated": true, + "description": "List all investment profiles ordered by most recent creation.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [], + "query_params": [], + "body": null + }, + "responses": { + "200": { + "description": "Array of profiles.", + "body": { + "type": "array", + "items": { + "$ref": "#/schemas/Profile" + } + } + }, + "401": { + "description": "Missing or invalid session." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "getProfile", + "method": "GET", + "path": "/profiles/", + "authenticated": true, + "description": "Retrieve a single profile by UUID.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "profile_id", + "type": "uuid", + "required": true, + "description": "Profile identifier." + } + ], + "query_params": [], + "body": null + }, + "responses": { + "200": { + "description": "Profile payload.", + "body": { + "$ref": "#/schemas/Profile" + } + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Profile not found." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "createProfile", + "method": "POST", + "path": "/profiles", + "authenticated": true, + "description": "Create a new investment profile.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "body": { + "type": "object", + "required_fields": ["name", "criteria"], + "fields": { + "profile_id": { + "type": "uuid", + "nullable": false, + "description": "Optional explicit UUID. Auto-generated when omitted." + }, + "name": { + "type": "string", + "nullable": false, + "description": "Profile name." + }, + "description": { + "type": "string", + "nullable": true, + "description": "Optional description. Use null to clear." + }, + "criteria": { + "type": "json", + "nullable": false, + "description": "Required JSON criteria payload." + }, + "created_at": { + "type": "datetime", + "nullable": false, + "description": "Optional creation timestamp. Defaults to now (UTC)." + }, + "is_active": { + "type": "boolean", + "nullable": false, + "description": "Defaults to true." + } + } + } + }, + "responses": { + "201": { + "description": "Profile created.", + "body": { + "$ref": "#/schemas/Profile" + } + }, + "400": { + "description": "Validation failure (missing criteria, invalid field formats, or empty request)." + }, + "401": { + "description": "Missing or invalid session." + }, + "409": { + "description": "Constraint violation (e.g., duplicate profile_id)." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "updateProfile", + "method": "PUT", + "path": "/profiles/", + "authenticated": true, + "description": "Update mutable fields on an existing profile.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "profile_id", + "type": "uuid", + "required": true, + "description": "Profile identifier." + } + ], + "body": { + "type": "object", + "allowed_fields": [ + "name", + "description", + "criteria", + "created_at", + "is_active" + ], + "min_fields": 1, + "fields": { + "name": { + "type": "string", + "nullable": false, + "description": "New profile name." + }, + "description": { + "type": "string", + "nullable": true, + "description": "Use null to clear description." + }, + "criteria": { + "type": "json", + "nullable": false, + "description": "Updated criteria JSON." + }, + "created_at": { + "type": "datetime", + "nullable": false, + "description": "New creation timestamp." + }, + "is_active": { + "type": "boolean", + "nullable": false, + "description": "Activation flag." + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated.", + "body": { + "$ref": "#/schemas/Profile" + } + }, + "400": { + "description": "No updatable fields provided or invalid data." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Profile not found." + }, + "409": { + "description": "Constraint violation." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "deleteProfile", + "method": "DELETE", + "path": "/profiles/", + "authenticated": true, + "description": "Remove a profile by UUID.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "profile_id", + "type": "uuid", + "required": true, + "description": "Profile identifier." + } + ], + "body": null + }, + "responses": { + "204": { + "description": "Profile deleted. Empty body." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Profile not found." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "listUsers", + "method": "GET", + "path": "/users", + "authenticated": true, + "description": "List all users ordered by ascending id.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "body": null + }, + "responses": { + "200": { + "description": "Array of users.", + "body": { + "type": "array", + "items": { + "$ref": "#/schemas/User" + } + } + }, + "401": { + "description": "Missing or invalid session." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "getUser", + "method": "GET", + "path": "/users/", + "authenticated": true, + "description": "Retrieve a user by numeric id.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "user_id", + "type": "integer", + "required": true, + "description": "User primary key." + } + ], + "body": null + }, + "responses": { + "200": { + "description": "User payload.", + "body": { + "$ref": "#/schemas/User" + } + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "User not found." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "createUser", + "method": "POST", + "path": "/users", + "authenticated": true, + "description": "Create a new user record.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "body": { + "type": "object", + "required_fields": [ + "password", + "username", + "first_name", + "last_name", + "email" + ], + "fields": { + "password": { + "type": "string", + "nullable": false, + "description": "Password to store. No hashing performed." + }, + "username": { + "type": "string", + "nullable": false, + "description": "Unique username." + }, + "first_name": { + "type": "string", + "nullable": false, + "description": "First name." + }, + "last_name": { + "type": "string", + "nullable": false, + "description": "Last name." + }, + "email": { + "type": "string", + "nullable": false, + "description": "Email address." + }, + "is_superuser": { + "type": "boolean", + "nullable": false, + "description": "Defaults to false when omitted." + }, + "is_staff": { + "type": "boolean", + "nullable": false, + "description": "Defaults to false when omitted." + }, + "is_active": { + "type": "boolean", + "nullable": false, + "description": "Defaults to true when omitted." + }, + "date_joined": { + "type": "datetime", + "nullable": true, + "description": "Optional. Defaults to current UTC when omitted or null." + }, + "last_login": { + "type": "datetime", + "nullable": true, + "description": "Optional last login timestamp." + } + } + } + }, + "responses": { + "201": { + "description": "User created.", + "body": { + "$ref": "#/schemas/User" + } + }, + "400": { + "description": "Validation failure." + }, + "401": { + "description": "Missing or invalid session." + }, + "409": { + "description": "Constraint violation (e.g., duplicate username or email)." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "updateUser", + "method": "PUT", + "path": "/users/", + "authenticated": true, + "description": "Update mutable fields on a user.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "user_id", + "type": "integer", + "required": true, + "description": "User primary key." + } + ], + "body": { + "type": "object", + "allowed_fields": [ + "password", + "username", + "first_name", + "last_name", + "email", + "is_superuser", + "is_staff", + "is_active", + "date_joined", + "last_login" + ], + "min_fields": 1, + "fields": { + "password": { + "type": "string", + "nullable": false, + "description": "New raw password." + }, + "username": { + "type": "string", + "nullable": false, + "description": "New username." + }, + "first_name": { + "type": "string", + "nullable": false, + "description": "New first name." + }, + "last_name": { + "type": "string", + "nullable": false, + "description": "New last name." + }, + "email": { + "type": "string", + "nullable": false, + "description": "New email." + }, + "is_superuser": { + "type": "boolean", + "nullable": false, + "description": "Updated admin flag." + }, + "is_staff": { + "type": "boolean", + "nullable": false, + "description": "Updated staff flag." + }, + "is_active": { + "type": "boolean", + "nullable": false, + "description": "Updated active flag." + }, + "date_joined": { + "type": "datetime", + "nullable": false, + "description": "Updated join timestamp." + }, + "last_login": { + "type": "datetime", + "nullable": true, + "description": "Updated last login timestamp." + } + } + } + }, + "responses": { + "200": { + "description": "User updated.", + "body": { + "$ref": "#/schemas/User" + } + }, + "400": { + "description": "No updatable fields provided or invalid data." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "User not found." + }, + "409": { + "description": "Constraint violation." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "deleteUser", + "method": "DELETE", + "path": "/users/", + "authenticated": true, + "description": "Delete a user by id.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "user_id", + "type": "integer", + "required": true, + "description": "User primary key." + } + ], + "body": null + }, + "responses": { + "204": { + "description": "User deleted. Empty body." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "User not found." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "listScrapers", + "method": "GET", + "path": "/scrapers", + "authenticated": true, + "description": "List all scraper configurations ordered by id.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "body": null + }, + "responses": { + "200": { + "description": "Array of scrapers.", + "body": { + "type": "array", + "items": { + "$ref": "#/schemas/Scraper" + } + } + }, + "401": { + "description": "Missing or invalid session." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "getScraper", + "method": "GET", + "path": "/scrapers/", + "authenticated": true, + "description": "Retrieve a single scraper by text id.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "scraper_id", + "type": "string", + "required": true, + "description": "Scraper identifier." + } + ], + "body": null + }, + "responses": { + "200": { + "description": "Scraper payload.", + "body": { + "$ref": "#/schemas/Scraper" + } + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Scraper not found." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "createScraper", + "method": "POST", + "path": "/scrapers", + "authenticated": true, + "description": "Create a scraper configuration.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "body": { + "type": "object", + "required_fields": ["id"], + "fields": { + "id": { + "type": "string", + "nullable": false, + "description": "Scraper primary key." + }, + "params": { + "type": "string", + "nullable": true, + "description": "Serialized parameter payload." + }, + "frequency": { + "type": "string", + "nullable": true, + "description": "Execution cadence descriptor." + }, + "task_name": { + "type": "string", + "nullable": true, + "description": "Linked task name." + }, + "property_types": { + "type": "string", + "nullable": true, + "description": "Comma-separated property types." + }, + "last_seen_days": { + "type": "integer", + "nullable": true, + "description": "Days since listing last seen." + }, + "first_seen_days": { + "type": "integer", + "nullable": true, + "description": "Days since listing first seen." + }, + "page_size": { + "type": "integer", + "nullable": true, + "description": "Fetch size per page." + }, + "max_pages": { + "type": "integer", + "nullable": true, + "description": "Maximum crawl pages." + }, + "enabled": { + "type": "integer", + "nullable": true, + "description": "Activation flag (1 or 0)." + }, + "enrich_llm": { + "type": "integer", + "nullable": true, + "description": "LLM enrichment toggle (1 or 0)." + }, + "only_match": { + "type": "integer", + "nullable": true, + "description": "Strict matching toggle (1 or 0)." + } + } + } + }, + "responses": { + "201": { + "description": "Scraper created.", + "body": { + "$ref": "#/schemas/Scraper" + } + }, + "400": { + "description": "Validation failure." + }, + "401": { + "description": "Missing or invalid session." + }, + "409": { + "description": "Constraint violation (e.g., duplicate id)." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "updateScraper", + "method": "PUT", + "path": "/scrapers/", + "authenticated": true, + "description": "Update mutable fields on a scraper configuration.", + "request": { + "headers": { + "Content-Type": { + "type": "string", + "required": true, + "example": "application/json" + }, + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "scraper_id", + "type": "string", + "required": true, + "description": "Scraper identifier." + } + ], + "body": { + "type": "object", + "allowed_fields": [ + "params", + "frequency", + "task_name", + "property_types", + "last_seen_days", + "first_seen_days", + "page_size", + "max_pages", + "enabled", + "enrich_llm", + "only_match" + ], + "min_fields": 1, + "fields": { + "params": { + "type": "string", + "nullable": true, + "description": "Updated serialized parameters." + }, + "frequency": { + "type": "string", + "nullable": true, + "description": "Updated cadence descriptor." + }, + "task_name": { + "type": "string", + "nullable": true, + "description": "Updated task name." + }, + "property_types": { + "type": "string", + "nullable": true, + "description": "Updated property type list." + }, + "last_seen_days": { + "type": "integer", + "nullable": true, + "description": "Updated last seen days." + }, + "first_seen_days": { + "type": "integer", + "nullable": true, + "description": "Updated first seen days." + }, + "page_size": { + "type": "integer", + "nullable": true, + "description": "Updated page size." + }, + "max_pages": { + "type": "integer", + "nullable": true, + "description": "Updated max pages." + }, + "enabled": { + "type": "integer", + "nullable": true, + "description": "Updated enabled flag." + }, + "enrich_llm": { + "type": "integer", + "nullable": true, + "description": "Updated enrichment flag." + }, + "only_match": { + "type": "integer", + "nullable": true, + "description": "Updated only match flag." + } + } + } + }, + "responses": { + "200": { + "description": "Scraper updated.", + "body": { + "$ref": "#/schemas/Scraper" + } + }, + "400": { + "description": "No updatable fields provided or invalid data." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Scraper not found." + }, + "409": { + "description": "Constraint violation." + }, + "503": { + "description": "Database unavailable." + } + } + }, + { + "operation_id": "deleteScraper", + "method": "DELETE", + "path": "/scrapers/", + "authenticated": true, + "description": "Delete a scraper configuration.", + "request": { + "headers": { + "X-Session-Token": { + "type": "string", + "required": true, + "description": "Session token from /auth/login." + } + }, + "path_params": [ + { + "name": "scraper_id", + "type": "string", + "required": true, + "description": "Scraper identifier." + } + ], + "body": null + }, + "responses": { + "204": { + "description": "Scraper deleted. Empty body." + }, + "401": { + "description": "Missing or invalid session." + }, + "404": { + "description": "Scraper not found." + }, + "503": { + "description": "Database unavailable." + } + } + } + ] +} diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..0f06ea6 --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,680 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.545.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-hook-form": "^7.65.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14", + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "babel-plugin-react-compiler": "^19.1.0-rc.3", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "npm:rolldown-vite@7.1.14", + }, + }, + }, + "overrides": { + "vite": "npm:rolldown-vite@7.1.14", + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], + + "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.92.0", "", {}, "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw=="], + + "@oxc-project/types": ["@oxc-project/types@0.93.0", "", {}, "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.41", "", { "os": "android", "cpu": "arm64" }, "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.41", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm" }, "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.41", "", { "os": "linux", "cpu": "x64" }, "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.41", "", { "os": "linux", "cpu": "x64" }, "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.41", "", { "os": "none", "cpu": "arm64" }, "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.41", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "arm64" }, "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "ia32" }, "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.41", "", { "os": "win32", "cpu": "x64" }, "sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.3", "", {}, "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.3", "", { "dependencies": { "@tanstack/query-core": "5.90.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/utils": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.1.0-rc.1-rc-af1b7da-20250421", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-E3kaokBhWDLf7ZD8fuYjYn0ZJHYZ+3EHtAWCdX2hl4lpu1z9S/Xr99sxhx2bTCVB41oIesz9FtM8f4INsrZaOw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.235", "", {}, "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.0-beta.41", "", { "dependencies": { "@oxc-project/types": "=0.93.0", "@rolldown/pluginutils": "1.0.0-beta.41", "ansis": "=4.2.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.41", "@rolldown/binding-darwin-arm64": "1.0.0-beta.41", "@rolldown/binding-darwin-x64": "1.0.0-beta.41", "@rolldown/binding-freebsd-x64": "1.0.0-beta.41", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.41", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.41", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.41", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.41", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.41", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.41", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.41", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.41", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.41", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.41" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + + "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.46.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "vite": ["rolldown-vite@7.1.14", "", { "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.41", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.41", "", {}, "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + } +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..2b0833f --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..b19330b --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0efe3f6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.545.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-hook-form": "^7.65.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "babel-plugin-react-compiler": "^19.1.0-rc.3", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "npm:rolldown-vite@7.1.14" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.1.14" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/scraper-schema.json b/frontend/scraper-schema.json new file mode 100644 index 0000000..4e3439b --- /dev/null +++ b/frontend/scraper-schema.json @@ -0,0 +1,353 @@ +{ + "adverts": [ + { + "firstSeenAt": { "max": "date", "min": "date" }, + "flxId": ["string"], + "isOnline": "boolean", + "lastSeenAt": { "max": "date", "min": "date" }, + "location": { + "city": "string", + "cityCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "department": "string", + "inseeCode": "string", + "irisCode": "string", + "locationCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "postalCode": "string" + }, + "price": { + "currency": ["CURRENCY_EUR", "CURRENCY_USD"], + "initial": { + "source": { + "flxId": "string", + "url": "string", + "website": "string" + }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "isAuction": "boolean", + "latest": { + "source": { + "flxId": "string", + "url": "string", + "website": "string" + }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "scope": ["PRICING_ONE_OFF", "PRICING_MENSUAL"], + "warrantyDeposit": { "max": "number", "min": "number" }, + "variation": [ + { + "sinceLastModified": { "max": "number", "min": "number" }, + "sincePublished": { "max": "number", "min": "number" } + } + ] + }, + "source": { "flxId": "string", "url": "string", "website": "string" }, + "isPro": "boolean", + "seller": [ + { + "flxId": "string", + "name": "string", + "siren": "string", + "type": "SELLER_TYPE_UNKNOWN | SELLER_TYPE_AGENCY | SELLER_TYPE_NETWORK" + } + ], + "hasAnomaly": "boolean", + "isExclusive": "boolean" + } + ], + "habitation": { + "bathroomCount": { "max": "number", "min": "number" }, + "bedroomCount": { "max": "number", "min": "number" }, + "characteristics": { + "hasAlarm": "boolean", + "hasBalcony": "boolean", + "hasCellar": "boolean", + "hasConcierge": "boolean", + "hasDigicode": "boolean", + "hasFireplace": "boolean", + "hasGarage": "boolean", + "hasGarden": "boolean", + "hasGrenier": "boolean", + "hasInterphone": "boolean", + "hasJacuzzi": "boolean", + "hasLand": "boolean", + "hasLift": "boolean", + "hasMezzanine": "boolean", + "hasParking": "boolean", + "hasPool": "boolean", + "hasTerrace": "boolean", + "hasVisAVis": "boolean", + "isPeaceful": "boolean" + }, + "climate": { + "epcClimate": [ + "GREENHOUSE_CLASSIFICATION_UNKNOWN", + "GREENHOUSE_CLASSIFICATION_G", + "GREENHOUSE_CLASSIFICATION_F", + "GREENHOUSE_CLASSIFICATION_E", + "GREENHOUSE_CLASSIFICATION_D", + "GREENHOUSE_CLASSIFICATION_C", + "GREENHOUSE_CLASSIFICATION_B", + "GREENHOUSE_CLASSIFICATION_A", + "GREENHOUSE_CLASSIFICATION_NC" + ], + "epcClimateScore": { "max": "number", "min": "number" }, + "epcEnergy": [ + "ENERGY_CLASSIFICATION_UNKNOWN", + "ENERGY_CLASSIFICATION_G", + "ENERGY_CLASSIFICATION_F", + "ENERGY_CLASSIFICATION_E", + "ENERGY_CLASSIFICATION_D", + "ENERGY_CLASSIFICATION_C", + "ENERGY_CLASSIFICATION_B", + "ENERGY_CLASSIFICATION_A", + "ENERGY_CLASSIFICATION_NC" + ], + "epcEnergyScore": { "max": "number", "min": "number" }, + "epcClimateDate": { "max": "date", "min": "date" }, + "epcEnergyDate": { "max": "date", "min": "date" } + }, + "features": { + "exposure": [ + "EXPOSURE_UNKNOWN", + "EXPOSURE_NORTH", + "EXPOSURE_NORTH_EAST", + "EXPOSURE_EAST", + "EXPOSURE_SOUTH_EAST", + "EXPOSURE_SOUTH", + "EXPOSURE_SOUTH_WEST", + "EXPOSURE_WEST", + "EXPOSURE_NORTH_WEST" + ], + "furniture": [ + "UNKNOWN_FURNITURE", + "UNFURNISHED", + "PARTIALLY_FURNISHED", + "FULLY_FURNISHED" + ], + "propertyFloor": { "max": "number", "min": "number" }, + "propertyTotalFloor": { "max": "number", "min": "number" }, + "constructionMaterials": ["string"], + "glazingTypes": ["string"], + "hasThroughExposure": "boolean", + "viewOns": ["string"], + "viewTypes": ["string"] + }, + "heatTypes": [ + "HEAT_TYPE_UNKNOWN", + "HEAT_TYPE_INDIVIDUAL", + "HEAT_TYPE_SHARED", + "HEAT_TYPE_MIX", + "HEAT_TYPE_CITY", + "HEAT_TYPE_CISTERN" + ], + "heatTypeDetails": [ + "HEAT_DETAIL_UNKNOWN", + "HEAT_DETAIL_FLOOR", + "HEAT_DETAIL_CEILING", + "HEAT_DETAIL_FIREPLACE", + "HEAT_DETAIL_INSERT", + "HEAT_DETAIL_AIR_CON", + "HEAT_DETAIL_REVERSIBLE_AIR_CON", + "HEAT_DETAIL_RADIANTS_TUBES", + "HEAT_DETAIL_RADIATOR", + "HEAT_DETAIL_SHEAHS", + "HEAT_DETAIL_CONVECTOR" + ], + "heatings": [ + "HEATING_UNKNOWN", + "HEATING_FUEL_OIL", + "HEATING_ELECTRICAL", + "HEATING_GAS", + "HEATING_BIOMASS", + "HEATING_SOLAR", + "HEATING_GEOTHERMAL", + "HEATING_WOOD", + "HEATING_HEAT_PUMP", + "HEATING_PELLET", + "HEATING_CANADIAN_WELL", + "HEATING_COAL" + ], + "propertyCondition": [ + { + "constructionYear": "number", + "interiorCondition": [ + "INTERIOR_CONDITION_UNKNOWN", + "INTERIOR_CONDITION_EXCELLENT", + "INTERIOR_CONDITION_TO_REFRESH", + "INTERIOR_CONDITION_SMALL_WORKS_TO_BE_PLANNED", + "INTERIOR_CONDITION_MAJOR_WORKS_TO_BE_PALLNED", + "INTERIOR_CONDITION_BRAND_NEW", + "INTERIOR_CONDITION_GOOD_CONDITION", + "INTERIOR_CONDITION_TO_BE_RENOVATED" + ], + "renovationYear": "number", + "generalConditions": ["string"] + } + ], + "roomCount": { "max": "number", "min": "number" }, + "surface": { + "balconies": { "max": "number", "min": "number" }, + "floorSpace": { "max": "number", "min": "number" }, + "gardens": { "max": "number", "min": "number" }, + "groundFloor": { "max": "number", "min": "number" }, + "kitchen": { "max": "number", "min": "number" }, + "livingSpace": { "max": "number", "min": "number" }, + "livingroom": { "max": "number", "min": "number" }, + "terraces": { "max": "number", "min": "number" }, + "total": { "max": "number", "min": "number" } + }, + "type": [ + "PROPERTY_TYPE_UNKNOWN", + "PROPERTY_TYPE_STUDIO", + "PROPERTY_TYPE_T1", + "PROPERTY_TYPE_T1_T2", + "PROPERTY_TYPE_T2", + "PROPERTY_TYPE_T2_T3", + "PROPERTY_TYPE_T3", + "PROPERTY_TYPE_T3_4", + "PROPERTY_TYPE_T4", + "PROPERTY_TYPE_T4_5", + "PROPERTY_TYPE_T5_MORE", + "PROPERTY_TYPE_LOFT", + "PROPERTY_TYPE_DUPLEX", + "PROPERTY_TYPE_OTHER_APARTMENT_TYPE", + "PROPERTY_TYPE_INDIVIDUAL", + "PROPERTY_TYPE_ONE_SIDE_TERRACED", + "PROPERTY_TYPE_TWO_SIDE_TERRACED", + "PROPERTY_TYPE_SINGLE_STOREY", + "PROPERTY_TYPE_TRADITIONAL", + "PROPERTY_TYPE_CONTEMPORARY", + "PROPERTY_TYPE_BOURGEOIS", + "PROPERTY_TYPE_VILLA", + "PROPERTY_TYPE_MANOR", + "PROPERTY_TYPE_CASTLE", + "PROPERTY_TYPE_FARM", + "PROPERTY_TYPE_MAS", + "PROPERTY_TYPE_BASTIDE", + "PROPERTY_TYPE_CHALET", + "PROPERTY_TYPE_ANCIENT", + "PROPERTY_TYPE_HERITAGE_LISTED", + "PROPERTY_TYPE_BUNGALOW" + ], + "wcCount": { "max": "number", "min": "number" } + }, + "isUrgent": "boolean", + "land": { + "canConstruct": "boolean", + "isServiced": "boolean", + "surface": { "max": "number", "min": "number" }, + "surfaceConstructable": { "max": "number", "min": "number" }, + "type": [ + "LAND_UNKNOWN", + "LAND_BUILDING_PLOT", + "LAND_AGRICULTURAL", + "LAND_VINEYARD", + "LAND_INDUSTRIAL", + "LAND_POND", + "LAND_FOREST" + ], + "haveBuildingPermit": "boolean", + "haveElectricity": "boolean", + "haveTelecom": "boolean", + "haveWater": "boolean" + }, + "location": [ + { + "city": "string", + "cityCoordinate": { "location": { "lon": "number", "lat": "number" } }, + "department": "string", + "inseeCode": "string", + "irisCode": "string", + "locationCoordinate": { + "location": { "lon": "number", "lat": "number" } + }, + "postalCode": "string" + } + ], + "meta": { + "firstSeenAt": { "max": "date", "min": "date" }, + "isTotallyOffline": "boolean", + "lastPublishedAt": { "max": "date", "min": "date" }, + "lastSeenAt": { "max": "date", "min": "date" }, + "lastUpdatedAt": { "max": "date", "min": "date" } + }, + "parking": { + "count": { "max": "number", "min": "number" }, + "numberOfCars": { "max": "number", "min": "number" }, + "surface": { "max": "number", "min": "number" }, + "type": ["PARKING_UNKNOWN", "PARKING_GARAGE", "PARKING_PARKING"] + }, + "price": { + "currency": ["CURRENCY_EUR", "CURRENCY_USD"], + "initial": { + "source": { "flxId": "string", "url": "string", "website": "string" }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "isAuction": "boolean", + "latest": { + "source": { "flxId": "string", "url": "string", "website": "string" }, + "value": { "max": "number", "min": "number" }, + "valuePerArea": { "max": "number", "min": "number" } + }, + "scope": ["PRICING_ONE_OFF", "PRICING_MENSUAL"], + "warrantyDeposit": { "max": "number", "min": "number" }, + "variation": [ + { + "sinceLastModified": { "max": "number", "min": "number" }, + "sincePublished": { "max": "number", "min": "number" } + } + ] + }, + "process": [ + "PROCESS_UNKNOWN", + "PROCESS_AVAILABLE_ON_MARKET", + "PROCESS_UNDER_COMPROMISE", + "PROCESS_RENTED_SOLD", + "PROCESS_REMOVED", + "PROCESS_RESERVED", + "PROCESS_ARCHIVED" + ], + "tags": ["string"], + "type": [ + "CLASS_UNKNOWN", + "CLASS_HOUSE", + "CLASS_FLAT", + "CLASS_PROGRAM", + "CLASS_SHOP", + "CLASS_PREMISES", + "CLASS_OFFICE", + "CLASS_LAND", + "CLASS_BUILDING", + "CLASS_PARKING" + ], + "hasAnomaly": "boolean", + "offer": [ + { + "isCurrentlyOccupied": "boolean", + "renting": { + "isColocation": "boolean", + "isLongTerm": "boolean", + "isShortTerm": "boolean", + "isSubLease": "boolean" + }, + "type": [ + "OFFER_UNKNOWN", + "OFFER_BUY", + "OFFER_RENT", + "OFFER_BUSINESS_TAKE_OVER", + "OFFER_LEASE_BACK", + "OFFER_LIFE_ANNUITY_SALE", + "OFFER_HOLIDAYS" + ] + } + ] +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..a9a3f9b --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; + +import { clearSessionToken, getSessionToken, setSessionToken } from "@/lib/api"; +import { ProfilesTab } from "@/features/profiles/ProfilesTab"; +import { ScrapersTab } from "@/features/scrapers/ScrapersTab"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +function SessionTokenPanel() { + const [token, setToken] = useState(() => getSessionToken() ?? ""); + const [feedback, setFeedback] = useState(null); + + useEffect(() => { + setToken(getSessionToken() ?? ""); + }, []); + + const handleSave = () => { + setSessionToken(token.trim()); + setFeedback("Token sauvegarde."); + }; + + const handleClear = () => { + clearSessionToken(); + setToken(""); + setFeedback("Token efface."); + }; + + return ( + + + Session API + + Fournissez le jeton recu depuis /auth/login pour authentifier les appels. + + + +
+ { + setToken(event.target.value); + setFeedback(null); + }} + /> +
+ + +
+
+ {feedback ? ( +

{feedback}

+ ) : null} +
+
+ ); +} + +function App() { + return ( +
+ + + + Profils + Scrapers + + + + + + + + +
+ ); +} + +export default App; diff --git a/frontend/src/ScraperPage.tsx b/frontend/src/ScraperPage.tsx new file mode 100644 index 0000000..ae3528a --- /dev/null +++ b/frontend/src/ScraperPage.tsx @@ -0,0 +1,26 @@ +import { useForm } from "react-hook-form"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./components/ui/card"; + +export type ScraperFormData = { + name: "string"; + params: "string"; +}; + +export const ScraperPage = () => { + const {} = useForm(); + return ( + + + + + + + + ); +}; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/json-fields.tsx b/frontend/src/components/json-fields.tsx new file mode 100644 index 0000000..5600311 --- /dev/null +++ b/frontend/src/components/json-fields.tsx @@ -0,0 +1,392 @@ +import { useEffect, useMemo, useState } from "react"; +import { + BracesIcon, + CodeIcon, + PlusIcon, + Trash2Icon, +} from "lucide-react"; + +import { cn } from "@/lib/utils"; + +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { Textarea } from "./ui/textarea"; + +type JsonPrimitiveType = "string" | "number" | "boolean" | "null" | "json"; + +const jsonTypeLabels: Record = { + string: "Texte", + number: "Nombre", + boolean: "Booleen", + null: "Null", + json: "JSON", +}; + +export type JsonObject = Record; + +interface JsonEntry { + id: string; + key: string; + type: JsonPrimitiveType; + value: string; +} + +export interface JsonBuilderProps { + value?: JsonObject; + onChange: (nextValue: JsonObject) => void; + label?: string; + description?: string; + error?: string; + className?: string; +} + +function createEntry(partial?: Partial): JsonEntry { + const id = typeof crypto !== "undefined" && crypto.randomUUID + ? crypto.randomUUID() + : Math.random().toString(36).slice(2); + + return { + id, + key: "", + type: "string", + value: "", + ...partial, + }; +} + +function formatJson(value?: JsonObject) { + return JSON.stringify(value ?? {}, null, 2); +} + +function isPlainObject(value: unknown): value is JsonObject { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function entriesFromValue(value?: JsonObject): JsonEntry[] { + if (!value || !isPlainObject(value)) { + return [createEntry()]; + } + + if (Object.keys(value).length === 0) { + return [createEntry()]; + } + + return Object.entries(value).map(([key, raw]) => { + if (raw === null) { + return createEntry({ key, type: "null", value: "" }); + } + + if (typeof raw === "boolean") { + return createEntry({ key, type: "boolean", value: raw ? "true" : "false" }); + } + + if (typeof raw === "number") { + return createEntry({ key, type: "number", value: String(raw) }); + } + + if (typeof raw === "string") { + return createEntry({ key, type: "string", value: raw }); + } + + return createEntry({ key, type: "json", value: JSON.stringify(raw, null, 2) }); + }); +} + +function parseEntries(entries: JsonEntry[]) { + const result: JsonObject = {}; + const errors: Record = {}; + const usedKeys = new Set(); + + entries.forEach((entry) => { + const trimmedKey = entry.key.trim(); + + if (!trimmedKey) { + errors[entry.id] = "La cle est obligatoire."; + return; + } + + if (usedKeys.has(trimmedKey)) { + errors[entry.id] = "Cette cle est deja utilisee."; + return; + } + + switch (entry.type) { + case "string": { + result[trimmedKey] = entry.value; + break; + } + case "number": { + if (entry.value.trim() === "") { + errors[entry.id] = "Entrez un nombre."; + return; + } + + const numeric = Number(entry.value); + + if (Number.isNaN(numeric)) { + errors[entry.id] = "Valeur numerique invalide."; + return; + } + + result[trimmedKey] = numeric; + break; + } + case "boolean": { + result[trimmedKey] = entry.value === "true"; + break; + } + case "null": { + result[trimmedKey] = null; + break; + } + case "json": { + if (entry.value.trim() === "") { + errors[entry.id] = "Entrez un JSON valide."; + return; + } + + try { + result[trimmedKey] = JSON.parse(entry.value); + } catch (parseError) { + errors[entry.id] = "JSON invalide."; + } + + break; + } + default: { + errors[entry.id] = "Type non supporte."; + } + } + + usedKeys.add(trimmedKey); + }); + + return { result, errors }; +} + +export function JsonBuilderField({ + value, + onChange, + label, + description, + error, + className, +}: JsonBuilderProps) { + const [mode, setMode] = useState<"builder" | "raw">("builder"); + const [entries, setEntries] = useState(() => entriesFromValue(value)); + const [entryErrors, setEntryErrors] = useState>({}); + const [rawValue, setRawValue] = useState(() => formatJson(value)); + const [rawError, setRawError] = useState(null); + + useEffect(() => { + setEntries(entriesFromValue(value)); + setRawValue(formatJson(value)); + }, [value]); + + const hasBuilderErrors = useMemo( + () => Object.keys(entryErrors).length > 0, + [entryErrors] + ); + + useEffect(() => { + if (mode === "builder") { + const { result, errors } = parseEntries(entries); + setEntryErrors(errors); + + if (Object.keys(errors).length === 0) { + onChange(result); + setRawValue(formatJson(result)); + setRawError(null); + } + } + }, [entries, mode, onChange]); + + const handleAddEntry = () => { + setEntries((current) => [...current, createEntry()]); + }; + + const handleRemoveEntry = (id: string) => { + setEntries((current) => current.filter((entry) => entry.id !== id)); + }; + + const handleEntryChange = (id: string, update: Partial>) => { + setEntries((current) => + current.map((entry) => (entry.id === id ? { ...entry, ...update } : entry)) + ); + }; + + const builderContent = ( +
+ {entries.map((entry) => { + const entryError = entryErrors[entry.id]; + + return ( +
+
+ handleEntryChange(entry.id, { key: event.target.value })} + /> + + + + {entry.type === "boolean" ? ( + + ) : entry.type === "json" ? ( +