commit 2cb80055409bbb3cad7e45da479be7852060c8ae Author: Yashwin Date: Mon Jan 19 19:36:31 2026 +0530 first commit diff --git a/.cursor/rules/qassurerules.mdc b/.cursor/rules/qassurerules.mdc new file mode 100644 index 0000000..4b6b598 --- /dev/null +++ b/.cursor/rules/qassurerules.mdc @@ -0,0 +1,320 @@ +--- +alwaysApply: true +--- +# QAssure.ai Management Console - Development Rules + +## Initial Response Protocol +Let's Start Building Frontend + +## Scope Control & Permission Protocol (CRITICAL) + +### Strict Scope Adherence +1. **ONLY** work on the specific component/feature explicitly requested +2. **NEVER** create additional components, files, or features without explicit permission +3. **STOP** after completing the requested task - do not continue or expand scope +4. **ASK** before doing anything beyond the original instruction + +### When to Ask Permission +You MUST ask for explicit approval before: +- Creating any file not explicitly requested +- Adding any dependencies not mentioned in the request +- Implementing related/adjacent features "to be helpful" +- Refactoring existing code not part of the request +- Creating utility functions, hooks, or services beyond scope +- Setting up folder structures not specifically asked for +- Adding tests, documentation, or configuration files + +### Completion Protocol +After finishing the requested component/task: +1. Confirm completion: "✅ [Component Name] has been created" +2. List what was delivered (files created, dependencies used) +3. **STOP and WAIT** for next instruction +5. **DO NOT** ask "Should I create X next?" - wait for explicit request + +## Technology Stack (STRICT - No Deviations) +- **Framework**: React 19.2.0 (Functional Components Only) +- **Build Tool**: Vite 7.2.4 +- **Language**: TypeScript 5.9.3 (Strict Mode) +- **Styling**: Tailwind CSS 4.1.18 +- **Routing**: React Router DOM 7.12.0 +- **State Management**: Redux Toolkit 2.11.2 +- **UI Components**: shadcn/ui (Radix UI primitives) +- **Forms**: React Hook Form 7.71.1 +- **API Client**: Axios 1.13.2 +- **Charts**: Recharts 3.6.0 +- **Icons**: Lucide React 0.562.0 + +## Dependency Management Rules +1. **NEVER** install libraries not listed in package.json without explicit approval, Only Install with permission. +2. Check `package.json` before suggesting any import +3. Use only existing dependencies; suggest additions only when critical +4. All imports must use exact package names from package.json + +## Figma MCP Automation Workflow +1. **Design Fetch**: Use Figma MCP to retrieve component designs +2. **Analysis Phase**: Identify component structure, variants, and responsive breakpoints +3. **Implementation**: Convert Figma designs to shadcn/ui + Tailwind CSS components +4. **Validation**: Ensure pixel-perfect accuracy across all breakpoints (320px - 1920px) +5. **Never** hardcode colors/spacing - extract to Tailwind config or CSS variables + +## Code Architecture & File Structure (MANDATORY) + +``` +src/ +├── components/ # Reusable UI components +│ ├── ui/ # shadcn/ui components +│ └── shared/ # Custom shared components +├── features/ # Feature-based modules (Dashboard, Modules, AI, etc.) +│ └── [feature]/ +│ ├── components/ # Feature-specific components +│ ├── hooks/ # Feature-specific hooks +│ └── services/ # Feature-specific API calls +├── hooks/ # Global custom hooks +├── services/ # API integration layer (Axios instances) +├── store/ # Redux store, slices, actions +├── types/ # TypeScript type definitions +├── utils/ # Utility/helper functions +├── constants/ # App constants, enums +├── styles/ # Global styles, Tailwind config +└── pages/ # Route-level page components +``` + +## File Size Constraints (HARD LIMITS) +- **React Components**: Maximum 500 lines (including imports/exports) +- **Hooks/Utils/Services**: Maximum 200 lines per file +- **Types**: Maximum 300 lines per file +- **Violation Action**: MUST refactor into smaller, focused modules + +## Component Development Standards + +### Component Structure Template +```tsx +// 1. Imports (grouped: React, external, internal, types, styles) +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { useAppSelector } from '@/hooks/redux-hooks'; +import type { ModuleStatus } from '@/types/module'; + +// 2. Types/Interfaces +interface ComponentProps { +title: string; +onAction: () => void; +} + +// 3. Component (functional, typed props, return type) +export const ComponentName = ({ title, onAction }: ComponentProps): JSX.Element => { +// Hooks first +const [state, setState] = useState(''); +const navigate = useNavigate(); + +// Event handlers +const handleClick = () => { +// Implementation +}; + +// Early returns for conditional rendering +if (!title) return
Loading...
; + +// Main render +return
{/* JSX */}
; +}; +``` + +### Component Rules +- **Functional components only** (no class components) +- **Props destructuring** in function signature +- **Explicit return types**: `: JSX.Element` or `: React.ReactNode` +- **Custom hooks** for reusable logic (prefix with `use`) +- **Compound components** for complex UIs (e.g., `Card.Header`, `Card.Body`) + +## Naming Conventions (NON-NEGOTIABLE) + +| Element | Convention | Example | +|---------|-----------|---------| +| Component | PascalCase | `UserDashboard`, `ModuleCatalog` | +| Function/Hook | camelCase | `getUserById`, `useModuleStatus` | +| Variable | camelCase | `moduleList`, `isActive` | +| Constant | SCREAMING_SNAKE | `API_BASE_URL`, `MAX_RETRIES` | +| Type/Interface | PascalCase | `ModuleConfig`, `UserRole` | +| File (Component) | PascalCase | `ModuleCatalog.tsx` | +| File (Other) | kebab-case | `api-client.ts`, `format-date.ts` | +| CSS Class | kebab-case | `module-card`, `status-badge` | + +### Boolean Naming +Prefix with `is`, `has`, `can`, `should`: `isLoading`, `hasPermission`, `canEdit` + +## Responsive Design (Tailwind Mobile-First) + +### Breakpoints (Tailwind 4) +``` +Default (mobile): 320px - 767px (no prefix) +md: ≥768px (Tablet) +lg: ≥1024px (Laptop) +xl: ≥1440px (Desktop) +2xl: ≥1920px (Large Display) +``` + +### Implementation Rules +1. **Mobile-first**: Start with mobile layout, enhance with `md:`, `lg:`, `xl:`, `2xl:` +2. **Touch targets**: Minimum `min-h-[44px] min-w-[44px]` for interactive elements +3. **Responsive patterns**: +```tsx +// Layout +
+ +// Grid +
+ +// Typography +

+ +// Spacing +
+``` +4. **NEVER** use fixed pixel widths; use `w-full`, `max-w-*`, percentage-based utilities + +## State Management (Redux Toolkit) + +### Slice Structure +```typescript +// features/modules/store/moduleSlice.ts +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; + +export const fetchModules = createAsyncThunk('modules/fetch', async () => { +// API call +}); + +const moduleSlice = createSlice({ +name: 'modules', +initialState: { items: [], loading: false, error: null }, +reducers: { /* sync actions */ }, +extraReducers: (builder) => { /* async actions */ } +}); +``` + +### Usage Rules +- **Async operations**: Use `createAsyncThunk` +- **Selectors**: Create memoized selectors with `createSelector` +- **Typed hooks**: Use `useAppDispatch`, `useAppSelector` (typed versions) + +## API Communication (Axios) + +### Service Layer Pattern +```typescript +// services/api/module-service.ts +import apiClient from '@/services/api-client'; +import type { Module, CreateModuleDto } from '@/types/module'; + +export const moduleService = { +getAll: () => apiClient.get('/modules'), +getById: (id: string) => apiClient.get(`/modules/${id}`), +create: (data: CreateModuleDto) => apiClient.post('/modules', data), +}; +``` + +### Rules +- **Centralized instance**: Single Axios instance with interceptors +- **Error handling**: Global error interceptor + component-level handling +- **Type safety**: Always type API responses +- **Loading states**: Track in Redux or local state + +## Form Handling (React Hook Form) + +```typescript +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const schema = z.object({ name: z.string().min(1) }); + +const MyForm = () => { +const { register, handleSubmit, formState: { errors } } = useForm({ +resolver: zodResolver(schema) +}); + +return
...
; +}; +``` + +## Error Handling + +### Component Error Boundaries +```tsx +// Wrap async components +}> + + +``` + +### Try-Catch Pattern +```typescript +try { +await apiCall(); +} catch (error) { +console.error('Context:', error); +// User-friendly message +toast.error('Failed to load data. Please try again.'); +} +``` + +## Code Quality Rules +1. **DRY**: Extract repeated logic into utils/hooks +2. **SRP**: One purpose per file/component/function +3. **KISS**: Prefer clarity over cleverness +4. **Max nesting**: 3 levels; refactor if deeper +5. **No any**: Use proper TypeScript types +6. **No inline styles**: Use Tailwind classes only + +## Performance Optimization +- **Code splitting**: Lazy load routes with `React.lazy()` +- **Memoization**: Use `useMemo`, `useCallback`, `React.memo` appropriately +- **List virtualization**: Use `react-window` for long lists (>100 items) +- **Image optimization**: WebP format, lazy loading, responsive images + +## Security Guidelines +- **Never** store tokens in localStorage (use httpOnly cookies) +- **Validate inputs**: Client-side + server-side validation +- **Sanitize outputs**: Prevent XSS with proper escaping +- **CORS**: Configure allowed origins properly + +## Prohibited Practices +1. ❌ Class components +2. ❌ Inline function definitions in JSX (unless trivial) +3. ❌ `any` type without explicit justification +4. ❌ Direct DOM manipulation (use refs sparingly) +5. ❌ Hardcoded strings (use constants/i18n) +6. ❌ Magic numbers (define as constants) +7. ❌ Unused imports/variables +8. ❌ Console.log in production code (use proper logging) +9. ❌ **Creating components/files beyond the specific request** +10. ❌ **Expanding scope without explicit permission** +11. ❌ **Installing dependencies without asking first** + +## Documentation Policy +**DO NOT** create documentation (README, API docs, guides) unless explicitly requested. +Focus solely on code implementation following these rules. + +## Code Review Checklist (Self-Audit) +- [ ] Follows file structure and naming conventions? +- [ ] Within file size limits? +- [ ] Fully responsive (320px - 1920px)? +- [ ] Proper TypeScript typing (no `any`)? +- [ ] Error handling implemented? +- [ ] No code duplication? +- [ ] Accessible (ARIA labels, keyboard navigation)? +- [ ] Performance optimized (memoization, lazy loading)? + +## Specific Instructions +1. When fetching Figma designs, analyze component hierarchy first +2. Generate TypeScript interfaces from Figma component properties +3. Map Figma styles to Tailwind classes (use closest utility) +4. Create responsive variants based on Figma frames +5. Suggest shadcn/ui components that match design patterns +6. Always ask for clarification if Figma design is ambiguous +7. Provide component usage examples after generation +8. **CRITICAL**: Ask permission before installing any new dependencies or shadcn components +9. **CRITICAL**: If you identify missing utilities/types needed, ASK before creating them + +--- + diff --git a/.env b/.env new file mode 100644 index 0000000..e4d05f7 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3000/api/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.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/README.md b/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# 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 not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## 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/components.json b/components.json new file mode 100644 index 0000000..8f00892 --- /dev/null +++ b/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": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/docs/project-context.md b/docs/project-context.md new file mode 100644 index 0000000..13862a8 --- /dev/null +++ b/docs/project-context.md @@ -0,0 +1,491 @@ +# QAssure.ai Management Console - Project Context + +## Project Overview + +**Product Name**: QAssure.ai Platform Management Console +**Organization**: L&T Technology Services - Enterprise Quality Management Division +**Version**: 1.0 +**Document Date**: December 2025 +**Development Phase**: Frontend Implementation + +### What is QAssure.ai? + +QAssure.ai is an AI-powered enterprise quality management platform designed for regulated industries (medical devices, pharmaceuticals, life sciences). The platform uses artificial intelligence to automate quality management tasks including complaint handling, clinical evaluations, design history files, and regulatory compliance. + +### What is the Management Console? + +The Management Console is a web-based application that provides a visual, user-friendly interface for managing and monitoring the entire QAssure.ai platform. It serves as the "control center" where administrators, operators, and support teams can manage the platform without writing code or using command-line tools. + +**Key Principle**: The Management Console complements (not replaces) the existing CLI and SDK tools, providing a visual alternative for users who prefer graphical interfaces. + +--- + +## User Personas & Use Cases + +### 1. Platform Administrators +**Role**: Senior technical staff responsible for platform health and security +**Primary Tasks**: +- Configure platform-wide settings +- Manage user accounts and permissions +- Monitor system health +- Ensure security compliance +- Respond to critical incidents + +**Console Features They Use**: +- Main Dashboard (health overview) +- User and Access Management +- System Configuration +- Audit Logs +- Security Settings + +### 2. DevOps Engineers +**Role**: Technical staff deploying and maintaining infrastructure +**Primary Tasks**: +- Deploy new module versions +- Scale services based on demand +- Monitor server resources +- Troubleshoot performance issues +- Manage deployment pipelines + +**Console Features They Use**: +- Module Management (deployment wizard) +- System Health Monitoring +- Log Viewer +- Request Tracing +- Performance Metrics + +### 3. AI/ML Engineers +**Role**: Specialists managing AI components +**Primary Tasks**: +- Configure AI models and providers +- Manage prompts (AI instructions) +- Monitor AI performance and costs +- Optimize AI response quality +- Manage knowledge bases (RAG) + +**Console Features They Use**: +- AI Management (providers, models) +- Prompt Management (editor, playground) +- Knowledge Base Management +- AI Cost Tracking +- AI Usage Analytics + +### 4. Support Engineers +**Role**: Staff responding to user issues +**Primary Tasks**: +- Investigate reported issues +- Search system logs +- Identify error causes +- Trace request flows +- Coordinate fixes with teams + +**Console Features They Use**: +- Log Viewer +- Request Tracing +- Alerts and Notifications +- System Health Monitoring +- Troubleshooting Tools + +### 5. Tenant Administrators +**Role**: Customer organization administrators +**Primary Tasks**: +- Manage their organization's users +- Configure tenant-specific settings +- View usage reports +- Integrate with existing QMS systems +- Monitor their environment health + +**Console Features They Use**: +- Tenant Dashboard +- QMS Integration Setup +- Usage Reports +- User Management (tenant-scoped) + +--- + +## Core Feature Areas + +### 1. Main Dashboard (Landing Page) + +**Purpose**: Instant overview of entire platform status at a glance + +**Key Components**: +- **Platform Health Score** (0-100): Single metric aggregating all system health indicators (color-coded: green/yellow/red) +- **Module Status Display**: Visual cards for each module (AiCE, AiCH, DHF, SQ) showing status (Active, Degraded, Suspended, Offline) +- **Real-Time Activity Metrics**: Live graphs updating every few seconds (requests/sec, response times, error rates) +- **Recent Alerts Panel**: List of recent issues requiring attention with severity indicators +- **AI Usage Summary**: Today's AI requests, token consumption, estimated costs +- **Quick Actions**: Buttons for frequent tasks (deploy module, create tenant, scale service) + +**Design Considerations**: +- Information density: Show maximum relevant data without overwhelming +- Glanceability: Critical status visible within 3 seconds +- Actionability: Direct access to common tasks from dashboard + +### 2. Module Management + +**Purpose**: Control and monitor all QAssure platform modules + +**Modules in QAssure.ai**: +- **AiCE**: AI-powered Complaint Evaluation +- **AiCH**: AI-powered Clinical Evaluation +- **DHF**: Design History File Management +- **SQ**: Supplier Quality Management +- **Custom Modules**: Tenant-specific or future modules + +**Key Features**: + +#### Module Catalog +- Browsable library of all modules +- Each module shows: name, description, icon, version, status, tech stack, resource usage, dependencies +- Search and filter capabilities +- Click-through to detailed module pages + +#### Deployment Wizard +Multi-step process for safe module updates: +1. Select module and target version +2. Review changelog (what's new/fixed) +3. Choose deployment strategy: + - **Blue-Green**: New version runs alongside old; traffic gradually shifts + - **Immediate**: Instant switch (for urgent hotfixes) +4. Configure traffic split (e.g., start with 10% of users) +5. Review settings and confirm +6. Monitor deployment progress with real-time health checks +7. **Safety Features**: One-click rollback, automatic health validation, approval workflows + +#### Module Lifecycle Management +Transition modules through stages: +- **Development** → **Testing** → **Staging** → **Active** → **Suspended** → **Deprecated** → **Retired** +- Actions: Suspend (immediately stop traffic), Deprecate (notify users of retirement date), Retire (permanent shutdown) + +### 3. AI Management + +**Purpose**: Configure, monitor, and optimize AI capabilities + +**Key Features**: + +#### AI Providers and Models +- **Supported Providers**: OpenAI (GPT-4), Anthropic (Claude), Google (Gemini) +- **Configuration**: Add/edit API keys, set spending limits, configure backup providers +- **Model Routing**: Assign specific models to different tasks (complex tasks → powerful model; simple tasks → cheaper model) +- **Health Monitoring**: Real-time status and response time for each provider + +#### Prompt Management +- **Prompt Editor**: Specialized text editor with syntax highlighting, variable placeholders, version history, side-by-side comparison +- **Testing Playground**: Test prompts with sample inputs, compare responses across models, measure performance, save test cases +- **Version Control**: Track all changes, revert to previous versions, see diff between versions + +#### Knowledge Base Management (RAG) +RAG = Retrieval-Augmented Generation (giving AI access to specific documents for accurate responses) +- **Document Upload**: PDFs, Word docs, web pages +- **Organization**: Collections for different purposes (regulations, manuals, policies) +- **Processing Status**: Track document processing progress +- **Search Testing**: Verify AI can find relevant information +- **Cleanup**: Remove outdated documents + +#### AI Cost Tracking +- **Breakdowns**: By provider, by module, by tenant, by use case +- **Time Ranges**: Today, week, month, custom +- **Trends**: Visualize usage patterns over time +- **Alerts**: Notify when spending exceeds thresholds + +### 4. Tenant Management + +**Purpose**: Manage multiple customer organizations (multi-tenancy) + +**Key Concepts**: +- **Multi-tenant Architecture**: Single platform instance serves multiple customers with isolated data +- **Tenant**: A customer organization with its own isolated environment + +**Key Features**: + +#### Tenant Onboarding Wizard +6-step process: +1. Company information (name, industry, contact) +2. Subscription plan selection +3. Module access configuration +4. Initial admin user setup +5. QMS integration settings +6. Review and create + +#### Tenant Dashboard +- List of all tenants with status (active, suspended, trial) +- Usage metrics per tenant (API calls, AI usage, storage) +- Subscription details and renewal dates +- Health status indicators +- Recent activity and support tickets + +#### QMS Integration Setup +Connect QAssure to existing Quality Management Systems: +- **Supported Systems**: Veeva Vault, MasterControl, Arena PLM, TrackWise, others +- **Configuration**: API credentials, field mapping (map QAssure fields to QMS fields), sync settings (frequency, direction, conflict resolution) +- **Testing**: Validate connection before activation +- **Monitoring**: View sync status and errors + +### 5. User and Access Management + +**Purpose**: Control who can access the platform and what they can do + +**Key Features**: + +#### User Management +- User directory (all users with roles, last login, status) +- CRUD operations (create, edit, deactivate accounts) +- Password reset and account unlocking +- Activity logs (login history, actions performed) + +#### Roles and Permissions (RBAC) +- **Role-Based Access Control**: Users assigned roles; roles contain permissions +- **Built-in Roles**: Administrator, Operator, Viewer, Module Admin, Tenant Admin +- **Custom Roles**: Create organization-specific roles +- **Permission Editor**: Visual grid showing resources × actions with checkboxes +- **Effective Permissions**: See exactly what a user can do (combined from all roles) + +#### Single Sign-On (SSO) +- **Supported Providers**: Microsoft Azure AD, Okta, Auth0, Google Workspace, any SAML 2.0 / OAuth 2.0 provider +- **Configuration Wizard**: Step-by-step SSO setup +- **User Experience**: Redirect to corporate login page, seamless authentication + +### 6. Monitoring and Troubleshooting + +**Purpose**: Understand platform behavior and diagnose issues + +**Key Features**: + +#### System Health Monitoring +Real-time metrics: +- **Servers**: CPU, memory, disk, network traffic +- **Databases**: Connections, query performance, storage +- **Caches**: Hit rate, memory usage +- **Message Queues**: Queue depth, consumer lag +- **APIs**: Requests/sec, response times, error rates + +Visualization: Time-series charts, zoom/pan, custom time ranges, comparison views + +#### Log Viewer +- **Search**: Keyword, error code, free-text search +- **Filters**: Time range, module, severity (error/warning/info), tenant +- **Live Tail**: Watch logs in real-time +- **Export**: Download for offline analysis +- **Saved Searches**: Bookmark frequent investigations + +#### Request Tracing (Distributed Tracing) +- **Purpose**: Follow a single request through multiple microservices +- **Timeline View**: See time spent in each service +- **Bottleneck Identification**: Spot slow components +- **Drill-Down**: Click any step for detailed logs +- **Use Case Example**: "Report generation is slow" → Trace request → Find AI call took 10s → Optimize + +#### Alerts and Notifications +- **Alert Types**: Threshold (CPU > 80%), Anomaly (unusual pattern), Availability (service down), Error Rate (errors > 5%) +- **Notification Channels**: Email, Slack, PagerDuty, SMS, In-app +- **Configuration**: Create custom alerts with conditions and channels +- **Alert Management**: View, acknowledge, resolve alerts + +### 7. Reporting and Analytics + +**Purpose**: Insights for decision-making and stakeholder communication + +**Report Types**: + +#### Usage Reports +- Total API calls over time +- Active users and login frequency +- Most-used features +- Storage consumption by tenant +- AI usage (requests, tokens, costs) + +**Users**: Account managers, product managers, finance teams, capacity planners + +#### Performance Reports +- **Availability**: Uptime percentage +- **Response Time**: Average, P50, P95, P99 +- **Error Rate**: Percentage of failed requests +- **SLA Compliance**: Met/missed service level objectives + +**Users**: Operations teams, customer success managers, engineering teams + +#### Cost Reports +- Infrastructure costs (cloud compute, storage, networking) +- AI costs by provider, model, use case +- Cost allocation by tenant/department +- Cost trends and forecasts + +**Export Formats**: PDF, Excel, CSV, Scheduled email delivery + +### 8. System Configuration + +**Purpose**: Platform-wide settings affecting all users + +**Categories**: + +#### General Settings +- Platform name and branding (logo, colors) +- Default timezone and date format +- Session timeout duration +- Email settings (SMTP server) +- Default UI language + +#### Security Settings +- Password policies (length, complexity, expiration) +- Multi-factor authentication (MFA) requirements +- IP allowlists (restrict access by IP) +- Session management (max concurrent sessions) +- API rate limits (prevent abuse) + +#### Integration Settings +- API gateway configuration (timeouts, retries) +- Event bus settings (message retention) +- External service connections +- Webhook configurations + +#### Audit Log +- Immutable record of all administrative actions +- Tracks: Who, What, When for every change +- Retention: 7 years (compliance requirement) +- Searchable and exportable + +--- + +## Technical Architecture Overview + +### Platform Components +- **Frontend**: React-based Management Console (this project) +- **Backend**: Microservices architecture (out of scope for frontend) +- **Modules**: Independent services (AiCE, AiCH, DHF, SQ) +- **AI Layer**: Integration with OpenAI, Anthropic, Google AI +- **Data Layer**: Databases, caches, message queues +- **Integration Layer**: API gateway, event bus, external QMS connectors + +### Multi-Tenancy Model +- **Data Isolation**: Each tenant's data completely separated +- **Resource Sharing**: Compute and infrastructure shared for efficiency +- **Customization**: Per-tenant configuration and branding + +### Authentication & Authorization Flow +1. User logs in (SSO or username/password) +2. Backend validates credentials +3. JWT token issued (httpOnly cookie) +4. Frontend reads user roles and permissions +5. UI renders based on permissions +6. API calls include token in Authorization header +7. Backend validates token and permissions for each request + +--- + +## Design System & UI Patterns + +### Visual Design Principles +- **Clarity**: Information hierarchy is obvious +- **Consistency**: Same patterns throughout +- **Efficiency**: Minimize clicks to complete tasks +- **Feedback**: Immediate response to user actions +- **Accessibility**: WCAG 2.1 AA compliant + +### Color Semantics +- **Green**: Healthy, success, active +- **Yellow/Amber**: Warning, degraded, attention needed +- **Red**: Error, critical, offline +- **Blue**: Informational, primary actions +- **Gray**: Neutral, disabled, secondary + +### Component Patterns +- **Cards**: Module status, tenant summaries, metric displays +- **Tables**: User lists, log entries, audit records +- **Graphs/Charts**: Time-series metrics, usage trends, cost breakdowns +- **Wizards**: Multi-step processes (deployment, onboarding) +- **Forms**: Configuration, user creation, settings +- **Modals/Dialogs**: Confirmations, quick edits +- **Toasts/Notifications**: Success/error feedback + +### Layout Patterns +- **Dashboard Layout**: Grid of metric cards + quick actions +- **Master-Detail**: List on left, details on right (e.g., module catalog) +- **Tabbed Interface**: Organize related settings (AI Management tabs: Providers, Prompts, Knowledge Base, Costs) +- **Full-Screen Views**: Log viewer, request tracing (maximize information density) + +--- + +## Key Technical Requirements + +### Performance Targets +- **First Contentful Paint (FCP)**: < 1.8s +- **Largest Contentful Paint (LCP)**: < 2.5s +- **Time to Interactive (TTI)**: < 3.8s +- **Bundle Size (main)**: < 250KB gzipped + +### Browser Support +- **Modern Browsers**: Chrome, Firefox, Safari, Edge (latest 2 versions) +- **No IE11 support** required + +### Accessibility Requirements +- **WCAG 2.1 Level AA** compliance +- **Keyboard Navigation**: All features accessible without mouse +- **Screen Reader Support**: Proper ARIA labels and landmarks +- **Color Contrast**: Minimum 4.5:1 for normal text, 3:1 for large text +- **Focus Indicators**: Visible keyboard focus states + +### Security Considerations +- **No Sensitive Data in URLs**: Use POST requests for sensitive operations +- **XSS Prevention**: Sanitize all user inputs +- **CSRF Protection**: CSRF tokens for state-changing operations +- **Rate Limiting**: Prevent brute force attacks +- **Audit Trail**: Log all administrative actions + +--- + +## Development Workflow (Figma to Code) + +### Phase 1: Design Analysis +1. Fetch Figma designs using MCP +2. Identify components and their variants +3. Map Figma styles to Tailwind utilities +4. Document responsive breakpoints from Figma frames + +### Phase 2: Component Implementation +1. Create TypeScript interfaces from Figma component properties +2. Implement with shadcn/ui base components +3. Style with Tailwind CSS (mobile-first) +4. Add interactivity (state, events, validation) + +### Phase 3: Integration +1. Connect to Redux state +2. Implement API calls with Axios +3. Add error handling and loading states +4. Implement accessibility features + +### Phase 4: Validation +1. Test across all breakpoints (320px - 1920px) +2. Validate against Figma design (pixel-perfect) +3. Test keyboard navigation and screen readers +4. Performance audit with Lighthouse + +--- + +## Glossary of Domain Terms + +- **Module**: Independent service handling a specific QA function (AiCE, AiCH, etc.) +- **Tenant**: Customer organization with isolated environment in multi-tenant system +- **RAG**: Retrieval-Augmented Generation (AI + knowledge base) +- **Prompt**: Instructions given to AI model to guide its response +- **Token**: Unit of AI processing (roughly 0.75 words) +- **Blue-Green Deployment**: Running old and new versions simultaneously, gradually shifting traffic +- **Lifecycle Stage**: Current status of module (Development, Testing, Staging, Active, etc.) +- **RBAC**: Role-Based Access Control (permissions assigned via roles) +- **SSO**: Single Sign-On (use corporate credentials to access QAssure) +- **QMS**: Quality Management System (external systems like Veeva Vault) +- **Health Score**: Aggregated metric (0-100) indicating overall platform status +- **Distributed Tracing**: Following a request through multiple microservices + +--- + +## Future Considerations (Not in Current Scope) + +- **Mobile App**: Native iOS/Android apps (future phase) +- **Offline Support**: Progressive Web App capabilities +- **Advanced Analytics**: Machine learning for anomaly detection +- **White-Labeling**: Custom branding per tenant +- **API Marketplace**: Third-party integrations +- **Workflow Automation**: Visual workflow builder + +--- \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/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.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..92fd9fe --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + qassure-frontend + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0173bec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4629 @@ +{ + "name": "qassure-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qassure-frontend", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-slot": "^1.2.4", + "@reduxjs/toolkit": "^2.11.2", + "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.12.0", + "recharts": "^3.6.0", + "redux-persist": "^6.0.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "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.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", + "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/type-utils": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", + "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", + "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", + "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.0", + "@typescript-eslint/parser": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e52ccac --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "qassure-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-slot": "^1.2.4", + "@reduxjs/toolkit": "^2.11.2", + "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.12.0", + "recharts": "^3.6.0", + "redux-persist": "^6.0.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..fe7a22f --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,79 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Login from "./pages/Login"; +import Dashboard from "./pages/Dashboard"; +import Tenants from "./pages/Tenants"; +import Users from "./pages/Users"; +import NotFound from "./pages/NotFound"; +import ProtectedRoute from "./pages/ProtectedRoute"; +import Roles from "./pages/Roles"; +import Modules from "./pages/Modules"; +import AuditLogs from "./pages/AuditLogs"; + +function App() { + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* Catch-all route for 404 */} + + + + } + /> + + + ); +} + +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..17d61df --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,235 @@ +import { useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ChevronRight, Search, Bell, ChevronDown, Menu, LogOut, User } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks'; +import { logoutAsync } from '@/store/authSlice'; +import { cn } from '@/lib/utils'; +import type { ReactElement } from 'react'; + +interface HeaderProps { + breadcrumbs?: Array<{ label: string; path?: string }>; + currentPage: string; + onMenuClick?: () => void; +} + +export const Header = ({ currentPage, onMenuClick }: HeaderProps): ReactElement => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { user, isLoading } = useAppSelector((state) => state.auth); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + // Get user initials for avatar + const getUserInitials = (): string => { + if (user?.first_name && user?.last_name) { + return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase(); + } + if (user?.email) { + return user.email[0].toUpperCase(); + } + return 'A'; + }; + + // Get user display name + const getUserDisplayName = (): string => { + if (user?.first_name && user?.last_name) { + return `${user.first_name} ${user.last_name}`; + } + return user?.email?.split('@')[0] || 'Admin'; + }; + + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + // Check if click is outside the dropdown container + if (dropdownRef.current && !dropdownRef.current.contains(target)) { + setIsDropdownOpen(false); + } + }; + + if (isDropdownOpen) { + // Use mousedown instead of click to avoid interfering with button clicks + // Add listener in bubble phase (not capture) so button clicks fire first + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isDropdownOpen]); + + // Handle logout + const handleLogout = async (e: React.MouseEvent): Promise => { + e.preventDefault(); + e.stopPropagation(); + + // Close dropdown immediately + setIsDropdownOpen(false); + + try { + // Call logout API with Bearer token + await dispatch(logoutAsync()).unwrap(); + // Clear state and redirect + navigate('/', { replace: true }); + } catch (error: any) { + // Even if API call fails, clear local state and redirect to login + console.error('Logout error:', error); + // Dispatch logout action to clear local state + dispatch({ type: 'auth/logout' }); + navigate('/', { replace: true }); + } + }; + + return ( +
+ {/* Left Side - Menu Button (Mobile) + Breadcrumbs */} +
+ {/* Mobile Menu Button */} + + + {/* Breadcrumbs */} + +
+ + {/* Right Side */} +
+ + + + {/* Desktop User Dropdown */} +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
e.stopPropagation()} + > + {/* User Info Section */} +
+
+
+ +
+
+

+ {getUserDisplayName()} +

+

{user?.email}

+
+
+
+ + {/* Logout Button */} +
+ +
+
+ )} +
+ + {/* Mobile User Avatar */} +
+ + + {/* Mobile Dropdown Menu */} + {isDropdownOpen && ( +
e.stopPropagation()} + > + {/* User Info Section */} +
+
+
+ +
+
+

+ {getUserDisplayName()} +

+

{user?.email}

+
+
+
+ + {/* Logout Button */} +
+ +
+
+ )} +
+
+
+ ); +}; diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 0000000..37be5c4 --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { Sidebar } from '@/components/layout/Sidebar'; +import { Header } from '@/components/layout/Header'; +import { PageHeader, type TabItem } from '@/components/shared'; +import type { ReactNode, ReactElement } from 'react'; + +interface LayoutProps { + children: ReactNode; + currentPage: string; + pageHeader?: { + title: string; + description?: string; + tabs?: TabItem[]; + }; +} + +export const Layout = ({ children, currentPage, pageHeader }: LayoutProps): ReactElement => { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const toggleSidebar = (): void => { + setIsSidebarOpen(!isSidebarOpen); + }; + + const closeSidebar = (): void => { + setIsSidebarOpen(false); + }; + + return ( +
+ {/* Background */} +
+ + {/* Content Wrapper */} +
+ {/* Mobile Overlay */} + {isSidebarOpen && ( + +
+ ); +}; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..d12c798 --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,155 @@ +import { Link, useLocation } from 'react-router-dom'; +import { + LayoutDashboard, + Building2, + Users, + Package, + FileText, + Settings, + HelpCircle, + X, + Shield +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface MenuItem { + icon: React.ComponentType<{ className?: string }>; + label: string; + path: string; +} + +interface SidebarProps { + isOpen: boolean; + onClose: () => void; +} + +const platformMenu: MenuItem[] = [ + { icon: LayoutDashboard, label: 'Dashboard', path: '/dashboard' }, + { icon: Building2, label: 'Tenants', path: '/tenants' }, + { icon: Users, label: 'User Management', path: '/users' }, + { icon: Shield, label: 'Roles', path: '/roles' }, + { icon: Package, label: 'Modules', path: '/modules' }, +]; + +const systemMenu: MenuItem[] = [ + { icon: FileText, label: 'Audit Logs', path: '/audit-logs' }, + { icon: Settings, label: 'Settings', path: '/settings' }, +]; + +export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { + const location = useLocation(); + + const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => ( +
+
+
+
+ {title} +
+
+
+ {items.map((item) => { + const Icon = item.icon; + const isActive = location.pathname === item.path; + return ( + { + // Close sidebar on mobile when navigating + if (window.innerWidth < 768) { + onClose(); + } + }} + className={cn( + 'flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors min-h-[44px]', + isActive + ? 'bg-[#112868] text-[#23dce1] shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]' + : 'text-[#0f1724] hover:bg-gray-50' + )} + > + + {item.label} + + ); + })} +
+
+
+ ); + + return ( + <> + {/* Mobile Sidebar */} + + + {/* Desktop Sidebar */} + + + ); +}; diff --git a/src/components/shared/ActionDropdown.tsx b/src/components/shared/ActionDropdown.tsx new file mode 100644 index 0000000..ff0c62b --- /dev/null +++ b/src/components/shared/ActionDropdown.tsx @@ -0,0 +1,100 @@ +import { useState, useRef, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { MoreVertical, Eye, Edit, Trash2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ActionDropdownProps { + onView?: () => void; + onEdit?: () => void; + onDelete?: () => void; + className?: string; +} + +export const ActionDropdown = ({ + onView, + onEdit, + onDelete, + className, +}: ActionDropdownProps): ReactElement => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const handleAction = (action: () => void | undefined) => { + if (action) { + action(); + } + setIsOpen(false); + }; + + return ( +
+ + + {isOpen && ( +
+
+ {onView && ( + + )} + {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+ )} +
+ ); +}; diff --git a/src/components/shared/DataTable.tsx b/src/components/shared/DataTable.tsx new file mode 100644 index 0000000..0bc0f3a --- /dev/null +++ b/src/components/shared/DataTable.tsx @@ -0,0 +1,163 @@ +import type { ReactElement, ReactNode } from 'react'; + +export interface Column { + key: string; + label: string; + render?: (item: T) => ReactNode; + align?: 'left' | 'right' | 'center'; + mobileLabel?: string; +} + +interface DataTableProps { + data: T[]; + columns: Column[]; + keyExtractor: (item: T) => string; + mobileCardRenderer?: (item: T) => ReactNode; + emptyMessage?: string; + isLoading?: boolean; + error?: string | null; +} + +export const DataTable = ({ + data, + columns, + keyExtractor, + mobileCardRenderer, + emptyMessage = 'No data found', + isLoading = false, + error = null, +}: DataTableProps): ReactElement => { + // Loading State + if (isLoading) { + return ( +
+

Loading...

+
+ ); + } + + // Error State + if (error) { + return ( +
+

{error}

+
+ ); + } + + // Empty State - show table structure with empty message + if (data.length === 0) { + return ( + <> + {/* Desktop Table Empty State */} +
+ + + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + + + + + + +
+ {column.label} +
+ {emptyMessage} +
+
+ {/* Mobile Empty State */} +
+

{emptyMessage}

+
+ + ); + } + + return ( + <> + {/* Desktop Table */} +
+ + + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + + + {data.map((item) => ( + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + ))} + +
+ {column.label} +
+ {column.render ? column.render(item) : String((item as any)[column.key])} +
+
+ + {/* Mobile Card View */} +
+ {mobileCardRenderer + ? data.map((item) =>
{mobileCardRenderer(item)}
) + : data.map((item) => ( +
+ {columns.map((column) => ( +
+ + {column.mobileLabel || column.label}: + +
+ {column.render ? column.render(item) : String((item as any)[column.key])} +
+
+ ))} +
+ ))} +
+ + ); +}; diff --git a/src/components/shared/DeleteConfirmationModal.tsx b/src/components/shared/DeleteConfirmationModal.tsx new file mode 100644 index 0000000..d0bffb0 --- /dev/null +++ b/src/components/shared/DeleteConfirmationModal.tsx @@ -0,0 +1,130 @@ +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import type { ReactElement } from 'react'; +import { X, AlertTriangle } from 'lucide-react'; +import { PrimaryButton, SecondaryButton } from '@/components/shared'; + +interface DeleteConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => Promise; + title: string; + message: string; + itemName?: string; + isLoading?: boolean; +} + +export const DeleteConfirmationModal = ({ + isOpen, + onClose, + onConfirm, + title, + message, + itemName, + isLoading = false, +}: DeleteConfirmationModalProps): ReactElement | null => { + const modalRef = useRef(null); + + // Handle click outside to close modal + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + + // Handle escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isOpen) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen, onClose]); + + const handleConfirm = async (): Promise => { + await onConfirm(); + }; + + if (!isOpen) return null; + + const modalContent = ( +
+
+ {/* Modal Header */} +
+
+
+ +
+
+

{title}

+
+
+ +
+ + {/* Modal Body */} +
+

+ {message} + {itemName && ( + "{itemName}" + )} + ? This action cannot be undone. +

+
+ + {/* Modal Footer */} +
+ + Cancel + + + {isLoading ? 'Deleting...' : 'Delete'} + +
+
+
+ ); + + return createPortal(modalContent, document.body); +}; diff --git a/src/components/shared/EditRoleModal.tsx b/src/components/shared/EditRoleModal.tsx new file mode 100644 index 0000000..d71517b --- /dev/null +++ b/src/components/shared/EditRoleModal.tsx @@ -0,0 +1,184 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Loader2 } from 'lucide-react'; +import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared'; +import type { Role, UpdateRoleRequest } from '@/types/role'; + +// Validation schema +const editRoleSchema = z.object({ + name: z.string().min(1, 'Role name is required'), + code: z.string().min(1, 'Role code is required'), + description: z.string().min(1, 'Description is required'), + scope: z.enum(['platform', 'tenant', 'module'], { + message: 'Scope is required', + }), +}); + +type EditRoleFormData = z.infer; + +interface EditRoleModalProps { + isOpen: boolean; + onClose: () => void; + roleId: string | null; + onLoadRole: (id: string) => Promise; + onSubmit: (id: string, data: UpdateRoleRequest) => Promise; + isLoading?: boolean; +} + +const scopeOptions = [ + { value: 'platform', label: 'Platform' }, + { value: 'tenant', label: 'Tenant' }, + { value: 'module', label: 'Module' }, +]; + +export const EditRoleModal = ({ + isOpen, + onClose, + roleId, + onLoadRole, + onSubmit, + isLoading = false, +}: EditRoleModalProps): ReactElement | null => { + const [isLoadingRole, setIsLoadingRole] = useState(false); + const [loadError, setLoadError] = useState(null); + + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(editRoleSchema), + }); + + const scopeValue = watch('scope'); + + // Load role data when modal opens + useEffect(() => { + if (isOpen && roleId) { + const loadRole = async (): Promise => { + try { + setIsLoadingRole(true); + setLoadError(null); + const role = await onLoadRole(roleId); + reset({ + name: role.name, + code: role.code, + description: role.description || '', + scope: role.scope, + }); + } catch (err: any) { + setLoadError(err?.response?.data?.error?.message || 'Failed to load role details'); + } finally { + setIsLoadingRole(false); + } + }; + loadRole(); + } else { + reset({ + name: '', + code: '', + description: '', + scope: 'platform', + }); + setLoadError(null); + } + }, [isOpen, roleId, onLoadRole, reset]); + + const handleFormSubmit = async (data: EditRoleFormData): Promise => { + if (roleId) { + await onSubmit(roleId, data); + } + }; + + return ( + + + Cancel + + + {isLoading ? 'Updating...' : 'Update Role'} + + + } + > + {isLoadingRole && ( +
+ +
+ )} + + {loadError && ( +
+

{loadError}

+
+ )} + + {!isLoadingRole && ( +
+ {/* Role Name and Role Code Row */} +
+ + + +
+ + {/* Description */} + + + {/* Scope */} + setValue('scope', value as 'platform' | 'tenant' | 'module')} + error={errors.scope?.message} + /> + + )} +
+ ); +}; diff --git a/src/components/shared/EditTenantModal.tsx b/src/components/shared/EditTenantModal.tsx new file mode 100644 index 0000000..399efc4 --- /dev/null +++ b/src/components/shared/EditTenantModal.tsx @@ -0,0 +1,202 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Loader2 } from 'lucide-react'; +import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared'; +import type { Tenant } from '@/types/tenant'; + +// Validation schema +const editTenantSchema = z.object({ + name: z.string().min(1, 'Tenant name is required'), + slug: z.string().min(1, 'Slug is required'), + status: z.enum(['active', 'suspended', 'blocked'], { + message: 'Status is required', + }), + timezone: z.string().min(1, 'Timezone is required'), +}); + +type EditTenantFormData = z.infer; + +interface EditTenantModalProps { + isOpen: boolean; + onClose: () => void; + tenantId: string | null; + onLoadTenant: (id: string) => Promise; + onSubmit: (id: string, data: EditTenantFormData) => Promise; + isLoading?: boolean; +} + +const statusOptions = [ + { value: 'active', label: 'Active' }, + { value: 'suspended', label: 'Suspended' }, + { value: 'blocked', label: 'Blocked' }, +]; + +const timezoneOptions = [ + { value: 'America/New_York', label: 'America/New_York (EST)' }, + { value: 'America/Chicago', label: 'America/Chicago (CST)' }, + { value: 'America/Denver', label: 'America/Denver (MST)' }, + { value: 'America/Los_Angeles', label: 'America/Los_Angeles (PST)' }, + { value: 'UTC', label: 'UTC' }, + { value: 'Europe/London', label: 'Europe/London (GMT)' }, + { value: 'Asia/Dubai', label: 'Asia/Dubai (GST)' }, + { value: 'Asia/Kolkata', label: 'Asia/Kolkata (IST)' }, + { value: 'Asia/Tokyo', label: 'Asia/Tokyo (JST)' }, + { value: 'Australia/Sydney', label: 'Australia/Sydney (AEDT)' }, +]; + +export const EditTenantModal = ({ + isOpen, + onClose, + tenantId, + onLoadTenant, + onSubmit, + isLoading = false, +}: EditTenantModalProps): ReactElement | null => { + const [isLoadingTenant, setIsLoadingTenant] = useState(false); + const [loadError, setLoadError] = useState(null); + + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(editTenantSchema), + }); + + const statusValue = watch('status'); + const timezoneValue = watch('timezone'); + + // Load tenant data when modal opens + useEffect(() => { + if (isOpen && tenantId) { + const loadTenant = async (): Promise => { + try { + setIsLoadingTenant(true); + setLoadError(null); + const tenant = await onLoadTenant(tenantId); + reset({ + name: tenant.name, + slug: tenant.slug, + status: tenant.status, + timezone: tenant.settings?.timezone || 'America/New_York', + }); + } catch (err: any) { + setLoadError(err?.response?.data?.error?.message || 'Failed to load tenant details'); + } finally { + setIsLoadingTenant(false); + } + }; + loadTenant(); + } else { + reset({ + name: '', + slug: '', + status: 'active', + timezone: 'America/New_York', + }); + setLoadError(null); + } + }, [isOpen, tenantId, onLoadTenant, reset]); + + const handleFormSubmit = async (data: EditTenantFormData): Promise => { + if (tenantId) { + await onSubmit(tenantId, data); + } + }; + + return ( + + + Cancel + + + {isLoading ? 'Updating...' : 'Update Tenant'} + + + } + > +
+ {isLoadingTenant && ( +
+ +
+ )} + + {loadError && ( +
+

{loadError}

+
+ )} + + {!isLoadingTenant && ( +
+ {/* Tenant Name */} + + + {/* Slug */} + + + {/* Status and Timezone Row */} +
+ setValue('status', value as 'active' | 'suspended' | 'blocked')} + error={errors.status?.message} + /> + + setValue('timezone', value)} + error={errors.timezone?.message} + /> +
+
+ )} +
+
+ ); +}; diff --git a/src/components/shared/EditUserModal.tsx b/src/components/shared/EditUserModal.tsx new file mode 100644 index 0000000..3e9b4e0 --- /dev/null +++ b/src/components/shared/EditUserModal.tsx @@ -0,0 +1,306 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Loader2 } from 'lucide-react'; +import { + Modal, + FormField, + FormSelect, + PaginatedSelect, + PrimaryButton, + SecondaryButton, +} from '@/components/shared'; +import { tenantService } from '@/services/tenant-service'; +import { roleService } from '@/services/role-service'; +import type { User } from '@/types/user'; + +// Validation schema +const editUserSchema = z.object({ + email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + first_name: z.string().min(1, 'First name is required'), + last_name: z.string().min(1, 'Last name is required'), + status: z.enum(['active', 'suspended', 'blocked'], { + message: 'Status is required', + }), + tenant_id: z.string().min(1, 'Tenant is required'), + role_id: z.string().min(1, 'Role is required'), +}); + +type EditUserFormData = z.infer; + +interface EditUserModalProps { + isOpen: boolean; + onClose: () => void; + userId: string | null; + onLoadUser: (id: string) => Promise; + onSubmit: (id: string, data: EditUserFormData) => Promise; + isLoading?: boolean; +} + +const statusOptions = [ + { value: 'active', label: 'Active' }, + { value: 'suspended', label: 'Suspended' }, + { value: 'blocked', label: 'Blocked' }, +]; + +export const EditUserModal = ({ + isOpen, + onClose, + userId, + onLoadUser, + onSubmit, + isLoading = false, +}: EditUserModalProps): ReactElement | null => { + const [isLoadingUser, setIsLoadingUser] = useState(false); + const [loadError, setLoadError] = useState(null); + const [selectedTenantId, setSelectedTenantId] = useState(''); + const [selectedRoleId, setSelectedRoleId] = useState(''); + + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(editUserSchema), + }); + + const statusValue = watch('status'); + const tenantIdValue = watch('tenant_id'); + const roleIdValue = watch('role_id'); + + // Load tenants for dropdown - ensure selected tenant is included + const loadTenants = async (page: number, limit: number) => { + const response = await tenantService.getAll(page, limit); + let options = response.data.map((tenant) => ({ + value: tenant.id, + label: tenant.name, + })); + + // If we have a selected tenant ID and it's not in the current options, fetch it specifically + if (selectedTenantId && page === 1 && !options.find((opt) => opt.value === selectedTenantId)) { + try { + const tenantResponse = await tenantService.getById(selectedTenantId); + if (tenantResponse.success) { + // Prepend the selected tenant to the options + options = [ + { + value: tenantResponse.data.id, + label: tenantResponse.data.name, + }, + ...options, + ]; + } + } catch (err) { + // If fetching fails, just continue with existing options + console.warn('Failed to fetch selected tenant:', err); + } + } + + return { + options, + pagination: response.pagination, + }; + }; + + // Load roles for dropdown - ensure selected role is included + const loadRoles = async (page: number, limit: number) => { + const response = await roleService.getAll(page, limit); + let options = response.data.map((role) => ({ + value: role.id, + label: role.name, + })); + + // If we have a selected role ID and it's not in the current options, fetch it specifically + if (selectedRoleId && page === 1 && !options.find((opt) => opt.value === selectedRoleId)) { + try { + const roleResponse = await roleService.getById(selectedRoleId); + if (roleResponse.success) { + // Prepend the selected role to the options + options = [ + { + value: roleResponse.data.id, + label: roleResponse.data.name, + }, + ...options, + ]; + } + } catch (err) { + // If fetching fails, just continue with existing options + console.warn('Failed to fetch selected role:', err); + } + } + + return { + options, + pagination: response.pagination, + }; + }; + + // Load user data when modal opens + useEffect(() => { + if (isOpen && userId) { + const loadUser = async (): Promise => { + try { + setIsLoadingUser(true); + setLoadError(null); + const user = await onLoadUser(userId); + + // Store selected IDs for dropdown pre-loading + const tenantId = user.tenant_id || ''; + const roleId = user.role_id || ''; + setSelectedTenantId(tenantId); + setSelectedRoleId(roleId); + + reset({ + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + status: user.status, + tenant_id: tenantId, + role_id: roleId, + }); + } catch (err: any) { + setLoadError(err?.response?.data?.error?.message || 'Failed to load user details'); + } finally { + setIsLoadingUser(false); + } + }; + loadUser(); + } else { + setSelectedTenantId(''); + setSelectedRoleId(''); + reset({ + email: '', + first_name: '', + last_name: '', + status: 'active', + tenant_id: '', + role_id: '', + }); + setLoadError(null); + } + }, [isOpen, userId, onLoadUser, reset]); + + const handleFormSubmit = async (data: EditUserFormData): Promise => { + if (userId) { + await onSubmit(userId, data); + } + }; + + return ( + + + Cancel + + + {isLoading ? 'Updating...' : 'Update User'} + + + } + > +
+ {isLoadingUser && ( +
+ +
+ )} + + {loadError && ( +
+

{loadError}

+
+ )} + + {!isLoadingUser && ( +
+ {/* Email */} + + + {/* First Name and Last Name Row */} +
+ + + +
+ + {/* Tenant and Role Row */} +
+ setValue('tenant_id', value)} + onLoadOptions={loadTenants} + error={errors.tenant_id?.message} + /> + + setValue('role_id', value)} + onLoadOptions={loadRoles} + error={errors.role_id?.message} + /> +
+ + {/* Status */} + setValue('status', value as 'active' | 'suspended' | 'blocked')} + error={errors.status?.message} + /> +
+ )} +
+
+ ); +}; diff --git a/src/components/shared/FilterDropdown.tsx b/src/components/shared/FilterDropdown.tsx new file mode 100644 index 0000000..b5c5acf --- /dev/null +++ b/src/components/shared/FilterDropdown.tsx @@ -0,0 +1,174 @@ +import { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import type { ReactElement } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface FilterOption { + value: string | string[]; + label: string; +} + +interface FilterDropdownProps { + label: string; + options: FilterOption[]; + value: string | string[] | null; + onChange: (value: string | string[] | null) => void; + placeholder?: string; + showIcon?: boolean; + icon?: ReactElement; +} + +export const FilterDropdown = ({ + label, + options, + value, + onChange, + placeholder = 'All', + showIcon = false, + icon, +}: FilterDropdownProps): ReactElement => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + const [dropdownStyle, setDropdownStyle] = useState<{ + top?: string; + bottom?: string; + left: string; + width: string; + }>({ left: '0', width: '0' }); + + // Handle click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const isDropdownClick = target.closest('[data-filter-dropdown="true"]'); + if ( + dropdownRef.current && + !dropdownRef.current.contains(target) && + !isDropdownClick && + buttonRef.current && + !buttonRef.current.contains(target) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = Math.min(options.length * 40 + 12, 240); + + const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + const left = rect.left; + const width = rect.width; + + if (shouldOpenUp) { + const bottom = window.innerHeight - rect.top; + setDropdownStyle({ + bottom: `${bottom}px`, + left: `${left}px`, + width: `${width}px`, + }); + } else { + const top = rect.bottom; + setDropdownStyle({ + top: `${top}px`, + left: `${left}px`, + width: `${width}px`, + }); + } + } + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen, options.length]); + + // Helper to compare values (handles both string and array) + const compareValues = (val1: string | string[], val2: string | string[]): boolean => { + if (Array.isArray(val1) && Array.isArray(val2)) { + return val1.length === val2.length && val1.every((v, i) => v === val2[i]); + } + return val1 === val2; + }; + + const selectedOption = value + ? options.find((opt) => compareValues(opt.value, value)) + : null; + const displayText = selectedOption ? selectedOption.label : placeholder; + + return ( +
+ + + {isOpen && + buttonRef.current && + createPortal( +
+
    +
  • + +
  • + {options.map((option, index) => { + const optionKey = Array.isArray(option.value) + ? option.value.join(':') + : option.value; + const isSelected = value ? compareValues(option.value, value) : false; + return ( +
  • + +
  • + ); + })} +
+
, + document.body + )} +
+ ); +}; diff --git a/src/components/shared/FormField.tsx b/src/components/shared/FormField.tsx new file mode 100644 index 0000000..3b11861 --- /dev/null +++ b/src/components/shared/FormField.tsx @@ -0,0 +1,59 @@ +import type { ReactElement, InputHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +interface FormFieldProps extends InputHTMLAttributes { + label: string; + required?: boolean; + error?: string; + helperText?: string; +} + +export const FormField = ({ + label, + required = false, + error, + helperText, + className, + id, + ...props +}: FormFieldProps): ReactElement => { + const fieldId = id || `field-${label.toLowerCase().replace(/\s+/g, '-')}`; + const hasError = Boolean(error); + + return ( +
+ + + {error && ( + + )} + {helperText && !error && ( +

+ {helperText} +

+ )} +
+ ); +}; diff --git a/src/components/shared/FormSelect.tsx b/src/components/shared/FormSelect.tsx new file mode 100644 index 0000000..176bc2a --- /dev/null +++ b/src/components/shared/FormSelect.tsx @@ -0,0 +1,200 @@ +import { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import type { ReactElement, SelectHTMLAttributes } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface SelectOption { + value: string; + label: string; +} + +interface FormSelectProps extends Omit, 'onChange'> { + label: string; + required?: boolean; + error?: string; + helperText?: string; + options: SelectOption[]; + placeholder?: string; + onValueChange?: (value: string) => void; +} + +export const FormSelect = ({ + label, + required = false, + error, + helperText, + options, + placeholder = 'Select Item', + className, + id, + value, + onValueChange, + ...props +}: FormSelectProps): ReactElement => { + const [isOpen, setIsOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState(value as string || ''); + const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; left: string; width: string }>({ left: '0', width: '0' }); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + const dropdownMenuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + dropdownMenuRef.current && + !dropdownMenuRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen && buttonRef.current) { + document.addEventListener('mousedown', handleClickOutside); + // Calculate position when dropdown opens + const rect = buttonRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = 240; // max-h-60 = 240px + + // Determine if should open upward or downward + const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + + // Calculate dropdown position + const left = rect.left; + const width = rect.width; + + if (shouldOpenUp) { + // Position above the button + const bottom = window.innerHeight - rect.top; + setDropdownStyle({ + bottom: `${bottom}px`, + left: `${left}px`, + width: `${width}px`, + }); + } else { + // Position below the button + const top = rect.bottom; + setDropdownStyle({ + top: `${top}px`, + left: `${left}px`, + width: `${width}px`, + }); + } + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + useEffect(() => { + if (value !== undefined) { + setSelectedValue(value as string); + } + }, [value]); + + const fieldId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`; + const hasError = Boolean(error); + const selectedOption = options.find((opt) => opt.value === selectedValue); + + const handleSelect = (optionValue: string) => { + setSelectedValue(optionValue); + if (onValueChange) { + onValueChange(optionValue); + } + setIsOpen(false); + }; + + return ( +
+ +
+ + + {isOpen && buttonRef.current && createPortal( +
+
    + {options.map((option) => ( +
  • + +
  • + ))} +
+
, + document.body + )} +
+ {error && ( + + )} + {helperText && !error && ( +

+ {helperText} +

+ )} + +
+ ); +}; diff --git a/src/components/shared/Modal.tsx b/src/components/shared/Modal.tsx new file mode 100644 index 0000000..4de8e1c --- /dev/null +++ b/src/components/shared/Modal.tsx @@ -0,0 +1,130 @@ +import { useEffect, useRef, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import type { ReactElement } from 'react'; +import { X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + description?: string; + children: ReactNode; + footer?: ReactNode; + maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; + className?: string; + showCloseButton?: boolean; + preventCloseOnClickOutside?: boolean; +} + +const maxWidthClasses = { + sm: 'max-w-[400px]', + md: 'max-w-[500px]', + lg: 'max-w-[600px]', + xl: 'max-w-[800px]', + '2xl': 'max-w-[1000px]', +}; + +export const Modal = ({ + isOpen, + onClose, + title, + description, + children, + footer, + maxWidth = 'md', + className, + showCloseButton = true, + preventCloseOnClickOutside = false, +}: ModalProps): ReactElement | null => { + const modalRef = useRef(null); + + // Handle click outside to close modal + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (preventCloseOnClickOutside) return; + + const target = event.target as HTMLElement; + + // Check if click is on dropdown menu (rendered via portal) + const isDropdownClick = target.closest('[data-dropdown-menu="true"]'); + + // Only close if click is outside modal AND not on dropdown + if (modalRef.current && !modalRef.current.contains(target) && !isDropdownClick) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + // Prevent body scroll when modal is open + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose, preventCloseOnClickOutside]); + + // Handle escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isOpen && !preventCloseOnClickOutside) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen, onClose, preventCloseOnClickOutside]); + + if (!isOpen) return null; + + const modalContent = ( +
+
+ {/* Modal Header */} +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {showCloseButton && ( + + )} +
+ + {/* Modal Body - Scrollable */} +
{children}
+ + {/* Modal Footer */} + {footer && ( +
+ {footer} +
+ )} +
+
+ ); + + return createPortal(modalContent, document.body); +}; diff --git a/src/components/shared/NewRoleModal.tsx b/src/components/shared/NewRoleModal.tsx new file mode 100644 index 0000000..2811ee4 --- /dev/null +++ b/src/components/shared/NewRoleModal.tsx @@ -0,0 +1,143 @@ +import { useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared'; +import type { CreateRoleRequest } from '@/types/role'; + +// Validation schema +const newRoleSchema = z.object({ + name: z.string().min(1, 'Role name is required'), + code: z.string().min(1, 'Role code is required'), + description: z.string().min(1, 'Description is required'), + scope: z.enum(['platform', 'tenant', 'module'], { + message: 'Scope is required', + }), +}); + +type NewRoleFormData = z.infer; + +interface NewRoleModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: CreateRoleRequest) => Promise; + isLoading?: boolean; +} + +const scopeOptions = [ + { value: 'platform', label: 'Platform' }, + { value: 'tenant', label: 'Tenant' }, + { value: 'module', label: 'Module' }, +]; + +export const NewRoleModal = ({ + isOpen, + onClose, + onSubmit, + isLoading = false, +}: NewRoleModalProps): ReactElement | null => { + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(newRoleSchema), + defaultValues: { + scope: 'platform', + }, + }); + + const scopeValue = watch('scope'); + + // Reset form when modal closes + useEffect(() => { + if (!isOpen) { + reset({ + name: '', + code: '', + description: '', + scope: 'platform', + }); + } + }, [isOpen, reset]); + + const handleFormSubmit = async (data: NewRoleFormData): Promise => { + await onSubmit(data); + }; + + return ( + + + Cancel + + + {isLoading ? 'Creating...' : 'Create Role'} + + + } + > +
+ {/* Role Name and Role Code Row */} +
+ + + +
+ + {/* Description */} + + + {/* Scope */} + setValue('scope', value as 'platform' | 'tenant' | 'module')} + error={errors.scope?.message} + /> + +
+ ); +}; diff --git a/src/components/shared/NewTenantModal.tsx b/src/components/shared/NewTenantModal.tsx new file mode 100644 index 0000000..a468a22 --- /dev/null +++ b/src/components/shared/NewTenantModal.tsx @@ -0,0 +1,161 @@ +import { useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared'; + +// Validation schema +const newTenantSchema = z.object({ + name: z.string().min(1, 'Tenant name is required'), + slug: z.string().min(1, 'Slug is required'), + status: z.enum(['active', 'suspended', 'blocked'], { + message: 'Status is required', + }), + timezone: z.string().min(1, 'Timezone is required'), +}); + +type NewTenantFormData = z.infer; + +interface NewTenantModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: NewTenantFormData) => Promise; + isLoading?: boolean; +} + +const statusOptions = [ + { value: 'active', label: 'Active' }, + { value: 'suspended', label: 'Suspended' }, + { value: 'blocked', label: 'Blocked' }, +]; + +const timezoneOptions = [ + { value: 'America/New_York', label: 'America/New_York (EST)' }, + { value: 'America/Chicago', label: 'America/Chicago (CST)' }, + { value: 'America/Denver', label: 'America/Denver (MST)' }, + { value: 'America/Los_Angeles', label: 'America/Los_Angeles (PST)' }, + { value: 'UTC', label: 'UTC' }, + { value: 'Europe/London', label: 'Europe/London (GMT)' }, + { value: 'Asia/Dubai', label: 'Asia/Dubai (GST)' }, + { value: 'Asia/Kolkata', label: 'Asia/Kolkata (IST)' }, + { value: 'Asia/Tokyo', label: 'Asia/Tokyo (JST)' }, + { value: 'Australia/Sydney', label: 'Australia/Sydney (AEDT)' }, +]; + +export const NewTenantModal = ({ + isOpen, + onClose, + onSubmit, + isLoading = false, +}: NewTenantModalProps): ReactElement | null => { + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(newTenantSchema), + defaultValues: { + status: 'active', + timezone: 'America/New_York', + }, + }); + + const statusValue = watch('status'); + const timezoneValue = watch('timezone'); + + // Reset form when modal closes + useEffect(() => { + if (!isOpen) { + reset({ + name: '', + slug: '', + status: 'active', + timezone: 'America/New_York', + }); + } + }, [isOpen, reset]); + + const handleFormSubmit = async (data: NewTenantFormData): Promise => { + await onSubmit(data); + }; + + return ( + + + Cancel + + + {isLoading ? 'Creating...' : 'Create Tenant'} + + + } + > +
+
+ {/* Tenant Name */} + + + {/* Slug */} + + + {/* Status and Timezone Row */} +
+ setValue('status', value as 'active' | 'suspended' | 'blocked')} + error={errors.status?.message} + /> + + setValue('timezone', value)} + error={errors.timezone?.message} + /> +
+
+
+
+ ); +}; diff --git a/src/components/shared/NewUserModal.tsx b/src/components/shared/NewUserModal.tsx new file mode 100644 index 0000000..4f14eaf --- /dev/null +++ b/src/components/shared/NewUserModal.tsx @@ -0,0 +1,245 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { + Modal, + FormField, + FormSelect, + PaginatedSelect, + PrimaryButton, + SecondaryButton, +} from '@/components/shared'; +import { tenantService } from '@/services/tenant-service'; +import { roleService } from '@/services/role-service'; + +// Validation schema +const newUserSchema = z + .object({ + email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'), + confirmPassword: z.string().min(1, 'Confirm password is required'), + first_name: z.string().min(1, 'First name is required'), + last_name: z.string().min(1, 'Last name is required'), + status: z.enum(['active', 'suspended', 'blocked'], { + message: 'Status is required', + }), + auth_provider: z.enum(['local'], { + message: 'Auth provider is required', + }), + tenant_id: z.string().min(1, 'Tenant is required'), + role_id: z.string().min(1, 'Role is required'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }); + +type NewUserFormData = z.infer; + +interface NewUserModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: Omit) => Promise; + isLoading?: boolean; +} + +const statusOptions = [ + { value: 'active', label: 'Active' }, + { value: 'suspended', label: 'Suspended' }, + { value: 'blocked', label: 'Blocked' }, +]; + +export const NewUserModal = ({ + isOpen, + onClose, + onSubmit, + isLoading = false, +}: NewUserModalProps): ReactElement | null => { + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(newUserSchema), + defaultValues: { + status: 'active', + auth_provider: 'local', + tenant_id: '', + role_id: '', + }, + }); + + const statusValue = watch('status'); + const tenantIdValue = watch('tenant_id'); + const roleIdValue = watch('role_id'); + + // Reset form when modal closes + useEffect(() => { + if (!isOpen) { + reset({ + email: '', + password: '', + confirmPassword: '', + first_name: '', + last_name: '', + status: 'active', + auth_provider: 'local', + tenant_id: '', + role_id: '', + }); + } + }, [isOpen, reset]); + + // Load tenants for dropdown + const loadTenants = async (page: number, limit: number) => { + const response = await tenantService.getAll(page, limit); + return { + options: response.data.map((tenant) => ({ + value: tenant.id, + label: tenant.name, + })), + pagination: response.pagination, + }; + }; + + // Load roles for dropdown + const loadRoles = async (page: number, limit: number) => { + const response = await roleService.getAll(page, limit); + return { + options: response.data.map((role) => ({ + value: role.id, + label: role.name, + })), + pagination: response.pagination, + }; + }; + + const handleFormSubmit = async (data: NewUserFormData): Promise => { + const { confirmPassword, ...submitData } = data; + await onSubmit(submitData); + }; + + return ( + + + Cancel + + + {isLoading ? 'Creating...' : 'Create User'} + + + } + > +
+
+ {/* Email */} + + + {/* First Name and Last Name Row */} +
+ + + +
+ + {/* Password and Confirm Password Row */} +
+ + + +
+ + {/* Tenant and Role Row */} +
+ setValue('tenant_id', value)} + onLoadOptions={loadTenants} + error={errors.tenant_id?.message} + /> + + setValue('role_id', value)} + onLoadOptions={loadRoles} + error={errors.role_id?.message} + /> +
+ + {/* Status */} + setValue('status', value as 'active' | 'suspended' | 'blocked')} + error={errors.status?.message} + /> +
+
+
+ ); +}; diff --git a/src/components/shared/PageHeader.tsx b/src/components/shared/PageHeader.tsx new file mode 100644 index 0000000..f65237f --- /dev/null +++ b/src/components/shared/PageHeader.tsx @@ -0,0 +1,80 @@ +import { useLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import type { ReactElement } from 'react'; +import { cn } from '@/lib/utils'; + +export interface TabItem { + label: string; + path: string; +} + +interface PageHeaderProps { + title: string; + description?: string; + tabs?: TabItem[]; +} + +const defaultTabs: TabItem[] = [ + { label: 'Overview', path: '/dashboard' }, + { label: 'Tenants', path: '/tenants' }, + { label: 'Users', path: '/users' }, + { label: 'Roles', path: '/roles' }, + { label: 'Modules', path: '/modules' }, + { label: 'Audit Logs', path: '/audit-logs' }, +]; + +export const PageHeader = ({ + title, + description, + tabs = defaultTabs, +}: PageHeaderProps): ReactElement => { + const location = useLocation(); + + const isActiveTab = (path: string): boolean => { + // Exact match for dashboard + if (path === '/dashboard') { + return location.pathname === '/dashboard'; + } + // For other paths, check if current path starts with the tab path + return location.pathname.startsWith(path); + }; + + return ( +
+ {/* Title and Description */} +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ + {/* Tabs Navigation */} + {tabs.length > 0 && ( +
+ {tabs.map((tab) => { + const isActive = isActiveTab(tab.path); + return ( + + {tab.label} + + ); + })} +
+ )} +
+ ); +}; diff --git a/src/components/shared/PaginatedSelect.tsx b/src/components/shared/PaginatedSelect.tsx new file mode 100644 index 0000000..113be0a --- /dev/null +++ b/src/components/shared/PaginatedSelect.tsx @@ -0,0 +1,284 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import type { ReactElement } from 'react'; +import { ChevronDown, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface PaginatedSelectOption { + value: string; + label: string; +} + +interface PaginatedSelectProps { + label: string; + required?: boolean; + error?: string; + helperText?: string; + placeholder?: string; + value: string; + onValueChange: (value: string) => void; + onLoadOptions: (page: number, limit: number) => Promise<{ + options: PaginatedSelectOption[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; + }; + }>; + className?: string; + id?: string; +} + +export const PaginatedSelect = ({ + label, + required = false, + error, + helperText, + placeholder = 'Select Item', + value, + onValueChange, + onLoadOptions, + className, + id, +}: PaginatedSelectProps): ReactElement => { + const [isOpen, setIsOpen] = useState(false); + const [options, setOptions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [pagination, setPagination] = useState<{ + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; + }>({ + page: 1, + limit: 20, + total: 0, + totalPages: 1, + hasMore: false, + }); + + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + const dropdownMenuRef = useRef(null); + const scrollContainerRef = useRef(null); + const [dropdownStyle, setDropdownStyle] = useState<{ + top?: string; + bottom?: string; + left: string; + width: string; + }>({ left: '0', width: '0' }); + + // Load initial options + const loadOptions = useCallback( + async (page: number = 1, append: boolean = false) => { + try { + if (page === 1) { + setIsLoading(true); + } else { + setIsLoadingMore(true); + } + + const result = await onLoadOptions(page, pagination.limit); + + if (append) { + setOptions((prev) => [...prev, ...result.options]); + } else { + setOptions(result.options); + } + + setPagination(result.pagination); + } catch (err) { + console.error('Error loading options:', err); + } finally { + setIsLoading(false); + setIsLoadingMore(false); + } + }, + [onLoadOptions, pagination.limit] + ); + + // Load options when dropdown opens + useEffect(() => { + if (isOpen) { + if (options.length === 0) { + loadOptions(1, false); + } + } else { + // Reset pagination when dropdown closes (but keep options for faster reopening) + // Only reset if we want fresh data each time + // setOptions([]); + // setPagination({ page: 1, limit: 20, total: 0, totalPages: 1, hasMore: false }); + } + }, [isOpen, options.length, loadOptions]); + + // Handle scroll for infinite loading + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer || !isOpen) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; + const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50; + + if (isNearBottom && pagination.hasMore && !isLoadingMore && !isLoading) { + loadOptions(pagination.page + 1, true); + } + }; + + scrollContainer.addEventListener('scroll', handleScroll); + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + }; + }, [isOpen, pagination, isLoadingMore, isLoading, loadOptions]); + + // Handle click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + dropdownRef.current && + !dropdownRef.current.contains(target) && + dropdownMenuRef.current && + !dropdownMenuRef.current.contains(target) + ) { + setIsOpen(false); + } + }; + + if (isOpen && buttonRef.current) { + document.addEventListener('mousedown', handleClickOutside); + + const rect = buttonRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = Math.min(240, 240); + + const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + + if (shouldOpenUp) { + setDropdownStyle({ + bottom: `${window.innerHeight - rect.top + 5}px`, + left: `${rect.left}px`, + width: `${rect.width}px`, + }); + } else { + setDropdownStyle({ + top: `${rect.bottom + 5}px`, + left: `${rect.left}px`, + width: `${rect.width}px`, + }); + } + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const fieldId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`; + const hasError = Boolean(error); + const selectedOption = options.find((opt) => opt.value === value); + + const handleSelect = (optionValue: string) => { + onValueChange(optionValue); + setIsOpen(false); + }; + + return ( +
+ +
+ + + {isOpen && + buttonRef.current && + createPortal( +
+ {isLoading && options.length === 0 ? ( +
+ +
+ ) : ( + <> +
    + {options.map((option) => ( +
  • + +
  • + ))} + {isLoadingMore && ( +
  • + +
  • + )} +
+ + )} +
, + document.body + )} +
+ {error && ( + + )} + {helperText && !error && ( +

+ {helperText} +

+ )} +
+ ); +}; diff --git a/src/components/shared/Pagination.tsx b/src/components/shared/Pagination.tsx new file mode 100644 index 0000000..f93070e --- /dev/null +++ b/src/components/shared/Pagination.tsx @@ -0,0 +1,200 @@ +import { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; +import type { ReactElement } from 'react'; +import { cn } from '@/lib/utils'; + +interface LimitOption { + value: string; + label: string; +} + +interface PaginationProps { + currentPage: number; + totalPages: number; + totalItems: number; + limit: number; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + limitOptions?: LimitOption[]; +} + +const defaultLimitOptions = [ + { value: '5', label: '5 per page' }, + { value: '10', label: '10 per page' }, + { value: '20', label: '20 per page' }, + { value: '50', label: '50 per page' }, + { value: '100', label: '100 per page' }, +]; + +export const Pagination = ({ + currentPage, + totalPages, + totalItems, + limit, + onPageChange, + onLimitChange, + limitOptions = defaultLimitOptions, +}: PaginationProps): ReactElement => { + const [isLimitOpen, setIsLimitOpen] = useState(false); + const limitDropdownRef = useRef(null); + const limitButtonRef = useRef(null); + const [limitDropdownStyle, setLimitDropdownStyle] = useState<{ + top?: string; + bottom?: string; + left: string; + width: string; + }>({ left: '0', width: '0' }); + + const startItem = totalItems === 0 ? 0 : (currentPage - 1) * limit + 1; + const endItem = Math.min(currentPage * limit, totalItems); + + const handlePrevious = (): void => { + if (currentPage > 1) { + onPageChange(currentPage - 1); + } + }; + + const handleNext = (): void => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1); + } + }; + + // Handle limit dropdown click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const isDropdownClick = target.closest('[data-limit-dropdown="true"]'); + if ( + limitDropdownRef.current && + !limitDropdownRef.current.contains(target) && + !isDropdownClick && + limitButtonRef.current && + !limitButtonRef.current.contains(target) + ) { + setIsLimitOpen(false); + } + }; + + if (isLimitOpen) { + document.addEventListener('mousedown', handleClickOutside); + if (limitButtonRef.current) { + const rect = limitButtonRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = 200; + + const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; + const left = rect.left; + const width = rect.width; + + if (shouldOpenUp) { + const bottom = window.innerHeight - rect.top; + setLimitDropdownStyle({ + bottom: `${bottom}px`, + left: `${left}px`, + width: `${width}px`, + }); + } else { + const top = rect.bottom; + setLimitDropdownStyle({ + top: `${top}px`, + left: `${left}px`, + width: `${width}px`, + }); + } + } + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isLimitOpen]); + + const selectedLimitOption = limitOptions.find((opt) => Number(opt.value) === limit); + + return ( +
+ {/* Items Info and Limit Selector */} +
+
+ Showing {startItem} to {endItem} of {totalItems} {totalItems === 1 ? 'item' : 'items'} +
+
+ Show: +
+ + {isLimitOpen && + limitButtonRef.current && + createPortal( +
+
    + {limitOptions.map((option) => ( +
  • + +
  • + ))} +
+
, + document.body + )} +
+
+
+ + {/* Pagination Controls */} +
+ +
+ + Page {currentPage} of {totalPages} + +
+ +
+
+ ); +}; diff --git a/src/components/shared/PrimaryButton.tsx b/src/components/shared/PrimaryButton.tsx new file mode 100644 index 0000000..6a26f21 --- /dev/null +++ b/src/components/shared/PrimaryButton.tsx @@ -0,0 +1,51 @@ +import type { ReactElement, ButtonHTMLAttributes } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const primaryButtonVariants = cva( + 'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed', + { + variants: { + size: { + default: 'h-10', + small: 'h-8', + large: 'h-12', + }, + variant: { + default: 'bg-[#112868] text-[#23dce1] hover:bg-[#23dce1] hover:text-[#112868]', + disabled: 'bg-[#112868] text-[#23dce1] opacity-50', + }, + }, + defaultVariants: { + size: 'default', + variant: 'default', + }, + } +); + +interface PrimaryButtonProps + extends ButtonHTMLAttributes, + VariantProps { + children: React.ReactNode; +} + +export const PrimaryButton = ({ + children, + size, + variant, + className, + disabled, + ...props +}: PrimaryButtonProps): ReactElement => { + const buttonVariant = disabled ? 'disabled' : variant || 'default'; + + return ( + + ); +}; diff --git a/src/components/shared/SecondaryButton.tsx b/src/components/shared/SecondaryButton.tsx new file mode 100644 index 0000000..7be44e0 --- /dev/null +++ b/src/components/shared/SecondaryButton.tsx @@ -0,0 +1,44 @@ +import type { ReactElement, ButtonHTMLAttributes } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const secondaryButtonVariants = cva( + 'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed', + { + variants: { + variant: { + default: 'bg-[#23dce1] text-[#112868] hover:bg-[#112868] hover:text-[#23dce1]', + disabled: 'bg-[#23dce1] text-[#112868] opacity-50', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +interface SecondaryButtonProps + extends ButtonHTMLAttributes, + VariantProps { + children: React.ReactNode; +} + +export const SecondaryButton = ({ + children, + variant, + className, + disabled, + ...props +}: SecondaryButtonProps): ReactElement => { + const buttonVariant = disabled ? 'disabled' : variant || 'default'; + + return ( + + ); +}; diff --git a/src/components/shared/StatusBadge.tsx b/src/components/shared/StatusBadge.tsx new file mode 100644 index 0000000..42458ab --- /dev/null +++ b/src/components/shared/StatusBadge.tsx @@ -0,0 +1,45 @@ +import type { ReactElement } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const statusBadgeVariants = cva( + 'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-semibold uppercase', + { + variants: { + variant: { + success: 'bg-[rgba(16,185,129,0.1)] text-[#059669]', + failure: 'bg-[rgba(239,68,68,0.1)] text-[#ef4444]', + info: 'bg-[#e7f8f2] text-[#084cc8]', + process: 'bg-[rgba(245,158,11,0.1)] text-[#d97706]', + }, + }, + defaultVariants: { + variant: 'success', + }, + } +); + +interface StatusBadgeProps extends VariantProps { + children: React.ReactNode; + className?: string; +} + +const statusDotColors = { + success: 'bg-[#059669]', + failure: 'bg-[#ef4444]', + info: 'bg-[#084cc8]', + process: 'bg-[#d97706]', +}; + +export const StatusBadge = ({ + children, + variant = 'success', + className, +}: StatusBadgeProps): ReactElement => { + return ( +
+
+ {children} +
+ ); +}; diff --git a/src/components/shared/ThemeButton.tsx b/src/components/shared/ThemeButton.tsx new file mode 100644 index 0000000..56c965e --- /dev/null +++ b/src/components/shared/ThemeButton.tsx @@ -0,0 +1,45 @@ +import type { ReactElement, ButtonHTMLAttributes } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const themeButtonVariants = cva( + 'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed', + { + variants: { + variant: { + default: 'bg-[#084cc8] text-white hover:bg-[#23dce1] hover:text-[#084cc8]', + disabled: 'bg-[#084cc8] text-white opacity-50', + light: 'bg-[#f5f7fa] text-[#0e1b2a] border border-[rgba(0,0,0,0.08)]', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +interface ThemeButtonProps + extends ButtonHTMLAttributes, + VariantProps { + children: React.ReactNode; +} + +export const ThemeButton = ({ + children, + variant, + className, + disabled, + ...props +}: ThemeButtonProps): ReactElement => { + const buttonVariant = disabled ? 'disabled' : variant || 'default'; + + return ( + + ); +}; diff --git a/src/components/shared/ViewRoleModal.tsx b/src/components/shared/ViewRoleModal.tsx new file mode 100644 index 0000000..64473c3 --- /dev/null +++ b/src/components/shared/ViewRoleModal.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { Loader2 } from 'lucide-react'; +import { SecondaryButton, StatusBadge, Modal } from '@/components/shared'; +import type { Role } from '@/types/role'; + +interface ViewRoleModalProps { + isOpen: boolean; + onClose: () => void; + roleId: string | null; + onLoadRole: (id: string) => Promise; +} + +// Helper function to get scope badge variant +const getScopeVariant = (scope: string): 'success' | 'failure' | 'process' => { + switch (scope.toLowerCase()) { + case 'platform': + return 'success'; + case 'tenant': + return 'process'; + case 'module': + return 'failure'; + default: + return 'success'; + } +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export const ViewRoleModal = ({ + isOpen, + onClose, + roleId, + onLoadRole, +}: ViewRoleModalProps): ReactElement | null => { + const [role, setRole] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Load role data when modal opens + useEffect(() => { + if (isOpen && roleId) { + const loadRole = async (): Promise => { + try { + setIsLoading(true); + setError(null); + const data = await onLoadRole(roleId); + setRole(data); + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load role details'); + } finally { + setIsLoading(false); + } + }; + loadRole(); + } else { + setRole(null); + setError(null); + } + }, [isOpen, roleId, onLoadRole]); + + return ( + + Close + + } + > + {isLoading && ( +
+ +
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!isLoading && !error && role && ( +
+ {/* Basic Information */} +
+

Basic Information

+
+
+ +

{role.name}

+
+
+ +

{role.code}

+
+
+ +
+ + {role.scope} + +
+
+
+ +

{role.id}

+
+
+
+ + {/* Description */} + {role.description && ( +
+

Description

+
+

{role.description}

+
+
+ )} + + {/* Timestamps */} +
+

Timestamps

+
+
+ +

{formatDate(role.created_at)}

+
+
+ +

{formatDate(role.updated_at)}

+
+
+
+
+ )} +
+ ); +}; diff --git a/src/components/shared/ViewTenantModal.tsx b/src/components/shared/ViewTenantModal.tsx new file mode 100644 index 0000000..d6e5249 --- /dev/null +++ b/src/components/shared/ViewTenantModal.tsx @@ -0,0 +1,169 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { Loader2 } from 'lucide-react'; +import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; +import type { Tenant } from '@/types/tenant'; + +interface ViewTenantModalProps { + isOpen: boolean; + onClose: () => void; + tenantId: string | null; + onLoadTenant: (id: string) => Promise; +} + +// Helper function to get status badge variant +const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { + switch (status.toLowerCase()) { + case 'active': + return 'success'; + case 'blocked': + return 'failure'; + case 'suspended': + return 'process'; + default: + return 'success'; + } +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export const ViewTenantModal = ({ + isOpen, + onClose, + tenantId, + onLoadTenant, +}: ViewTenantModalProps): ReactElement | null => { + const [tenant, setTenant] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Load tenant data when modal opens + useEffect(() => { + if (isOpen && tenantId) { + const loadTenant = async (): Promise => { + try { + setIsLoading(true); + setError(null); + const data = await onLoadTenant(tenantId); + setTenant(data); + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load tenant details'); + } finally { + setIsLoading(false); + } + }; + loadTenant(); + } else { + setTenant(null); + setError(null); + } + }, [isOpen, tenantId, onLoadTenant]); + + return ( + + Close + + } + > +
+ {isLoading && ( +
+ +
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!isLoading && !error && tenant && ( +
+ {/* Basic Information */} +
+

Basic Information

+
+
+ +

{tenant.name}

+
+
+ +

{tenant.slug}

+
+
+ +
+ + {tenant.status} + +
+
+
+
+ + {/* Settings */} +
+

Settings

+
+
+ +

+ {tenant.settings?.timezone || 'N/A'} +

+
+
+ +

+ {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'} +

+
+
+ +

{tenant.max_users ?? 'N/A'}

+
+
+ +

{tenant.max_modules ?? 'N/A'}

+
+
+
+ + {/* Timestamps */} +
+

Timestamps

+
+
+ +

{formatDate(tenant.created_at)}

+
+
+ +

{formatDate(tenant.updated_at)}

+
+
+
+
+ )} +
+
+ ); +}; diff --git a/src/components/shared/ViewUserModal.tsx b/src/components/shared/ViewUserModal.tsx new file mode 100644 index 0000000..c9fcef5 --- /dev/null +++ b/src/components/shared/ViewUserModal.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import { Loader2 } from 'lucide-react'; +import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; +import type { User } from '@/types/user'; + +// Helper function to get status badge variant +const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { + switch (status.toLowerCase()) { + case 'active': + return 'success'; + case 'blocked': + return 'failure'; + case 'suspended': + return 'process'; + default: + return 'success'; + } +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +interface ViewUserModalProps { + isOpen: boolean; + onClose: () => void; + userId: string | null; + onLoadUser: (id: string) => Promise; +} + +export const ViewUserModal = ({ + isOpen, + onClose, + userId, + onLoadUser, +}: ViewUserModalProps): ReactElement | null => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Load user data when modal opens + useEffect(() => { + if (isOpen && userId) { + const loadUser = async (): Promise => { + try { + setIsLoading(true); + setError(null); + const data = await onLoadUser(userId); + setUser(data); + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load user details'); + } finally { + setIsLoading(false); + } + }; + loadUser(); + } else { + setUser(null); + setError(null); + } + }, [isOpen, userId, onLoadUser]); + + return ( + + Close + + } + > +
+ {isLoading && ( +
+ +
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!isLoading && !error && user && ( +
+ {/* Basic Information */} +
+

Basic Information

+
+
+ +

{user.email}

+
+
+ +

+ {user.first_name} {user.last_name} +

+
+
+ +
+ + {user.status} + +
+
+
+ +

{user.auth_provider}

+
+ {user.tenant_id && ( +
+ +

{user.tenant_id}

+
+ )} +
+
+ + {/* Timestamps */} +
+

Timestamps

+
+
+ +

{formatDate(user.created_at)}

+
+
+ +

{formatDate(user.updated_at)}

+
+
+
+
+ )} +
+
+ ); +}; diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts new file mode 100644 index 0000000..70cf955 --- /dev/null +++ b/src/components/shared/index.ts @@ -0,0 +1,25 @@ +export { PrimaryButton } from './PrimaryButton'; +export { SecondaryButton } from './SecondaryButton'; +export { ThemeButton } from './ThemeButton'; +export { ActionDropdown } from './ActionDropdown'; +export { FormField } from './FormField'; +export { FormSelect } from './FormSelect'; +export { PaginatedSelect } from './PaginatedSelect'; +export { StatusBadge } from './StatusBadge'; +export { Modal } from './Modal'; +export { DataTable } from './DataTable'; +export type { Column } from './DataTable'; +export { Pagination } from './Pagination'; +export { FilterDropdown } from './FilterDropdown'; +export { NewTenantModal } from './NewTenantModal'; +export { ViewTenantModal } from './ViewTenantModal'; +export { EditTenantModal } from './EditTenantModal'; +export { DeleteConfirmationModal } from './DeleteConfirmationModal'; +export { NewUserModal } from './NewUserModal'; +export { ViewUserModal } from './ViewUserModal'; +export { EditUserModal } from './EditUserModal'; +export { NewRoleModal } from './NewRoleModal'; +export { ViewRoleModal } from './ViewRoleModal'; +export { EditRoleModal } from './EditRoleModal'; +export { PageHeader } from './PageHeader'; +export type { TabItem } from './PageHeader'; \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..37a7d4b --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/src/features/dashboard/components/DashboardTabs.tsx b/src/features/dashboard/components/DashboardTabs.tsx new file mode 100644 index 0000000..7ec8ec3 --- /dev/null +++ b/src/features/dashboard/components/DashboardTabs.tsx @@ -0,0 +1,29 @@ +import { cn } from '@/lib/utils'; + +interface DashboardTabsProps { + activeTab: string; + onTabChange: (tab: string) => void; +} + +const tabs = ['Overview', 'Tenants', 'Users', 'Roles', 'Modules', 'Audit Logs']; + +export const DashboardTabs = ({ activeTab, onTabChange }: DashboardTabsProps) => { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +}; diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx new file mode 100644 index 0000000..4c8db66 --- /dev/null +++ b/src/features/dashboard/components/QuickActions.tsx @@ -0,0 +1,42 @@ +import { useNavigate } from 'react-router-dom'; +import { Plus, UserPlus, Shield, Settings } from 'lucide-react'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; +import type { QuickAction } from '@/types/dashboard'; + +export const QuickActions = () => { + const navigate = useNavigate(); + + const quickActions: QuickAction[] = [ + { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') }, + { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/users') }, + { icon: Shield, label: 'Add Role', onClick: () => navigate('/roles') }, + { icon: Settings, label: 'Config', onClick: () => console.log('Config') }, + ]; + + return ( + + +

Quick Actions

+
+ +
+ {quickActions.map((action, index) => { + const Icon = action.icon; + return ( + + ); + })} +
+
+
+ ); +}; diff --git a/src/features/dashboard/components/RecentActivity.tsx b/src/features/dashboard/components/RecentActivity.tsx new file mode 100644 index 0000000..12c210f --- /dev/null +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -0,0 +1,140 @@ +import { ChevronDown, Filter } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; +import type { ActivityLog } from '@/types/dashboard'; +import { cn } from '@/lib/utils'; + +const activityData: ActivityLog[] = [ + { + action: 'CREATE', + resourceType: 'USER', + resourceId: 'john@acme.com', + ipAddress: '192.168.1.100', + timestamp: '2 mins ago', + }, + { + action: 'UPDATE', + resourceType: 'TENANT', + resourceId: 'acme-medical', + ipAddress: '10.0.0.21', + timestamp: '5 mins ago', + }, + { + action: 'SUSPEND', + resourceType: 'USER', + resourceId: 'alice@globex.io', + ipAddress: '172.16.3.8', + timestamp: '12 mins ago', + }, + { + action: 'LOGIN', + resourceType: 'SESSION', + resourceId: 'session-98f2a', + ipAddress: '192.168.10.4', + timestamp: '20 mins ago', + }, + { + action: 'LOGOUT', + resourceType: 'SESSION', + resourceId: 'session-77bd1', + ipAddress: '192.168.10.4', + timestamp: '28 mins ago', + }, + { + action: 'CREATE', + resourceType: 'MODULE', + resourceId: 'hello-world-v1', + ipAddress: '10.1.0.15', + timestamp: '35 mins ago', + }, +]; + +const getActionBadgeClass = (action: ActivityLog['action']): string => { + const baseClass = 'px-2.5 py-1 rounded-full text-[11px] font-semibold uppercase h-[14px]'; + switch (action) { + case 'CREATE': + return cn(baseClass, 'bg-[rgba(16,185,129,0.1)] text-[#059669]'); + case 'UPDATE': + case 'LOGIN': + return cn(baseClass, 'bg-[rgba(37,99,235,0.1)] text-[#2563eb]'); + case 'SUSPEND': + return cn(baseClass, 'bg-[rgba(245,158,11,0.1)] text-[#d97706]'); + case 'LOGOUT': + return cn(baseClass, 'bg-white text-[#6b7280]'); + default: + return baseClass; + } +}; + +export const RecentActivity = () => { + return ( + + +

Recent Activity

+
+
+ Action + All + +
+
+ +
+ + +
+ + + + + + + + + + + + {activityData.map((activity, index) => ( + + + + + + + + ))} + +
+ Action + + Resource Type + + Resource ID + + IP Address + + Timestamp +
+ + {activity.action} + + + {activity.resourceType} + + {activity.resourceId} + + {activity.ipAddress} + + {activity.timestamp} +
+
+
+ + ); +}; diff --git a/src/features/dashboard/components/StatCard.tsx b/src/features/dashboard/components/StatCard.tsx new file mode 100644 index 0000000..6cd02dd --- /dev/null +++ b/src/features/dashboard/components/StatCard.tsx @@ -0,0 +1,38 @@ +import { cn } from '@/lib/utils'; +import type { StatCardData } from '@/types/dashboard'; + +interface StatCardProps { + data: StatCardData; +} + +export const StatCard = ({ data }: StatCardProps) => { + const Icon = data.icon; + + return ( +
+
+ + {data.badge && ( +
+ {data.badge.text} +
+ )} +
+
+
+ {data.value} +
+
+ {data.label} +
+
+
+ ); +}; diff --git a/src/features/dashboard/components/StatsGrid.tsx b/src/features/dashboard/components/StatsGrid.tsx new file mode 100644 index 0000000..aa42731 --- /dev/null +++ b/src/features/dashboard/components/StatsGrid.tsx @@ -0,0 +1,52 @@ +import { Building2, CheckCircle2, Users, TrendingUp, Package, Heart } from 'lucide-react'; +import { StatCard } from './StatCard'; +import type { StatCardData } from '@/types/dashboard'; + +const statsData: StatCardData[] = [ + { + icon: Building2, + value: '12', + label: 'Total Tenants', + badge: { text: '+3 this week', variant: 'green' }, + }, + { + icon: CheckCircle2, + value: '10', + label: 'Active Tenants', + badge: { text: '92% Rate', variant: 'green' }, + }, + { + icon: Users, + value: '156', + label: 'Total Users', + badge: { text: '+24 new', variant: 'green' }, + }, + { + icon: TrendingUp, + value: '23', + label: 'Active Sessions', + badge: { text: 'Peak 38', variant: 'gray' }, + }, + { + icon: Package, + value: '5', + label: 'Registered Modules', + badge: { text: 'Last 24h', variant: 'gray' }, + }, + { + icon: Heart, + value: '4', + label: 'Healthy Modules', + badge: { text: '100% Uptime', variant: 'green' }, + }, +]; + +export const StatsGrid = () => { + return ( +
+ {statsData.map((stat, index) => ( + + ))} +
+ ); +}; diff --git a/src/features/dashboard/components/SystemHealth.tsx b/src/features/dashboard/components/SystemHealth.tsx new file mode 100644 index 0000000..4ec193b --- /dev/null +++ b/src/features/dashboard/components/SystemHealth.tsx @@ -0,0 +1,60 @@ +import { Activity } from 'lucide-react'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; +import type { HealthMetric } from '@/types/dashboard'; + +const healthMetrics: HealthMetric[] = [ + { label: 'API Latency', value: '45ms', percentage: 100, variant: 'success' }, + { label: 'Database Load', value: '42%', percentage: 42, variant: 'info' }, + { label: 'Storage Usage', value: '78%', percentage: 78, variant: 'warning' }, +]; + +const getVariantStyles = (variant: HealthMetric['variant']) => { + switch (variant) { + case 'success': + return { text: 'text-[#059669]', bg: 'bg-[#059669]' }; + case 'info': + return { text: 'text-[#23dce1]', bg: 'bg-[#23dce1]' }; + case 'warning': + return { text: 'text-[#f59e0b]', bg: 'bg-[#f59e0b]' }; + } +}; + +export const SystemHealth = () => { + return ( + + +

System Health

+ +
+ +
+ {healthMetrics.map((metric, index) => { + const styles = getVariantStyles(metric.variant); + return ( +
+
+ + {metric.label} + + + {metric.value} + +
+
+
+
+
+ ); + })} +
+ + + ); +}; diff --git a/src/hooks/redux-hooks.ts b/src/hooks/redux-hooks.ts new file mode 100644 index 0000000..52e3b4e --- /dev/null +++ b/src/hooks/redux-hooks.ts @@ -0,0 +1,6 @@ +import { useDispatch, useSelector } from 'react-redux'; +import type { TypedUseSelectorHook } from 'react-redux'; +import type { RootState, AppDispatch } from '@/store/store'; + +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..15fcf15 --- /dev/null +++ b/src/index.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..15e40cb --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { store, persistor } from './store/store'; +import './index.css'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + + + + + +); diff --git a/src/pages/AuditLogs.tsx b/src/pages/AuditLogs.tsx new file mode 100644 index 0000000..37685fa --- /dev/null +++ b/src/pages/AuditLogs.tsx @@ -0,0 +1,18 @@ +import { Layout } from "@/components/layout/Layout" +import type { ReactElement } from "react" + +const AuditLogs = (): ReactElement => { + return ( + +
Audit Logs
+
+ ) +} + +export default AuditLogs \ No newline at end of file diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..d234f11 --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,32 @@ +import type { ReactElement } from 'react'; +import { Layout } from '@/components/layout/Layout'; +import { StatsGrid } from '@/features/dashboard/components/StatsGrid'; +import { RecentActivity } from '@/features/dashboard/components/RecentActivity'; +import { QuickActions } from '@/features/dashboard/components/QuickActions'; +import { SystemHealth } from '@/features/dashboard/components/SystemHealth'; + +const Dashboard = (): ReactElement => { + return ( + + {/* Stats Grid */} + + + {/* Bottom Section */} +
+ +
+ + +
+
+
+ ); +}; + +export default Dashboard; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..fb2bc6d --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,182 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import type { ReactElement } from 'react'; +import { Shield } from 'lucide-react'; +import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks'; +import { loginAsync, clearError } from '@/store/authSlice'; +import { FormField } from '@/components/shared'; +import { PrimaryButton } from '@/components/shared'; +import type { LoginError } from '@/services/auth-service'; + +// Zod validation schema +const loginSchema = z.object({ + email: z + .string() + .min(1, 'Email is required') + .email('Please enter a valid email address'), + password: z + .string() + .min(1, 'Password is required') + .min(6, 'Password must be at least 6 characters'), +}); + +type LoginFormData = z.infer; + +const Login = (): ReactElement => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { isLoading, error, isAuthenticated } = useAppSelector((state) => state.auth); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + clearErrors, + } = useForm({ + resolver: zodResolver(loginSchema), + mode: 'onBlur', // Validate on blur for better UX + }); + + const [generalError, setGeneralError] = useState(''); + + // Redirect if already authenticated + useEffect(() => { + if (isAuthenticated) { + navigate('/dashboard'); + } + }, [isAuthenticated, navigate]); + + // Clear errors when component mounts + useEffect(() => { + dispatch(clearError()); + setGeneralError(''); + }, [dispatch]); + + const onSubmit = async (data: LoginFormData, event?: React.BaseSyntheticEvent): Promise => { + // Explicitly prevent form default submission + event?.preventDefault(); + event?.stopPropagation(); + + setGeneralError(''); + clearErrors(); + + try { + const result = await dispatch(loginAsync(data)).unwrap(); + if (result) { + navigate('/dashboard'); + } + } catch (error: any) { + if (error?.payload) { + const loginError = error.payload as LoginError; + + if ('details' in loginError && Array.isArray(loginError.details)) { + // Validation errors from server - set field-specific errors + loginError.details.forEach((detail) => { + if (detail.path === 'email' || detail.path === 'password') { + setError(detail.path as keyof LoginFormData, { + type: 'server', + message: detail.message, + }); + } + }); + } else if ('error' in loginError && typeof loginError.error === 'object') { + // General error from server + setGeneralError(loginError.error.message || 'Login failed'); + } else { + setGeneralError('An unexpected error occurred'); + } + } else { + setGeneralError(error?.message || 'Login failed'); + } + } + }; + + return ( +
+
+ {/* Login Card */} +
+ {/* Logo Section */} +
+
+
+ +
+
+ QAssure +
+
+
+
+

+ Welcome Back +

+

+ Sign in to your account to continue +

+
+ + {/* General Error Message */} + {(generalError || error) && ( +
+

{generalError || error}

+
+ )} + +
{ + e.preventDefault(); + handleSubmit(onSubmit)(e); + }} + className="space-y-4" + > + {/* Email Field */} + + + {/* Password Field */} + + + {/* Submit Button */} +
+ + {isLoading ? 'Signing in...' : 'Sign In'} + +
+ +
+ + {/* Footer */} +
+

+ © 2026 QAssure. All rights reserved. +

+
+
+
+ ); +}; + +export default Login; diff --git a/src/pages/Modules.tsx b/src/pages/Modules.tsx new file mode 100644 index 0000000..d6ff5d6 --- /dev/null +++ b/src/pages/Modules.tsx @@ -0,0 +1,18 @@ +import { Layout } from "@/components/layout/Layout" +import type { ReactElement } from "react" + +const Modules = (): ReactElement => { + return ( + +
Modules
+
+ ) +} + +export default Modules \ No newline at end of file diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..5642926 --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,64 @@ +import { useNavigate } from 'react-router-dom'; +import type { ReactElement } from 'react'; +import { Layout } from '@/components/layout/Layout'; +import { PrimaryButton } from '@/components/shared'; +import { Home, ArrowLeft } from 'lucide-react'; + +const NotFound = (): ReactElement => { + const navigate = useNavigate(); + + const handleGoHome = (): void => { + navigate('/dashboard'); + }; + + const handleGoBack = (): void => { + navigate(-1); + }; + + return ( + +
+ {/* 404 Number */} +
+

+ 404 +

+
+ + {/* Error Message */} +
+

+ Page Not Found +

+

+ The page you're looking for doesn't exist or has been moved. Please check the URL or + navigate back to the dashboard. +

+
+ + {/* Action Buttons */} +
+ + + Go to Dashboard + + + + Go Back + +
+
+
+ ); +}; + +export default NotFound; diff --git a/src/pages/ProtectedRoute.tsx b/src/pages/ProtectedRoute.tsx new file mode 100644 index 0000000..3a2f009 --- /dev/null +++ b/src/pages/ProtectedRoute.tsx @@ -0,0 +1,15 @@ +import { Navigate } from 'react-router-dom'; +import { useAppSelector } from '@/hooks/redux-hooks'; +import type { ReactElement } from 'react'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +const ProtectedRoute = ({ children }: ProtectedRouteProps): ReactElement => { + const { isAuthenticated } = useAppSelector((state) => state.auth); + + return isAuthenticated ? <>{children} : ; +}; + +export default ProtectedRoute; diff --git a/src/pages/Roles.tsx b/src/pages/Roles.tsx new file mode 100644 index 0000000..195b2d3 --- /dev/null +++ b/src/pages/Roles.tsx @@ -0,0 +1,437 @@ +import { useState, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { Layout } from '@/components/layout/Layout'; +import { + PrimaryButton, + StatusBadge, + ActionDropdown, + NewRoleModal, + ViewRoleModal, + EditRoleModal, + DeleteConfirmationModal, + DataTable, + Pagination, + FilterDropdown, + type Column, +} from '@/components/shared'; +import { Plus, Download, ArrowUpDown } from 'lucide-react'; +import { roleService } from '@/services/role-service'; +import type { Role } from '@/types/role'; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +// Helper function to get scope badge variant +const getScopeVariant = (scope: string): 'success' | 'failure' | 'process' => { + switch (scope.toLowerCase()) { + case 'platform': + return 'success'; + case 'tenant': + return 'process'; + case 'module': + return 'failure'; + default: + return 'success'; + } +}; + +const Roles = (): ReactElement => { + const [roles, setRoles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(20); + const [pagination, setPagination] = useState<{ + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; + }>({ + page: 1, + limit: 20, + total: 0, + totalPages: 1, + hasMore: false, + }); + + // Filter state + const [scopeFilter, setScopeFilter] = useState(null); + const [orderBy, setOrderBy] = useState(null); + + // View, Edit, Delete modals + const [viewModalOpen, setViewModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [selectedRoleId, setSelectedRoleId] = useState(null); + const [selectedRoleName, setSelectedRoleName] = useState(''); + const [isUpdating, setIsUpdating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const fetchRoles = async ( + page: number, + itemsPerPage: number, + scope: string | null = null, + sortBy: string[] | null = null + ): Promise => { + try { + setIsLoading(true); + setError(null); + const response = await roleService.getAll(page, itemsPerPage, scope, sortBy); + if (response.success) { + setRoles(response.data); + setPagination(response.pagination); + } else { + setError('Failed to load roles'); + } + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load roles'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchRoles(currentPage, limit, scopeFilter, orderBy); + }, [currentPage, limit, scopeFilter, orderBy]); + + const handleCreateRole = async (data: { + name: string; + code: string; + description: string; + scope: 'platform' | 'tenant' | 'module'; + }): Promise => { + try { + setIsCreating(true); + await roleService.create(data); + setIsModalOpen(false); + await fetchRoles(currentPage, limit, scopeFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsCreating(false); + } + }; + + // View role handler + const handleViewRole = (roleId: string): void => { + setSelectedRoleId(roleId); + setViewModalOpen(true); + }; + + // Edit role handler + const handleEditRole = (roleId: string, roleName: string): void => { + setSelectedRoleId(roleId); + setSelectedRoleName(roleName); + setEditModalOpen(true); + }; + + // Update role handler + const handleUpdateRole = async ( + id: string, + data: { + name: string; + code: string; + description: string; + scope: 'platform' | 'tenant' | 'module'; + } + ): Promise => { + try { + setIsUpdating(true); + await roleService.update(id, data); + setEditModalOpen(false); + setSelectedRoleId(null); + await fetchRoles(currentPage, limit, scopeFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsUpdating(false); + } + }; + + // Delete role handler + const handleDeleteRole = (roleId: string, roleName: string): void => { + setSelectedRoleId(roleId); + setSelectedRoleName(roleName); + setDeleteModalOpen(true); + }; + + // Confirm delete handler + const handleConfirmDelete = async (): Promise => { + if (!selectedRoleId) return; + + try { + setIsDeleting(true); + await roleService.delete(selectedRoleId); + setDeleteModalOpen(false); + setSelectedRoleId(null); + setSelectedRoleName(''); + await fetchRoles(currentPage, limit, scopeFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsDeleting(false); + } + }; + + // Load role for view/edit + const loadRole = async (id: string): Promise => { + const response = await roleService.getById(id); + return response.data; + }; + + // Table columns + const columns: Column[] = [ + { + key: 'name', + label: 'Role Name', + render: (role) => ( + {role.name} + ), + }, + { + key: 'code', + label: 'Code', + render: (role) => ( + {role.code} + ), + }, + { + key: 'scope', + label: 'Scope', + render: (role) => ( + {role.scope} + ), + }, + { + key: 'description', + label: 'Description', + render: (role) => ( + + {role.description || 'N/A'} + + ), + }, + { + key: 'created_at', + label: 'Created Date', + render: (role) => ( + {formatDate(role.created_at)} + ), + }, + { + key: 'actions', + label: 'Actions', + align: 'right', + render: (role) => ( +
+ handleViewRole(role.id)} + onEdit={() => handleEditRole(role.id, role.name)} + onDelete={() => handleDeleteRole(role.id, role.name)} + /> +
+ ), + }, + ]; + + // Mobile card renderer + const mobileCardRenderer = (role: Role) => ( +
+
+
+

{role.name}

+

{role.code}

+
+ handleViewRole(role.id)} + onEdit={() => handleEditRole(role.id, role.name)} + onDelete={() => handleDeleteRole(role.id, role.name)} + /> +
+
+
+ Scope: +
+ {role.scope} +
+
+
+ Created: +

{formatDate(role.created_at)}

+
+ {role.description && ( +
+ Description: +

{role.description}

+
+ )} +
+
+ ); + + return ( + + {/* Table Container */} +
+ {/* Table Header with Filters */} +
+ {/* Filters */} +
+ {/* Scope Filter */} + { + setScopeFilter(value as string | null); + setCurrentPage(1); + }} + placeholder="All" + /> + + {/* Sort Filter */} + { + setOrderBy(value as string[] | null); + setCurrentPage(1); + }} + placeholder="Default" + showIcon + icon={} + /> +
+ + {/* Actions */} +
+ {/* Export Button */} + + + {/* New Role Button */} + setIsModalOpen(true)} + > + + New Role + +
+
+ + {/* Table */} + role.id} + mobileCardRenderer={mobileCardRenderer} + emptyMessage="No roles found" + isLoading={isLoading} + error={error} + /> + + {/* Table Footer with Pagination */} + {pagination.total > 0 && ( + { + setCurrentPage(page); + }} + onLimitChange={(newLimit: number) => { + setLimit(newLimit); + setCurrentPage(1); + }} + /> + )} +
+ + {/* New Role Modal */} + setIsModalOpen(false)} + onSubmit={handleCreateRole} + isLoading={isCreating} + /> + + {/* View Role Modal */} + { + setViewModalOpen(false); + setSelectedRoleId(null); + }} + roleId={selectedRoleId} + onLoadRole={loadRole} + /> + + {/* Edit Role Modal */} + { + setEditModalOpen(false); + setSelectedRoleId(null); + setSelectedRoleName(''); + }} + roleId={selectedRoleId} + onLoadRole={loadRole} + onSubmit={handleUpdateRole} + isLoading={isUpdating} + /> + + {/* Delete Confirmation Modal */} + { + setDeleteModalOpen(false); + setSelectedRoleId(null); + setSelectedRoleName(''); + }} + onConfirm={handleConfirmDelete} + title="Delete Role" + message={`Are you sure you want to delete this role`} + itemName={selectedRoleName} + isLoading={isDeleting} + /> +
+ ); +}; + +export default Roles; diff --git a/src/pages/Tenants.tsx b/src/pages/Tenants.tsx new file mode 100644 index 0000000..5532235 --- /dev/null +++ b/src/pages/Tenants.tsx @@ -0,0 +1,553 @@ +import { useState, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { Layout } from '@/components/layout/Layout'; +import { + PrimaryButton, + StatusBadge, + ActionDropdown, + NewTenantModal, + ViewTenantModal, + EditTenantModal, + DeleteConfirmationModal, + Pagination, + FilterDropdown, +} from '@/components/shared'; +import { Plus, Download, ArrowUpDown } from 'lucide-react'; +import { tenantService } from '@/services/tenant-service'; +import type { Tenant } from '@/types/tenant'; + +// Helper function to get tenant initials +const getTenantInitials = (name: string): string => { + const words = name.trim().split(/\s+/); + if (words.length >= 2) { + return `${words[0][0]}${words[1][0]}`.toUpperCase(); + } + return name.substring(0, 2).toUpperCase(); +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +// Helper function to get status badge variant +const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { + switch (status.toLowerCase()) { + case 'active': + return 'success'; + case 'blocked': + return 'failure'; + case 'suspended': + return 'process'; + default: + return 'success'; + } +}; + +// Helper function to format subscription tier +const formatSubscriptionTier = (tier: string | null): string => { + if (!tier) return 'N/A'; + return tier.charAt(0).toUpperCase() + tier.slice(1); +}; + +const Tenants = (): ReactElement => { + const [tenants, setTenants] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(5); + const [pagination, setPagination] = useState<{ + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; + }>({ + page: 1, + limit: 5, + total: 0, + totalPages: 1, + hasMore: false, + }); + + // Filter state + const [statusFilter, setStatusFilter] = useState(null); + const [orderBy, setOrderBy] = useState(null); + + // View, Edit, Delete modals + const [viewModalOpen, setViewModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [selectedTenantId, setSelectedTenantId] = useState(null); + const [selectedTenantName, setSelectedTenantName] = useState(''); + const [isUpdating, setIsUpdating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const fetchTenants = async ( + page: number, + itemsPerPage: number, + status: string | null = null, + sortBy: string[] | null = null + ): Promise => { + try { + setIsLoading(true); + setError(null); + const response = await tenantService.getAll(page, itemsPerPage, status, sortBy); + if (response.success) { + setTenants(response.data); + setPagination(response.pagination); + } else { + setError('Failed to load tenants'); + } + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load tenants'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchTenants(currentPage, limit, statusFilter, orderBy); + }, [currentPage, limit, statusFilter, orderBy]); + + const handleCreateTenant = async (data: { + name: string; + slug: string; + status: 'active' | 'suspended' | 'blocked'; + timezone: string; + }): Promise => { + try { + setIsCreating(true); + await tenantService.create({ + name: data.name, + slug: data.slug, + status: data.status, + settings: { + timezone: data.timezone, + }, + }); + // Close modal and refresh tenant list + setIsModalOpen(false); + await fetchTenants(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; // Let the modal handle the error display + } finally { + setIsCreating(false); + } + }; + + // View tenant handler + const handleViewTenant = (tenantId: string): void => { + setSelectedTenantId(tenantId); + setViewModalOpen(true); + }; + + // Edit tenant handler + const handleEditTenant = (tenantId: string, tenantName: string): void => { + setSelectedTenantId(tenantId); + setSelectedTenantName(tenantName); + setEditModalOpen(true); + }; + + // Update tenant handler + const handleUpdateTenant = async ( + id: string, + data: { + name: string; + slug: string; + status: 'active' | 'suspended' | 'blocked'; + timezone: string; + } + ): Promise => { + try { + setIsUpdating(true); + await tenantService.update(id, { + name: data.name, + slug: data.slug, + status: data.status, + settings: { + timezone: data.timezone, + }, + }); + setEditModalOpen(false); + setSelectedTenantId(null); + await fetchTenants(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; // Let the modal handle the error display + } finally { + setIsUpdating(false); + } + }; + + // Delete tenant handler + const handleDeleteTenant = (tenantId: string, tenantName: string): void => { + setSelectedTenantId(tenantId); + setSelectedTenantName(tenantName); + setDeleteModalOpen(true); + }; + + // Confirm delete handler + const handleConfirmDelete = async (): Promise => { + if (!selectedTenantId) return; + + try { + setIsDeleting(true); + await tenantService.delete(selectedTenantId); + setDeleteModalOpen(false); + setSelectedTenantId(null); + setSelectedTenantName(''); + await fetchTenants(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; // Let the modal handle the error display + } finally { + setIsDeleting(false); + } + }; + + // Load tenant for view/edit + const loadTenant = async (id: string): Promise => { + const response = await tenantService.getById(id); + return response.data; + }; + + return ( + + + {/* Table Container */} +
+ {/* Table Header with Filters */} +
+ {/* Filters */} +
+ {/* Status Filter */} + { + setStatusFilter(value as string | null); + setCurrentPage(1); // Reset to first page when filter changes + }} + placeholder="All" + /> + + {/* Sort Filter */} + { + setOrderBy(value as string[] | null); + setCurrentPage(1); // Reset to first page when sort changes + }} + placeholder="Default" + showIcon + icon={} + /> +
+ + {/* Actions */} +
+ {/* Export Button */} + + + {/* New Tenant Button */} + setIsModalOpen(true)} + > + + New Tenant + +
+
+ + {/* Loading State */} + {isLoading && ( +
+

Loading tenants...

+
+ )} + + {/* Error State */} + {error && !isLoading && ( +
+

{error}

+
+ )} + + {/* Table */} + {!isLoading && !error && ( + <> + {/* Desktop Table */} +
+ + + + + + + + + + + + + + {tenants.length === 0 ? ( + + + + ) : ( + tenants.map((tenant) => ( + + {/* Tenant Name */} + + + {/* Status */} + + + {/* Users */} + + + {/* Plan */} + + + {/* Modules */} + + + {/* Joined Date */} + + + {/* Actions */} + + + )) + )} + +
+ Tenant Name + + Status + + Users + + Plan + + Modules + + Joined Date + + Actions +
+ No tenants found +
+
+
+ + {getTenantInitials(tenant.name)} + +
+ + {tenant.name} + +
+
+ + {tenant.status} + + + + {tenant.max_users ?? 'N/A'} + + + + {formatSubscriptionTier(tenant.subscription_tier)} + + + + {tenant.max_modules ?? 'N/A'} + + + + {formatDate(tenant.created_at)} + + +
+ handleViewTenant(tenant.id)} + onEdit={() => handleEditTenant(tenant.id, tenant.name)} + onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} + /> +
+
+
+ + {/* Mobile Card View */} +
+ {tenants.length === 0 ? ( +
+

No tenants found

+
+ ) : ( + tenants.map((tenant) => ( +
+
+
+
+ + {getTenantInitials(tenant.name)} + +
+
+

+ {tenant.name} +

+

+ {formatDate(tenant.created_at)} +

+
+
+ handleViewTenant(tenant.id)} + onEdit={() => handleEditTenant(tenant.id, tenant.name)} + onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} + /> +
+
+
+ Status: +
+ + {tenant.status} + +
+
+
+ Plan: +

+ {formatSubscriptionTier(tenant.subscription_tier)} +

+
+
+ Users: +

+ {tenant.max_users ?? 'N/A'} +

+
+
+ Modules: +

+ {tenant.max_modules ?? 'N/A'} +

+
+
+
+ )) + )} +
+ + {/* Table Footer with Pagination */} + {pagination.total > 0 && ( + { + setCurrentPage(page); + }} + onLimitChange={(newLimit: number) => { + setLimit(newLimit); + setCurrentPage(1); // Reset to first page when limit changes + }} + /> + )} + + )} +
+ + {/* New Tenant Modal */} + setIsModalOpen(false)} + onSubmit={handleCreateTenant} + isLoading={isCreating} + /> + + {/* View Tenant Modal */} + { + setViewModalOpen(false); + setSelectedTenantId(null); + }} + tenantId={selectedTenantId} + onLoadTenant={loadTenant} + /> + + {/* Edit Tenant Modal */} + { + setEditModalOpen(false); + setSelectedTenantId(null); + setSelectedTenantName(''); + }} + tenantId={selectedTenantId} + onLoadTenant={loadTenant} + onSubmit={handleUpdateTenant} + isLoading={isUpdating} + /> + + {/* Delete Confirmation Modal */} + { + setDeleteModalOpen(false); + setSelectedTenantId(null); + setSelectedTenantName(''); + }} + onConfirm={handleConfirmDelete} + title="Delete Tenant" + message="Are you sure you want to delete this tenant" + itemName={selectedTenantName} + isLoading={isDeleting} + /> +
+ ); +}; + +export default Tenants; diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx new file mode 100644 index 0000000..7cd5353 --- /dev/null +++ b/src/pages/Users.tsx @@ -0,0 +1,470 @@ +import { useState, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import { Layout } from '@/components/layout/Layout'; +import { + PrimaryButton, + StatusBadge, + ActionDropdown, + NewUserModal, + ViewUserModal, + EditUserModal, + DeleteConfirmationModal, + DataTable, + Pagination, + FilterDropdown, + type Column, +} from '@/components/shared'; +import { Plus, Download, ArrowUpDown } from 'lucide-react'; +import { userService } from '@/services/user-service'; +import type { User } from '@/types/user'; + +// Helper function to get user initials +const getUserInitials = (firstName: string, lastName: string): string => { + return `${firstName[0]}${lastName[0]}`.toUpperCase(); +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +// Helper function to get status badge variant +const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { + switch (status.toLowerCase()) { + case 'active': + return 'success'; + case 'pending_verification': + return 'process'; + case 'inactive': + return 'failure'; + case 'blocked': + return 'failure'; + case 'suspended': + return 'process'; + default: + return 'success'; + } +}; + +const Users = (): ReactElement => { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(5); + const [pagination, setPagination] = useState<{ + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; + }>({ + page: 1, + limit: 5, + total: 0, + totalPages: 1, + hasMore: false, + }); + + // Filter state + const [statusFilter, setStatusFilter] = useState(null); + const [orderBy, setOrderBy] = useState(null); + + // View, Edit, Delete modals + const [viewModalOpen, setViewModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedUserName, setSelectedUserName] = useState(''); + const [isUpdating, setIsUpdating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const fetchUsers = async ( + page: number, + itemsPerPage: number, + status: string | null = null, + sortBy: string[] | null = null + ): Promise => { + try { + setIsLoading(true); + setError(null); + const response = await userService.getAll(page, itemsPerPage, status, sortBy); + if (response.success) { + setUsers(response.data); + setPagination(response.pagination); + } else { + setError('Failed to load users'); + } + } catch (err: any) { + setError(err?.response?.data?.error?.message || 'Failed to load users'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchUsers(currentPage, limit, statusFilter, orderBy); + }, [currentPage, limit, statusFilter, orderBy]); + + const handleCreateUser = async (data: { + email: string; + password: string; + first_name: string; + last_name: string; + status: 'active' | 'suspended' | 'blocked'; + auth_provider: 'local'; + tenant_id: string; + role_id: string; + }): Promise => { + try { + setIsCreating(true); + await userService.create(data); + setIsModalOpen(false); + await fetchUsers(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsCreating(false); + } + }; + + // View user handler + const handleViewUser = (userId: string): void => { + setSelectedUserId(userId); + setViewModalOpen(true); + }; + + // Edit user handler + const handleEditUser = (userId: string, userName: string): void => { + setSelectedUserId(userId); + setSelectedUserName(userName); + setEditModalOpen(true); + }; + + // Update user handler + const handleUpdateUser = async ( + id: string, + data: { + email: string; + first_name: string; + last_name: string; + status: 'active' | 'suspended' | 'blocked'; + tenant_id: string; + role_id: string; + } + ): Promise => { + try { + setIsUpdating(true); + await userService.update(id, data); + setEditModalOpen(false); + setSelectedUserId(null); + await fetchUsers(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsUpdating(false); + } + }; + + // Delete user handler + const handleDeleteUser = (userId: string, userName: string): void => { + setSelectedUserId(userId); + setSelectedUserName(userName); + setDeleteModalOpen(true); + }; + + // Confirm delete handler + const handleConfirmDelete = async (): Promise => { + if (!selectedUserId) return; + + try { + setIsDeleting(true); + await userService.delete(selectedUserId); + setDeleteModalOpen(false); + setSelectedUserId(null); + setSelectedUserName(''); + await fetchUsers(currentPage, limit, statusFilter, orderBy); + } catch (err: any) { + throw err; + } finally { + setIsDeleting(false); + } + }; + + // Load user for view/edit + const loadUser = async (id: string): Promise => { + const response = await userService.getById(id); + return response.data; + }; + + // Define table columns + const columns: Column[] = [ + { + key: 'name', + label: 'User Name', + render: (user) => ( +
+
+ + {getUserInitials(user.first_name, user.last_name)} + +
+ + {user.first_name} {user.last_name} + +
+ ), + mobileLabel: 'Name', + }, + { + key: 'email', + label: 'Email', + render: (user) => {user.email}, + }, + { + key: 'status', + label: 'Status', + render: (user) => ( + {user.status} + ), + }, + { + key: 'auth_provider', + label: 'Auth Provider', + render: (user) => ( + {user.auth_provider} + ), + }, + { + key: 'created_at', + label: 'Joined Date', + render: (user) => ( + {formatDate(user.created_at)} + ), + mobileLabel: 'Joined', + }, + { + key: 'actions', + label: 'Actions', + align: 'right', + render: (user) => ( +
+ handleViewUser(user.id)} + onEdit={() => handleEditUser(user.id, `${user.first_name} ${user.last_name}`)} + onDelete={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)} + /> +
+ ), + }, + ]; + + // Mobile card renderer + const mobileCardRenderer = (user: User) => ( +
+
+
+
+ + {getUserInitials(user.first_name, user.last_name)} + +
+
+

+ {user.first_name} {user.last_name} +

+

{user.email}

+
+
+ handleViewUser(user.id)} + onEdit={() => handleEditUser(user.id, `${user.first_name} ${user.last_name}`)} + onDelete={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)} + /> +
+
+
+ Status: +
+ {user.status} +
+
+
+ Auth Provider: +

{user.auth_provider}

+
+
+ Joined: +

{formatDate(user.created_at)}

+
+
+
+ ); + + return ( + + {/* Table Container */} +
+ {/* Table Header with Filters */} +
+ {/* Filters */} +
+ {/* Status Filter */} + { + setStatusFilter(value as string | null); + setCurrentPage(1); // Reset to first page when filter changes + }} + placeholder="All" + /> + + {/* Sort Filter */} + { + setOrderBy(value as string[] | null); + setCurrentPage(1); // Reset to first page when sort changes + }} + placeholder="Default" + showIcon + icon={} + /> +
+ + {/* Actions */} +
+ {/* Export Button */} + + + {/* New User Button */} + setIsModalOpen(true)} + > + + New User + +
+
+ + {/* Data Table */} + user.id} + mobileCardRenderer={mobileCardRenderer} + emptyMessage="No users found" + isLoading={isLoading} + error={error} + /> + + {/* Table Footer with Pagination */} + {pagination.total > 0 && ( + { + setCurrentPage(page); + }} + onLimitChange={(newLimit: number) => { + setLimit(newLimit); + setCurrentPage(1); // Reset to first page when limit changes + }} + /> + )} +
+ + {/* New User Modal */} + setIsModalOpen(false)} + onSubmit={handleCreateUser} + isLoading={isCreating} + /> + + {/* View User Modal */} + { + setViewModalOpen(false); + setSelectedUserId(null); + }} + userId={selectedUserId} + onLoadUser={loadUser} + /> + + {/* Edit User Modal */} + { + setEditModalOpen(false); + setSelectedUserId(null); + setSelectedUserName(''); + }} + userId={selectedUserId} + onLoadUser={loadUser} + onSubmit={handleUpdateUser} + isLoading={isUpdating} + /> + + {/* Delete Confirmation Modal */} + { + setDeleteModalOpen(false); + setSelectedUserId(null); + setSelectedUserName(''); + }} + onConfirm={handleConfirmDelete} + title="Delete User" + message="Are you sure you want to delete this user" + itemName={selectedUserName} + isLoading={isDeleting} + /> +
+ ); +}; + +export default Users; diff --git a/src/services/api-client.ts b/src/services/api-client.ts new file mode 100644 index 0000000..dcc4c14 --- /dev/null +++ b/src/services/api-client.ts @@ -0,0 +1,62 @@ +import axios, { type AxiosInstance, type AxiosError, type InternalAxiosRequestConfig } from 'axios'; +import type { RootState } from '@/store/store'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; + +// Create axios instance +const apiClient: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor to add auth token to ALL requests +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // Always try to get token from Redux store and add to Authorization header + try { + const store = (window as any).__REDUX_STORE__; + if (store) { + const state = store.getState() as RootState; + const token = state?.auth?.accessToken; + + // Add Bearer token to all requests if token exists + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + } + } catch (error) { + // Silently fail if store is not available + console.warn('Redux store not available for token injection'); + } + + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +// Response interceptor for error handling +apiClient.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response?.status === 401) { + // Handle unauthorized - clear auth and redirect to login + try { + const store = (window as any).__REDUX_STORE__; + if (store) { + store.dispatch({ type: 'auth/logout' }); + window.location.href = '/'; + } + } catch (e) { + // Silently fail if store is not available + window.location.href = '/'; + } + } + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts new file mode 100644 index 0000000..d1ff676 --- /dev/null +++ b/src/services/auth-service.ts @@ -0,0 +1,64 @@ +import apiClient from './api-client'; + +export interface LoginRequest { + email: string; + password: string; +} + +export interface User { + id: string; + email: string; + first_name: string; + last_name: string; +} + +export interface LoginResponse { + success: boolean; + data: { + user: User; + tenant_id: string; + roles: string[]; + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + expires_at: string; + }; +} + +export interface ValidationError { + success: false; + error: 'Validation failed'; + details: Array<{ + path: string; + message: string; + code: string; + }>; +} + +export interface GeneralError { + success: false; + error: { + code: string; + message: string; + }; +} + +export type LoginError = ValidationError | GeneralError; + +export interface LogoutResponse { + success: boolean; + message?: string; +} + +export const authService = { + login: async (credentials: LoginRequest): Promise => { + const response = await apiClient.post('/auth/login', credentials); + return response.data; + }, + logout: async (): Promise => { + // Token will be automatically added by api-client interceptor + const response = await apiClient.post('/auth/logout', {}); + return response.data; + }, +}; diff --git a/src/services/role-service.ts b/src/services/role-service.ts new file mode 100644 index 0000000..4e44720 --- /dev/null +++ b/src/services/role-service.ts @@ -0,0 +1,48 @@ +import apiClient from './api-client'; +import type { + RolesResponse, + CreateRoleRequest, + CreateRoleResponse, + GetRoleResponse, + UpdateRoleRequest, + UpdateRoleResponse, + DeleteRoleResponse, +} from '@/types/role'; + +export const roleService = { + getAll: async ( + page: number = 1, + limit: number = 20, + scope?: string | null, + orderBy?: string[] | null + ): Promise => { + const params = new URLSearchParams(); + params.append('page', String(page)); + params.append('limit', String(limit)); + if (scope) { + params.append('scope', scope); + } + if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { + params.append('orderBy[]', orderBy[0]); + params.append('orderBy[]', orderBy[1]); + } + const response = await apiClient.get(`/roles?${params.toString()}`); + return response.data; + }, + getById: async (id: string): Promise => { + const response = await apiClient.get(`/roles/${id}`); + return response.data; + }, + create: async (data: CreateRoleRequest): Promise => { + const response = await apiClient.post('/roles', data); + return response.data; + }, + update: async (id: string, data: UpdateRoleRequest): Promise => { + const response = await apiClient.put(`/roles/${id}`, data); + return response.data; + }, + delete: async (id: string): Promise => { + const response = await apiClient.delete(`/roles/${id}`); + return response.data; + }, +}; diff --git a/src/services/tenant-service.ts b/src/services/tenant-service.ts new file mode 100644 index 0000000..77da2cc --- /dev/null +++ b/src/services/tenant-service.ts @@ -0,0 +1,95 @@ +import apiClient from './api-client'; +import type { Tenant, Pagination } from '@/types/tenant'; + +export interface TenantsResponse { + success: boolean; + data: Tenant[]; + pagination: Pagination; +} + +export interface CreateTenantRequest { + name: string; + slug: string; + status: 'active' | 'suspended' | 'blocked'; + settings: { + timezone: string; + }; +} + +export interface CreateTenantResponse { + success: boolean; + data: Tenant; +} + +export interface GeneralError { + success: false; + error: { + code: string; + message: string; + }; +} + +export type TenantsError = GeneralError; + +export interface GetTenantResponse { + success: boolean; + data: Tenant; +} + +export interface UpdateTenantRequest { + name: string; + slug: string; + status: 'active' | 'suspended' | 'blocked'; + settings: { + timezone: string; + }; +} + +export interface UpdateTenantResponse { + success: boolean; + data: Tenant; +} + +export interface DeleteTenantResponse { + success: boolean; + message?: string; +} + +export const tenantService = { + getAll: async ( + page: number = 1, + limit: number = 20, + status?: string | null, + orderBy?: string[] | null + ): Promise => { + const params = new URLSearchParams(); + params.append('page', String(page)); + params.append('limit', String(limit)); + if (status) { + params.append('status', status); + } + if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { + // Send array as orderBy[]=field&orderBy[]=direction + params.append('orderBy[]', orderBy[0]); + params.append('orderBy[]', orderBy[1]); + } + const response = await apiClient.get(`/tenants?${params.toString()}`); + return response.data; + }, + create: async (data: CreateTenantRequest): Promise => { + const response = await apiClient.post('/tenants', data); + return response.data; + }, + getById: async (id: string): Promise => { + const response = await apiClient.get(`/tenants/${id}`); + return response.data; + }, + update: async (id: string, data: UpdateTenantRequest): Promise => { + const response = await apiClient.put(`/tenants/${id}`, data); + return response.data; + }, + delete: async (id: string): Promise => { + const response = await apiClient.delete(`/tenants/${id}`); + return response.data; + }, +}; diff --git a/src/services/user-service.ts b/src/services/user-service.ts new file mode 100644 index 0000000..213fe29 --- /dev/null +++ b/src/services/user-service.ts @@ -0,0 +1,49 @@ +import apiClient from './api-client'; +import type { + UsersResponse, + CreateUserRequest, + CreateUserResponse, + GetUserResponse, + UpdateUserRequest, + UpdateUserResponse, + DeleteUserResponse, +} from '@/types/user'; + +export const userService = { + getAll: async ( + page: number = 1, + limit: number = 20, + status?: string | null, + orderBy?: string[] | null + ): Promise => { + const params = new URLSearchParams(); + params.append('page', String(page)); + params.append('limit', String(limit)); + if (status) { + params.append('status', status); + } + if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { + // Send array as orderBy[]=field&orderBy[]=direction + params.append('orderBy[]', orderBy[0]); + params.append('orderBy[]', orderBy[1]); + } + const response = await apiClient.get(`/users?${params.toString()}`); + return response.data; + }, + create: async (data: CreateUserRequest): Promise => { + const response = await apiClient.post('/users', data); + return response.data; + }, + getById: async (id: string): Promise => { + const response = await apiClient.get(`/users/${id}`); + return response.data; + }, + update: async (id: string, data: UpdateUserRequest): Promise => { + const response = await apiClient.put(`/users/${id}`, data); + return response.data; + }, + delete: async (id: string): Promise => { + const response = await apiClient.delete(`/users/${id}`); + return response.data; + }, +}; diff --git a/src/store/authSlice.ts b/src/store/authSlice.ts new file mode 100644 index 0000000..597a44a --- /dev/null +++ b/src/store/authSlice.ts @@ -0,0 +1,166 @@ +import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; +import { authService, type LoginRequest, type LoginResponse, type LoginError, type GeneralError } from '@/services/auth-service'; + +interface User { + id: string; + email: string; + first_name: string; + last_name: string; +} + +interface AuthState { + user: User | null; + tenantId: string | null; + roles: string[]; + accessToken: string | null; + refreshToken: string | null; + tokenType: string | null; + expiresIn: number | null; + expiresAt: string | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; +} + +const initialState: AuthState = { + user: null, + tenantId: null, + roles: [], + accessToken: null, + refreshToken: null, + tokenType: null, + expiresIn: null, + expiresAt: null, + isAuthenticated: false, + isLoading: false, + error: null, +}; + +// Async thunk for login +export const loginAsync = createAsyncThunk< + LoginResponse['data'], + LoginRequest, + { rejectValue: LoginError } +>('auth/login', async (credentials, { rejectWithValue }) => { + try { + const response = await authService.login(credentials); + if (response.success) { + return response.data; + } + return rejectWithValue(response as unknown as LoginError); + } catch (error: any) { + if (error.response?.data) { + return rejectWithValue(error.response.data as LoginError); + } + return rejectWithValue({ + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error.message || 'An unexpected error occurred', + }, + } as GeneralError); + } +}); + +// Async thunk for logout +export const logoutAsync = createAsyncThunk('auth/logout', async (_, { rejectWithValue }) => { + try { + await authService.logout(); + return true; + } catch (error: any) { + // Even if API call fails, we should still logout locally + return rejectWithValue(error?.response?.data || { message: 'Logout failed' }); + } +}); + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + logout: (state) => { + state.user = null; + state.tenantId = null; + state.roles = []; + state.accessToken = null; + state.refreshToken = null; + state.tokenType = null; + state.expiresIn = null; + state.expiresAt = null; + state.isAuthenticated = false; + state.error = null; + }, + clearError: (state) => { + state.error = null; + }, + }, + extraReducers: (builder) => { + builder + .addCase(loginAsync.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(loginAsync.fulfilled, (state, action: PayloadAction) => { + state.isLoading = false; + state.user = action.payload.user; + state.tenantId = action.payload.tenant_id; + state.roles = action.payload.roles; + state.accessToken = action.payload.access_token; + state.refreshToken = action.payload.refresh_token; + state.tokenType = action.payload.token_type; + state.expiresIn = action.payload.expires_in; + state.expiresAt = action.payload.expires_at; + state.isAuthenticated = true; + state.error = null; + }) + .addCase(loginAsync.rejected, (state, action) => { + state.isLoading = false; + state.isAuthenticated = false; + if (action.payload) { + const error = action.payload; + if ('error' in error && typeof error.error === 'object' && 'message' in error.error) { + // General error + state.error = error.error.message; + } else { + // Validation error or other + state.error = 'Validation failed'; + } + } else { + state.error = action.error.message || 'Login failed'; + } + }) + .addCase(logoutAsync.pending, (state) => { + state.isLoading = true; + }) + .addCase(logoutAsync.fulfilled, (state) => { + // Reset to initial state + state.user = null; + state.tenantId = null; + state.roles = []; + state.accessToken = null; + state.refreshToken = null; + state.tokenType = null; + state.expiresIn = null; + state.expiresAt = null; + state.isAuthenticated = false; + state.isLoading = false; + state.error = null; + }) + .addCase(logoutAsync.rejected, (state) => { + // Even if API call fails, clear local state + state.user = null; + state.tenantId = null; + state.roles = []; + state.accessToken = null; + state.refreshToken = null; + state.tokenType = null; + state.expiresIn = null; + state.expiresAt = null; + state.isAuthenticated = false; + state.isLoading = false; + state.error = null; + }); + }, +}); + +export const { logout, clearError } = authSlice.actions; +export default authSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..bbd6d75 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,35 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { persistStore, persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import authReducer from './authSlice'; + +// Persist config for auth slice only +const authPersistConfig = { + key: 'auth', + storage, + whitelist: ['user', 'tenantId', 'roles', 'accessToken', 'refreshToken', 'tokenType', 'expiresIn', 'expiresAt', 'isAuthenticated'], +}; + +const persistedAuthReducer = persistReducer(authPersistConfig, authReducer); + +export const store = configureStore({ + reducer: { + auth: persistedAuthReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE', 'persist/PAUSE', 'persist/PURGE', 'persist/REGISTER', 'persist/FLUSH'], + }, + }), +}); + +export const persistor = persistStore(store); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +// Make store available globally for axios interceptor +if (typeof window !== 'undefined') { + (window as any).__REDUX_STORE__ = store; +} diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts new file mode 100644 index 0000000..44ae806 --- /dev/null +++ b/src/types/dashboard.ts @@ -0,0 +1,30 @@ +export interface StatCardData { + icon: React.ComponentType<{ className?: string }>; + value: string | number; + label: string; + badge?: { + text: string; + variant: 'green' | 'gray'; + }; +} + +export interface ActivityLog { + action: 'CREATE' | 'UPDATE' | 'SUSPEND' | 'LOGIN' | 'LOGOUT'; + resourceType: string; + resourceId: string; + ipAddress: string; + timestamp: string; +} + +export interface QuickAction { + icon: React.ComponentType<{ className?: string }>; + label: string; + onClick: () => void; +} + +export interface HealthMetric { + label: string; + value: string; + percentage: number; + variant: 'success' | 'info' | 'warning'; +} diff --git a/src/types/role.ts b/src/types/role.ts new file mode 100644 index 0000000..fc9de12 --- /dev/null +++ b/src/types/role.ts @@ -0,0 +1,57 @@ +export interface Role { + id: string; + name: string; + code: string; + description?: string; + scope: 'platform' | 'tenant' | 'module'; + created_at: string; + updated_at: string; +} + +export interface Pagination { + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; +} + +export interface RolesResponse { + success: boolean; + data: Role[]; + pagination: Pagination; +} + +export interface CreateRoleRequest { + name: string; + code: string; + description: string; + scope: 'platform' | 'tenant' | 'module'; +} + +export interface CreateRoleResponse { + success: boolean; + data: Role; +} + +export interface GetRoleResponse { + success: boolean; + data: Role; +} + +export interface UpdateRoleRequest { + name: string; + code: string; + description: string; + scope: 'platform' | 'tenant' | 'module'; +} + +export interface UpdateRoleResponse { + success: boolean; + data: Role; +} + +export interface DeleteRoleResponse { + success: boolean; + message?: string; +} diff --git a/src/types/tenant.ts b/src/types/tenant.ts new file mode 100644 index 0000000..c5eeb0d --- /dev/null +++ b/src/types/tenant.ts @@ -0,0 +1,24 @@ +export interface TenantSettings { + timezone?: string; +} + +export interface Tenant { + id: string; + name: string; + slug: string; + status: 'active' | 'suspended' | 'blocked'; + settings: TenantSettings | null; + subscription_tier: string | null; + max_users: number | null; + max_modules: number | null; + created_at: string; + updated_at: string; +} + +export interface Pagination { + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; +} diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..a4fc62d --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,67 @@ +export interface User { + id: string; + email: string; + first_name: string; + last_name: string; + status: 'active' | 'suspended' | 'blocked'; + auth_provider: string; + tenant_id?: string; + role_id?: string; + created_at: string; + updated_at: string; +} + +export interface Pagination { + page: number; + limit: number; + total: number; + totalPages: number; + hasMore: boolean; +} + +export interface UsersResponse { + success: boolean; + data: User[]; + pagination: Pagination; +} + +export interface CreateUserRequest { + email: string; + password: string; + first_name: string; + last_name: string; + status: 'active' | 'suspended' | 'blocked'; + auth_provider: 'local'; + tenant_id: string; + role_id: string; +} + +export interface CreateUserResponse { + success: boolean; + data: User; +} + +export interface GetUserResponse { + success: boolean; + data: User; +} + +export interface UpdateUserRequest { + email: string; + first_name: string; + last_name: string; + status: 'active' | 'suspended' | 'blocked'; + auth_provider?: string; + tenant_id: string; + role_id: string; +} + +export interface UpdateUserResponse { + success: boolean; + data: User; +} + +export interface DeleteUserResponse { + success: boolean; + message?: string; +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..25d643e --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,21 @@ +import type { RootState } from '@/store/store'; + +// Get store instance for checking auth state +const getStore = () => { + if (typeof window !== 'undefined') { + return (window as any).__REDUX_STORE__; + } + return null; +}; + +export const isAuthenticated = (): boolean => { + const store = getStore(); + if (store) { + const state = store.getState() as RootState; + return state.auth.isAuthenticated && Boolean(state.auth.accessToken); + } + return false; +}; + +// Note: logout is now handled by Redux action +// Use: dispatch(logout()) from components diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..6d7544a --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6849f2c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [{ + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } + +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d278b27 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from "path"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [tailwindcss(),react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + +})