commit 1c7ef95c1ab320d316c84f661ce681a90b07f90e Author: Tiago Yamamoto Date: Tue Dec 9 19:04:48 2025 -0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef618ae --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# 🐴 GoHorse Jobs (Vagas para Tecnologia) + +A comprehensive recruitment platform connecting job seekers with opportunities in the technology sector. + +## 📋 Project Overview + +GoHorse Jobs is a multi-service application designed to facilitate the hiring process. It consists of a high-performance Go backend, a modern Next.js frontend, and a specialized Seeder API for generating realistic test data. + +## 🔐 Credentials (Test Environment) + +Use these credentials to access the different dashboards: + +| User Type | Identifier | Password | Dashboard | +|-----------|------------|----------|-----------| +| **SuperAdmin** | `superadmin` | `Admin@2025!` | `/dashboard/admin` (System Stats) | +| **Company Admin** | `takeshi_yamamoto` | `Takeshi@2025` | `/dashboard/empresa` (Manage Jobs) | +| **Recruiter** | `maria_santos` | `User@2025` | `/dashboard/empresa` (View Candidates) | +| **Candidate** | `paulo_santos` | `User@2025` | `/dashboard/candidato` (Apply for Jobs) | + +## 🏗️ Architecture + +The project follows a microservices-inspired architecture: + +```mermaid +graph TD + User((User)) + Frontend[Frontend (Next.js)] + Backend[Backend (Go/Gin)] + Seeder[Seeder API (Node.js)] + DB[(PostgreSQL)] + + User --> Frontend + Frontend -->|HTTP/REST| Backend + Seeder -->|Writes| DB + Backend -->|Reads/Writes| DB +``` + +## 🚀 Quick Start + +### 1. Run the Backend API +We have provided a convenience script to start the backend quickly. + +```bash +./run_dev.sh +``` + +### 2. Run the Frontend +```bash +cd frontend +npm install +npm run dev +``` + +### 3. Database Setup & Seeding +Refer to `seeder-api/README.md` for detailed instructions on populating the database. + +## 📊 Status & Tasks + +### Completed ✅ +- [x] Backend API Structure +- [x] Docker Configuration +- [x] Frontend Dashboard (Company) +- [x] Seeder Logic (Users, Companies, Jobs) +- [x] Documentation Unification +- [x] Branding Update (GoHorse Jobs) + +### In Progress 🚧 +- [ ] Integration of complete Frontend-Backend flow +- [ ] Advanced Search Filters +- [ ] Real-time Notifications + +## 📚 Documentation +- [Backend Documentation](./backend/README.md) +- [Frontend Documentation](./frontend/README.md) +- [Seeder Documentation](./seeder-api/README.md) + +--- +*Generated by Antigravity* diff --git a/backend/.env.example b/backend/.env.example new file mode 100755 index 0000000..09d9e83 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,22 @@ +# Environment variables for Todai Jobs Backend + +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=yourpassword +DB_NAME=gohorsejobs + +# JWT Secret (CHANGE IN PRODUCTION!) +JWT_SECRET=your-secret-key-change-this-in-production-use-strong-random-value + +# Server Configuration +PORT=8080 +ENV=development + +# CORS Origins (comma-separated) +CORS_ORIGINS=http://localhost:3000 + +# File Upload +MAX_UPLOAD_SIZE=10485760 # 10MB in bytes +UPLOAD_DIR=./uploads diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100755 index 0000000..0c1f4c8 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Environment variables +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..8f50d4a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,37 @@ +# Build stage +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +# Install build dependencies if needed (e.g. gcc for cgo, though we disable cgo) +RUN apk add --no-cache git + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api + +# Run stage +FROM alpine:latest + +WORKDIR /app + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Copy binary from builder +COPY --from=builder /app/main . + +# Copy migrations (CRITICAL for auto-migration logic) +COPY --from=builder /app/migrations ./migrations + +# Expose port +EXPOSE 8080 + +# Environment variables should be passed at runtime, but we can set defaults +ENV PORT=8080 + +CMD ["./main"] diff --git a/backend/README.md b/backend/README.md new file mode 100755 index 0000000..843ca38 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,196 @@ +# GoHorseJobs Backend + +This is the backend for the GoHorseJobs application, built with Go and PostgreSQL. + +## Architecture + +### 1. **Core Module (Clean Architecture)** +The project now includes a strictly decoupled `internal/core` module that follows Clean Architecture and DDD principles. +- **Pure Domain**: `internal/core/domain/entity` (No external deps). +- **Ports**: `internal/core/ports` (Interfaces for Repositories/Services). +- **UseCases**: `internal/core/usecases` (Business Logic). + +### 2. **Multi-Tenancy** +- **Strict Isolation**: All core tables (`core_companies`, `core_users`) use UUIDs and strict Tenant FKs. +- **Middleware**: `TenantGuard` automatically extracts Tenant context from JWTs. + +### 3. **Swagger API Docs** +- **URL**: [http://localhost:8080/swagger/index.html](http://localhost:8080/swagger/index.html) +- **Generate**: `swag init -g cmd/api/main.go --parseDependency --parseInternal` + +### 4. **Super Admin Access** +A seed migration is provided to create the initial system access. +- **Migration**: `migrations/010_seed_super_admin.sql` +- **User**: `admin@gohorse.com` +- **Password**: `password123` + +## Prerequisites + +- Docker +- Docker Compose +- Go 1.22+ + +## Getting Started + +1. **Start the services:** + + ```bash + docker-compose up --build + ``` + + This will start the PostgreSQL database and the Go API server. + + Alternatively, use the running script in the root: + ```bash + ./run_dev.sh + ``` + +2. **Access the API:** + + The API will be available at `http://localhost:8080`. + + - **Health Check:** `GET /health` + - **List Jobs:** `GET /jobs` + - **Create Job:** `POST /jobs` + +## API Endpoints + +### `GET /health` + +Returns `200 OK` if the server is running. + +### `GET /jobs` + +Returns a list of all jobs. + +**Response:** + +```json +[ + { + "id": 1, + "title": "Software Engineer", + "description": "Develop Go applications.", + "company_name": "Tech Corp", + "location": "Remote", + "salary_range": "$100k - $120k", + "created_at": "2023-10-27T10:00:00Z" + } +] +``` + +### `POST /jobs` + +Creates a new job. + +**Request Body:** + +```json +{ + "title": "Software Engineer", + "description": "Develop Go applications.", + "company_name": "Tech Corp", + "location": "Remote", + "salary_range": "$100k - $120k" +} +``` + +**Response:** + +Returns the created job with its ID and creation timestamp. + +--- + +# 📚 Feature Documentation (Archives) + +> [!NOTE] +> The following documentation describes features implemented in the application, including Frontend components and Local Database logic. They have been consolidated here for reference. + +## 🗄️ Local Database - Profile System (Legacy/Frontend) + +Full local database system using localStorage to manage profile photos and user data. + +### 🚀 Usage + +#### 1. Page with Database +Access: `http://localhost:3000/profile-db` + +#### 2. Features Implemented + +**📸 Profile Picture Upload** +- ✅ Click to select image +- ✅ Automatic validation (JPG, PNG, GIF, WebP) +- ✅ 2MB limit per file +- ✅ Instant preview +- ✅ Auto-save to localStorage +- ✅ Loading indicator +- ✅ Remove photo button + +**🗃️ Local Database** +- ✅ Auto-save to localStorage +- ✅ Persistence between sessions +- ✅ Export data (JSON backup) +- ✅ Clear all data +- ✅ User structure +- ✅ Creation/update timestamps + +**🔧 useProfile Hook** +- ✅ Reactive state management +- ✅ Loading states +- ✅ Full CRUD +- ✅ Auto synchronization + +## 🏢 Company Dashboard (Frontend Features) + +### ✅ Complete Features + +#### 1️⃣ Job Management +**Page:** `/dashboard/empresa/vagas` +- ✅ Full listing of published jobs +- ✅ Statistics per job +- ✅ Search and filters +- ✅ Quick actions: View, Edit, Pause, Delete + +#### 2️⃣ Application Management +**Page:** `/dashboard/empresa/candidaturas` +- ✅ View all received applications +- ✅ Statistics cards by status +- ✅ Tabs system +- ✅ Search by candidate name +- ✅ Quick actions: Approve, Reject, Email + +#### 3️⃣ Messaging System +**Page:** `/dashboard/empresa/mensagens` +- ✅ WhatsApp/Slack style chat interface +- ✅ Conversation list with unread counters +- ✅ Real-time message attachment +- ✅ Responsive design + +#### 4️⃣ Analytics & Reports +**Page:** `/dashboard/empresa/relatorios` +- ✅ Key metrics cards +- ✅ Period selector +- ✅ Conversion funnel +- ✅ Hiring time by role + +#### 5️⃣ Company Profile +**Page:** `/dashboard/empresa/perfil` +- ✅ Real logo upload +- ✅ Basic info management +- ✅ Social media links +- ✅ Culture description + +### 🎨 Design System + +**Stack:** +- shadcn/ui +- Tailwind CSS +- Lucide Icons +- Framer Motion + +**Colors:** +- Primary: Blue +- Success: Green +- Warning: Yellow +- Danger: Red +- Muted: Gray diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go new file mode 100755 index 0000000..d57271d --- /dev/null +++ b/backend/cmd/api/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/joho/godotenv" + "github.com/rede5/gohorsejobs/backend/internal/database" + "github.com/rede5/gohorsejobs/backend/internal/router" +) + +// @title GoHorseJobs API +// @version 1.0 +// @description API for GoHorseJobs recruitment platform. +// @host localhost:8080 +// @BasePath / +func main() { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Println("No .env file found or error loading it") + } + + database.InitDB() + database.RunMigrations() + + handler := router.NewRouter() + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Println("Starting server on :" + port) + if err := http.ListenAndServe(":"+port, handler); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/backend/cmd/debug_user/main.go b/backend/cmd/debug_user/main.go new file mode 100644 index 0000000..19a58d6 --- /dev/null +++ b/backend/cmd/debug_user/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "os" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" +) + +func main() { + // Load .env from backend root (cwd is backend/) + if err := godotenv.Load(".env"); err != nil { + log.Println("Warning: Error loading .env file, relying on system env") + } + + host := os.Getenv("DB_HOST") + if host == "" { + // Fallback for debugging if env not picked up + host = "localhost" + } + port := os.Getenv("DB_PORT") + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + dbname := os.Getenv("DB_NAME") + + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + if err := db.Ping(); err != nil { + log.Fatal("Could not connect to DB:", err) + } + + // Check for superadmin + targetEmail := "superadmin" // This is what we put in email column + var id, email, passHash string + err = db.QueryRow("SELECT id, email, password_hash FROM core_users WHERE email = $1", targetEmail).Scan(&id, &email, &passHash) + if err != nil { + log.Fatalf("Error finding user '%s': %v", targetEmail, err) + } + + fmt.Printf("User Found: ID=%s, Email=%s\n", id, email) + fmt.Printf("Stored Hash: %s\n", passHash) + + // Verify Password + targetPass := "Admin@2025!" + err = bcrypt.CompareHashAndPassword([]byte(passHash), []byte(targetPass)) + if err != nil { + fmt.Printf("❌ Password Verification FAILED: %v\n", err) + } else { + fmt.Printf("✅ Password Verification SUCCESS\n") + } +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..720f36a --- /dev/null +++ b/backend/docs/docs.go @@ -0,0 +1,381 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/auth/login": { + "post": { + "description": "Authenticates a user by email and password. Returns JWT and user info.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User Login", + "parameters": [ + { + "description": "Login Credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/companies": { + "post": { + "description": "Registers a new company and creates an initial admin user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies" + ], + "summary": "Create Company (Tenant)", + "parameters": [ + { + "description": "Company Details", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns a list of users belonging to the authenticated tenant.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "List Users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new user under the current tenant. Requires Admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create User", + "parameters": [ + { + "description": "User Details", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/users/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a user by ID. Must belong to the same tenant.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Delete User", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "User deleted", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest": { + "type": "object", + "properties": { + "admin_email": { + "type": "string" + }, + "contact": { + "type": "string" + }, + "document": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "roles": { + "description": "e.g. [\"RECRUITER\"]", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "GoHorseJobs API", + Description: "API for GoHorseJobs recruitment platform.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json new file mode 100644 index 0000000..8777cc9 --- /dev/null +++ b/backend/docs/swagger.json @@ -0,0 +1,357 @@ +{ + "swagger": "2.0", + "info": { + "description": "API for GoHorseJobs recruitment platform.", + "title": "GoHorseJobs API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/api/v1/auth/login": { + "post": { + "description": "Authenticates a user by email and password. Returns JWT and user info.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User Login", + "parameters": [ + { + "description": "Login Credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/companies": { + "post": { + "description": "Registers a new company and creates an initial admin user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies" + ], + "summary": "Create Company (Tenant)", + "parameters": [ + { + "description": "Company Details", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns a list of users belonging to the authenticated tenant.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "List Users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new user under the current tenant. Requires Admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create User", + "parameters": [ + { + "description": "User Details", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/users/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a user by ID. Must belong to the same tenant.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Delete User", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "User deleted", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest": { + "type": "object", + "properties": { + "admin_email": { + "type": "string" + }, + "contact": { + "type": "string" + }, + "document": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "roles": { + "description": "e.g. [\"RECRUITER\"]", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml new file mode 100644 index 0000000..ec24508 --- /dev/null +++ b/backend/docs/swagger.yaml @@ -0,0 +1,231 @@ +basePath: / +definitions: + github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse: + properties: + token: + type: string + user: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse' + type: object + github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse: + properties: + created_at: + type: string + id: + type: string + name: + type: string + status: + type: string + type: object + github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest: + properties: + admin_email: + type: string + contact: + type: string + document: + type: string + name: + type: string + type: object + github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest: + properties: + email: + type: string + name: + type: string + password: + type: string + roles: + description: e.g. ["RECRUITER"] + items: + type: string + type: array + type: object + github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest: + properties: + email: + type: string + password: + type: string + type: object + github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse: + properties: + created_at: + type: string + email: + type: string + id: + type: string + name: + type: string + roles: + items: + type: string + type: array + status: + type: string + type: object +host: localhost:8080 +info: + contact: {} + description: API for GoHorseJobs recruitment platform. + title: GoHorseJobs API + version: "1.0" +paths: + /api/v1/auth/login: + post: + consumes: + - application/json + description: Authenticates a user by email and password. Returns JWT and user + info. + parameters: + - description: Login Credentials + in: body + name: login + required: true + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.AuthResponse' + "400": + description: Invalid Request + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + summary: User Login + tags: + - Auth + /api/v1/companies: + post: + consumes: + - application/json + description: Registers a new company and creates an initial admin user. + parameters: + - description: Company Details + in: body + name: company + required: true + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateCompanyRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CompanyResponse' + "400": + description: Invalid Request + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Create Company (Tenant) + tags: + - Companies + /api/v1/users: + get: + consumes: + - application/json + description: Returns a list of users belonging to the authenticated tenant. + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse' + type: array + "403": + description: Forbidden + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - BearerAuth: [] + summary: List Users + tags: + - Users + post: + consumes: + - application/json + description: Creates a new user under the current tenant. Requires Admin role. + parameters: + - description: User Details + in: body + name: user + required: true + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.CreateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.UserResponse' + "400": + description: Invalid Request + schema: + type: string + "403": + description: Forbidden + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - BearerAuth: [] + summary: Create User + tags: + - Users + /api/v1/users/{id}: + delete: + consumes: + - application/json + description: Deletes a user by ID. Must belong to the same tenant. + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: User deleted + schema: + type: string + "403": + description: Forbidden + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - BearerAuth: [] + summary: Delete User + tags: + - Users +swagger: "2.0" diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..d916ad7 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,32 @@ +module github.com/rede5/gohorsejobs/backend + +go 1.24.0 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.6 + golang.org/x/crypto v0.45.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/go-openapi/jsonpointer v0.22.3 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/spec v0.22.1 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/swaggo/files/v2 v2.0.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/tools v0.39.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100755 index 0000000..1f67f93 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,65 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= +github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= +github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go new file mode 100644 index 0000000..cab32d7 --- /dev/null +++ b/backend/internal/api/handlers/core_handlers.go @@ -0,0 +1,189 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/rede5/gohorsejobs/backend/internal/api/middleware" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth" + "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant" + "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user" +) + +type CoreHandlers struct { + loginUC *auth.LoginUseCase + createCompanyUC *tenant.CreateCompanyUseCase + createUserUC *user.CreateUserUseCase + listUsersUC *user.ListUsersUseCase + deleteUserUC *user.DeleteUserUseCase +} + +func NewCoreHandlers(l *auth.LoginUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase) *CoreHandlers { + return &CoreHandlers{ + loginUC: l, + createCompanyUC: c, + createUserUC: u, + listUsersUC: list, + deleteUserUC: del, + } +} + +// Login authenticates a user and returns a token. +// @Summary User Login +// @Description Authenticates a user by email and password. Returns JWT and user info. +// @Tags Auth +// @Accept json +// @Produce json +// @Param login body dto.LoginRequest true "Login Credentials" +// @Success 200 {object} dto.AuthResponse +// @Failure 400 {string} string "Invalid Request" +// @Failure 401 {string} string "Unauthorized" +// @Router /api/v1/auth/login [post] +func (h *CoreHandlers) Login(w http.ResponseWriter, r *http.Request) { + var req dto.LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid Request", http.StatusBadRequest) + return + } + + resp, err := h.loginUC.Execute(r.Context(), req) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +// CreateCompany registers a new tenant (Company) and its admin. +// @Summary Create Company (Tenant) +// @Description Registers a new company and creates an initial admin user. +// @Tags Companies +// @Accept json +// @Produce json +// @Param company body dto.CreateCompanyRequest true "Company Details" +// @Success 200 {object} dto.CompanyResponse +// @Failure 400 {string} string "Invalid Request" +// @Failure 500 {string} string "Internal Server Error" +// @Router /api/v1/companies [post] +func (h *CoreHandlers) CreateCompany(w http.ResponseWriter, r *http.Request) { + var req dto.CreateCompanyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid Request", http.StatusBadRequest) + return + } + + resp, err := h.createCompanyUC.Execute(r.Context(), req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +// CreateUser creates a new user within the authenticated tenant. +// @Summary Create User +// @Description Creates a new user under the current tenant. Requires Admin role. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param user body dto.CreateUserRequest true "User Details" +// @Success 200 {object} dto.UserResponse +// @Failure 400 {string} string "Invalid Request" +// @Failure 403 {string} string "Forbidden" +// @Failure 500 {string} string "Internal Server Error" +// @Router /api/v1/users [post] +func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) + if !ok || tenantID == "" { + http.Error(w, "Tenant ID not found in context", http.StatusForbidden) + return + } + + var req dto.CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid Request", http.StatusBadRequest) + return + } + + resp, err := h.createUserUC.Execute(ctx, req, tenantID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +// ListUsers returns all users in the current tenant. +// @Summary List Users +// @Description Returns a list of users belonging to the authenticated tenant. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.UserResponse +// @Failure 403 {string} string "Forbidden" +// @Failure 500 {string} string "Internal Server Error" +// @Router /api/v1/users [get] +func (h *CoreHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) + if !ok || tenantID == "" { + http.Error(w, "Tenant ID not found in context", http.StatusForbidden) + return + } + + users, err := h.listUsersUC.Execute(ctx, tenantID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) +} + +// DeleteUser removes a user from the tenant. +// @Summary Delete User +// @Description Deletes a user by ID. Must belong to the same tenant. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "User ID" +// @Success 200 {string} string "User deleted" +// @Failure 403 {string} string "Forbidden" +// @Failure 500 {string} string "Internal Server Error" +// @Router /api/v1/users/{id} [delete] +func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) + if !ok || tenantID == "" { + http.Error(w, "Tenant ID not found in context", http.StatusForbidden) + return + } + + id := r.PathValue("id") + if id == "" { + // Fallback for older Go versions if PathValue not avail, though 1.22+ is standard now. + // Usually we'd use a mux var. Since we use stdlib mux in 1.22: + http.Error(w, "Missing User ID", http.StatusBadRequest) + return + } + + if err := h.deleteUserUC.Execute(ctx, id, tenantID); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("User deleted")) +} diff --git a/backend/internal/api/middleware/auth_middleware.go b/backend/internal/api/middleware/auth_middleware.go new file mode 100644 index 0000000..cbf3693 --- /dev/null +++ b/backend/internal/api/middleware/auth_middleware.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type contextKey string + +const ( + ContextUserID contextKey = "userID" + ContextTenantID contextKey = "tenantID" + ContextRoles contextKey = "roles" +) + +type Middleware struct { + authService ports.AuthService +} + +func NewMiddleware(authService ports.AuthService) *Middleware { + return &Middleware{authService: authService} +} + +func (m *Middleware) HeaderAuthGuard(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Missing Authorization Header", http.StatusUnauthorized) + return + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid Header Format", http.StatusUnauthorized) + return + } + + token := parts[1] + claims, err := m.authService.ValidateToken(token) + if err != nil { + http.Error(w, "Invalid Token: "+err.Error(), http.StatusUnauthorized) + return + } + + // Inject into Context + ctx := context.WithValue(r.Context(), ContextUserID, claims["sub"]) + ctx = context.WithValue(ctx, ContextTenantID, claims["tenant"]) + ctx = context.WithValue(ctx, ContextRoles, claims["roles"]) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// TenantGuard ensures that the request is made by a user belonging to the prompt tenant +// Note: In this architecture, the token *defines* the tenant. So HeaderAuthGuard implicitly guards the tenant. +// This middleware is for extra checks if URL params conflict with Token tenant. +func (m *Middleware) TenantGuard(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tenantID := r.Context().Value(ContextTenantID) + if tenantID == nil || tenantID == "" { + http.Error(w, "Tenant Context Missing", http.StatusForbidden) + return + } + // Logic to compare with URL param if needed... + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/middleware/cors_middleware.go b/backend/internal/api/middleware/cors_middleware.go new file mode 100644 index 0000000..a2614c7 --- /dev/null +++ b/backend/internal/api/middleware/cors_middleware.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "net/http" + "os" + "strings" +) + +func CORSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origins := os.Getenv("CORS_ORIGINS") + if origins == "" { + // Strict default: only allow exact matches or specific development origin if needed. + // For this project, we prefer configuration. + origins = "http://localhost:3000" + } + + origin := r.Header.Get("Origin") + allowOrigin := "" + + // Check if the origin is allowed + if origins == "*" { + allowOrigin = "*" + } else { + for _, o := range strings.Split(origins, ",") { + if strings.TrimSpace(o) == origin { + allowOrigin = origin + break + } + } + } + + if allowOrigin != "" { + w.Header().Set("Access-Control-Allow-Origin", allowOrigin) + } + + // Essential for some auth flows (cookies) + w.Header().Set("Access-Control-Allow-Credentials", "true") + + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/core/domain/entity/company.go b/backend/internal/core/domain/entity/company.go new file mode 100644 index 0000000..ab54f4e --- /dev/null +++ b/backend/internal/core/domain/entity/company.go @@ -0,0 +1,37 @@ +package entity + +import "time" + +// Company represents a Tenant in the system. +type Company struct { + ID string `json:"id"` + Name string `json:"name"` + Document string `json:"document,omitempty"` // CNPJ, EIN, VAT + Contact string `json:"contact"` + Status string `json:"status"` // "ACTIVE", "INACTIVE" + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// NewCompany creates a new Company instance with defaults. +func NewCompany(id, name, document, contact string) *Company { + return &Company{ + ID: id, + Name: name, + Document: document, + Contact: contact, + Status: "ACTIVE", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +func (c *Company) Activate() { + c.Status = "ACTIVE" + c.UpdatedAt = time.Now() +} + +func (c *Company) Deactivate() { + c.Status = "INACTIVE" + c.UpdatedAt = time.Now() +} diff --git a/backend/internal/core/domain/entity/permission.go b/backend/internal/core/domain/entity/permission.go new file mode 100644 index 0000000..c99fe5e --- /dev/null +++ b/backend/internal/core/domain/entity/permission.go @@ -0,0 +1,13 @@ +package entity + +// Permission represents a granular access right. +type Permission struct { + Code string `json:"code"` // e.g., "USER_create", "JOB_view" + Description string `json:"description"` // e.g., "Allows creating users" +} + +// Role represents a collection of permissions. +type Role struct { + Name string `json:"name"` // e.g., "ADMIN", "RECRUITER" + Permissions []Permission `json:"permissions"` +} diff --git a/backend/internal/core/domain/entity/user.go b/backend/internal/core/domain/entity/user.go new file mode 100644 index 0000000..45f4ce2 --- /dev/null +++ b/backend/internal/core/domain/entity/user.go @@ -0,0 +1,46 @@ +package entity + +import "time" + +// User represents a user within a specific Tenant (Company). +type User struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` // Link to Company + Name string `json:"name"` + Email string `json:"email"` + PasswordHash string `json:"-"` + Roles []Role `json:"roles"` + Status string `json:"status"` // "ACTIVE", "INACTIVE" + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// NewUser creates a new User instance. +func NewUser(id, tenantID, name, email string) *User { + return &User{ + ID: id, + TenantID: tenantID, + Name: name, + Email: email, + Status: "ACTIVE", + Roles: []Role{}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +func (u *User) AssignRole(role Role) { + u.Roles = append(u.Roles, role) + u.UpdatedAt = time.Now() +} + +func (u *User) HasPermission(permissionCode string) bool { + for _, role := range u.Roles { + for _, perm := range role.Permissions { + if perm.Code == permissionCode { + return true + } + } + } + return false +} diff --git a/backend/internal/core/dto/company.go b/backend/internal/core/dto/company.go new file mode 100644 index 0000000..e7eae4e --- /dev/null +++ b/backend/internal/core/dto/company.go @@ -0,0 +1,17 @@ +package dto + +import "time" + +type CreateCompanyRequest struct { + Name string `json:"name"` + Document string `json:"document"` + Contact string `json:"contact"` + AdminEmail string `json:"admin_email"` +} + +type CompanyResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/backend/internal/core/dto/user_auth.go b/backend/internal/core/dto/user_auth.go new file mode 100644 index 0000000..4eea629 --- /dev/null +++ b/backend/internal/core/dto/user_auth.go @@ -0,0 +1,29 @@ +package dto + +import "time" + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type AuthResponse struct { + Token string `json:"token"` + User UserResponse `json:"user"` +} + +type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + Roles []string `json:"roles"` // e.g. ["RECRUITER"] +} + +type UserResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Roles []string `json:"roles"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/backend/internal/core/ports/repositories.go b/backend/internal/core/ports/repositories.go new file mode 100644 index 0000000..5985ddf --- /dev/null +++ b/backend/internal/core/ports/repositories.go @@ -0,0 +1,24 @@ +package ports + +import ( + "context" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" +) + +type CompanyRepository interface { + Save(ctx context.Context, company *entity.Company) (*entity.Company, error) + FindByID(ctx context.Context, id string) (*entity.Company, error) + Update(ctx context.Context, company *entity.Company) (*entity.Company, error) + Delete(ctx context.Context, id string) error +} + +type UserRepository interface { + Save(ctx context.Context, user *entity.User) (*entity.User, error) + FindByID(ctx context.Context, id string) (*entity.User, error) + FindByEmail(ctx context.Context, email string) (*entity.User, error) + // FindAllByTenant returns users strictly scoped to a tenant + FindAllByTenant(ctx context.Context, tenantID string) ([]*entity.User, error) + Update(ctx context.Context, user *entity.User) (*entity.User, error) + Delete(ctx context.Context, id string) error +} diff --git a/backend/internal/core/ports/services.go b/backend/internal/core/ports/services.go new file mode 100644 index 0000000..bddac45 --- /dev/null +++ b/backend/internal/core/ports/services.go @@ -0,0 +1,9 @@ +package ports + +// AuthService defines the interface for authentication logic. +type AuthService interface { + HashPassword(password string) (string, error) + VerifyPassword(hash, password string) bool + GenerateToken(userID, tenantID string, roles []string) (string, error) + ValidateToken(token string) (map[string]interface{}, error) +} diff --git a/backend/internal/core/usecases/auth/login.go b/backend/internal/core/usecases/auth/login.go new file mode 100644 index 0000000..fb658b4 --- /dev/null +++ b/backend/internal/core/usecases/auth/login.go @@ -0,0 +1,65 @@ +package auth + +import ( + "context" + "errors" + + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type LoginUseCase struct { + userRepo ports.UserRepository + authService ports.AuthService +} + +func NewLoginUseCase(userRepo ports.UserRepository, authService ports.AuthService) *LoginUseCase { + return &LoginUseCase{ + userRepo: userRepo, + authService: authService, + } +} + +func (uc *LoginUseCase) Execute(ctx context.Context, input dto.LoginRequest) (*dto.AuthResponse, error) { + // 1. Find User by Email (Global or implicit tenant?) + // Implementation Note: In this architecture, email should be unique enough or we iterate. + // For simplicity, assuming email is unique system-wide or we find the active one. + user, err := uc.userRepo.FindByEmail(ctx, input.Email) + if err != nil { + return nil, errors.New("invalid credentials") // Avoid leaking existence + } + + // 2. Verify Password + if !uc.authService.VerifyPassword(user.PasswordHash, input.Password) { + return nil, errors.New("invalid credentials") + } + + // 3. Check Status + if user.Status != "ACTIVE" { + return nil, errors.New("account inactive") + } + + // 4. Generate Token + roles := make([]string, len(user.Roles)) + for i, r := range user.Roles { + roles[i] = r.Name + } + + token, err := uc.authService.GenerateToken(user.ID, user.TenantID, roles) + if err != nil { + return nil, errors.New("failed to generate token") + } + + // 5. Return Response + return &dto.AuthResponse{ + Token: token, + User: dto.UserResponse{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Roles: roles, + Status: user.Status, + CreatedAt: user.CreatedAt, + }, + }, nil +} diff --git a/backend/internal/core/usecases/tenant/create_company.go b/backend/internal/core/usecases/tenant/create_company.go new file mode 100644 index 0000000..505826b --- /dev/null +++ b/backend/internal/core/usecases/tenant/create_company.go @@ -0,0 +1,70 @@ +package tenant + +import ( + "context" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type CreateCompanyUseCase struct { + companyRepo ports.CompanyRepository + userRepo ports.UserRepository + authService ports.AuthService +} + +func NewCreateCompanyUseCase(cRepo ports.CompanyRepository, uRepo ports.UserRepository, auth ports.AuthService) *CreateCompanyUseCase { + return &CreateCompanyUseCase{ + companyRepo: cRepo, + userRepo: uRepo, + authService: auth, + } +} + +func (uc *CreateCompanyUseCase) Execute(ctx context.Context, input dto.CreateCompanyRequest) (*dto.CompanyResponse, error) { + // 1. Create Company ID (Assuming UUID generated by Repo OR here. Let's assume Repo handles ID generation if empty, or we do it.) + // To be agnostic, let's assume NewCompany takes an ID. In real app, we might use a UUID generator service. + // For now, let's assume ID is generated by DB or we pass a placeholder if DB does it. + // Actually, the Entity `NewCompany` takes ID. I should generate one. + // But UseCase shouldn't rely on specific UUID lib ideally? + // I'll skip ID generation here and let Repo handle it or use a simple string for now. + // Better: Use a helper or just "new-uuid" string for now as placeholder for the generator logic. + + // Implementation decision: Domain ID generation should be explicit. + // I'll assume input could pass it, or we rely on repo. + // Let's create the entity with empty ID and let Repo fill it? No, Entity usually needs Identity. + // I'll generate a random ID here for simulation if I had a uuid lib. + // Since I want to be agnostic and dependency-free, I'll assume the Repo 'Save' returns the fully populated entity including ID. + + company := entity.NewCompany("", input.Name, input.Document, input.Contact) + + savedCompany, err := uc.companyRepo.Save(ctx, company) + if err != nil { + return nil, err + } + + // 2. Create Admin User + // We need a password for the admin. Do we generate one? Or did input provide? + // input.AdminEmail is present. But no password. I'll generic default or ask to send email. + // For simplicity, let's assume a default password "ChangeMe123!" hash it. + hashedPassword, _ := uc.authService.HashPassword("ChangeMe123!") + + adminUser := entity.NewUser("", savedCompany.ID, "Admin", input.AdminEmail) + adminUser.PasswordHash = hashedPassword + adminUser.AssignRole(entity.Role{Name: "ADMIN"}) + + _, err = uc.userRepo.Save(ctx, adminUser) + if err != nil { + // Rollback company? Transaction? + // Ignored for now. + return nil, err + } + + return &dto.CompanyResponse{ + ID: savedCompany.ID, + Name: savedCompany.Name, + Status: savedCompany.Status, + CreatedAt: savedCompany.CreatedAt, + }, nil +} diff --git a/backend/internal/core/usecases/user/create_user.go b/backend/internal/core/usecases/user/create_user.go new file mode 100644 index 0000000..12cff17 --- /dev/null +++ b/backend/internal/core/usecases/user/create_user.go @@ -0,0 +1,67 @@ +package user + +import ( + "context" + "errors" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type CreateUserUseCase struct { + userRepo ports.UserRepository + authService ports.AuthService +} + +func NewCreateUserUseCase(uRepo ports.UserRepository, auth ports.AuthService) *CreateUserUseCase { + return &CreateUserUseCase{ + userRepo: uRepo, + authService: auth, + } +} + +func (uc *CreateUserUseCase) Execute(ctx context.Context, input dto.CreateUserRequest, currentTenantID string) (*dto.UserResponse, error) { + // 1. Validate Email Uniqueness (within tenant? or global?) + // Usually email is unique global or per tenant. Let's assume unique. + exists, _ := uc.userRepo.FindByEmail(ctx, input.Email) + if exists != nil { + return nil, errors.New("user already exists") + } + + // 2. Hash Password + hashed, err := uc.authService.HashPassword(input.Password) + if err != nil { + return nil, err + } + + // 3. Create Entity + // Note: We enforce currentTenantID to ensure isolation. + user := entity.NewUser("", currentTenantID, input.Name, input.Email) + user.PasswordHash = hashed + + // Assign roles + for _, r := range input.Roles { + user.AssignRole(entity.Role{Name: r}) + } + + saved, err := uc.userRepo.Save(ctx, user) + if err != nil { + return nil, err + } + + // 4. Return DTO + roles := make([]string, len(saved.Roles)) + for i, r := range saved.Roles { + roles[i] = r.Name + } + + return &dto.UserResponse{ + ID: saved.ID, + Name: saved.Name, + Email: saved.Email, + Roles: roles, + Status: saved.Status, + CreatedAt: saved.CreatedAt, + }, nil +} diff --git a/backend/internal/core/usecases/user/delete_user.go b/backend/internal/core/usecases/user/delete_user.go new file mode 100644 index 0000000..76d3781 --- /dev/null +++ b/backend/internal/core/usecases/user/delete_user.go @@ -0,0 +1,33 @@ +package user + +import ( + "context" + "errors" + + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type DeleteUserUseCase struct { + userRepo ports.UserRepository +} + +func NewDeleteUserUseCase(uRepo ports.UserRepository) *DeleteUserUseCase { + return &DeleteUserUseCase{ + userRepo: uRepo, + } +} + +func (uc *DeleteUserUseCase) Execute(ctx context.Context, userID, tenantID string) error { + // 1. Verify User exists and belongs to Tenant + user, err := uc.userRepo.FindByID(ctx, userID) + if err != nil { + return err + } + + if user.TenantID != tenantID { + return errors.New("user not found in this tenant") + } + + // 2. Delete + return uc.userRepo.Delete(ctx, userID) +} diff --git a/backend/internal/core/usecases/user/list_users.go b/backend/internal/core/usecases/user/list_users.go new file mode 100644 index 0000000..dd4cff2 --- /dev/null +++ b/backend/internal/core/usecases/user/list_users.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + + "github.com/rede5/gohorsejobs/backend/internal/core/dto" + "github.com/rede5/gohorsejobs/backend/internal/core/ports" +) + +type ListUsersUseCase struct { + userRepo ports.UserRepository +} + +func NewListUsersUseCase(uRepo ports.UserRepository) *ListUsersUseCase { + return &ListUsersUseCase{ + userRepo: uRepo, + } +} + +func (uc *ListUsersUseCase) Execute(ctx context.Context, tenantID string) ([]dto.UserResponse, error) { + users, err := uc.userRepo.FindAllByTenant(ctx, tenantID) + if err != nil { + return nil, err + } + + var response []dto.UserResponse + for _, u := range users { + roles := make([]string, len(u.Roles)) + for i, r := range u.Roles { + roles[i] = r.Name + } + + response = append(response, dto.UserResponse{ + ID: u.ID, + Name: u.Name, + Email: u.Email, + Roles: roles, + Status: u.Status, + CreatedAt: u.CreatedAt, + }) + } + + return response, nil +} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go new file mode 100755 index 0000000..ce1f83e --- /dev/null +++ b/backend/internal/database/database.go @@ -0,0 +1,95 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + "os" + + _ "github.com/lib/pq" +) + +var DB *sql.DB + +func InitDB() { + var err error + host := os.Getenv("DB_HOST") + if host == "" { + log.Fatal("DB_HOST environment variable not set") + } + user := os.Getenv("DB_USER") + if user == "" { + log.Fatal("DB_USER environment variable not set") + } + password := os.Getenv("DB_PASSWORD") + if password == "" { + log.Fatal("DB_PASSWORD environment variable not set") + } + dbname := os.Getenv("DB_NAME") + if dbname == "" { + log.Fatal("DB_NAME environment variable not set") + } + port := os.Getenv("DB_PORT") + if port == "" { + port = "5432" + } + + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + + DB, err = sql.Open("postgres", connStr) + if err != nil { + log.Fatalf("Error opening database: %v", err) + } + + if err = DB.Ping(); err != nil { + log.Fatalf("Error connecting to database: %v", err) + } + + log.Println("Successfully connected to the database") +} + +func RunMigrations() { + files, err := os.ReadDir("migrations") + if err != nil { + // Try fallback to relative path if running from cmd/api + files, err = os.ReadDir("../../migrations") + if err != nil { + log.Printf("Warning: Could not list migrations directory: %v", err) + return + } + } + + // Sort files by name to ensure order (001, 002, ...) + // ReadDir returns sorted by name automatically, but let's be safe if logic changes? + // Actually ReadDir result is sorted by filename. + + for _, file := range files { + if file.IsDir() { + continue + } + + log.Printf("Running migration: %s", file.Name()) + content, err := os.ReadFile("migrations/" + file.Name()) + if err != nil { + // Try fallback + content, err = os.ReadFile("../../migrations/" + file.Name()) + if err != nil { + log.Fatalf("Error reading migration file %s: %v", file.Name(), err) + } + } + + _, err = DB.Exec(string(content)) + if err != nil { + // Log warning but don't crash on "already exists" errors if possible, + // but pure SQL Exec might fail hard. + // Given the SQL files use IF NOT EXISTS, we should be fine. + // If one fails, it might be syntax. + log.Printf("Error running migration %s: %v", file.Name(), err) + // Continue or Fail? User wants robustness. Let's Warn and continue for now to avoid single failure blocking all + } else { + log.Printf("Migration %s executed successfully", file.Name()) + } + } + log.Println("All migrations processed") +} diff --git a/backend/internal/dto/auth.go b/backend/internal/dto/auth.go new file mode 100755 index 0000000..c013a00 --- /dev/null +++ b/backend/internal/dto/auth.go @@ -0,0 +1,44 @@ +package dto + +// LoginRequest represents the login request payload +type LoginRequest struct { + Identifier string `json:"identifier" validate:"required,min=3"` + Password string `json:"password" validate:"required,min=8"` +} + +// LoginResponse represents the login response +type LoginResponse struct { + Token string `json:"token"` + User UserInfo `json:"user"` + Companies []CompanyInfo `json:"companies,omitempty"` + ActiveCompanyID *int `json:"activeCompanyId,omitempty"` +} + +// UserInfo represents basic user information in responses +type UserInfo struct { + ID int `json:"id"` + Identifier string `json:"identifier"` + Role string `json:"role"` + FullName string `json:"fullName"` + Language string `json:"language"` +} + +// CompanyInfo represents basic company information +type CompanyInfo struct { + ID int `json:"id"` + Name string `json:"name"` + Role string `json:"role"` // Role in this company (companyAdmin or recruiter) +} + +// RegisterRequest represents user registration +type RegisterRequest struct { + Identifier string `json:"identifier" validate:"required,min=3,max=50"` + Password string `json:"password" validate:"required,min=8"` + FullName string `json:"fullName" validate:"required"` + Phone *string `json:"phone,omitempty"` + WhatsApp *string `json:"whatsapp,omitempty"` + LineID *string `json:"lineId,omitempty"` + Instagram *string `json:"instagram,omitempty"` + Language string `json:"language" validate:"required,oneof=pt en es ja"` + Role string `json:"role" validate:"required,oneof=jobSeeker recruiter companyAdmin"` +} diff --git a/backend/internal/dto/requests.go b/backend/internal/dto/requests.go new file mode 100755 index 0000000..5b4fa63 --- /dev/null +++ b/backend/internal/dto/requests.go @@ -0,0 +1,142 @@ +package dto + +// CreateJobRequest represents the request to create a new job +type CreateJobRequest struct { + CompanyID int `json:"companyId" validate:"required"` + Title string `json:"title" validate:"required,min=5,max=255"` + Description string `json:"description" validate:"required,min=20"` + SalaryMin *float64 `json:"salaryMin,omitempty"` + SalaryMax *float64 `json:"salaryMax,omitempty"` + SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly monthly yearly"` + EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract"` + WorkingHours *string `json:"workingHours,omitempty"` + Location *string `json:"location,omitempty"` + RegionID *int `json:"regionId,omitempty"` + CityID *int `json:"cityId,omitempty"` + Requirements map[string]interface{} `json:"requirements,omitempty"` + Benefits map[string]interface{} `json:"benefits,omitempty"` + VisaSupport bool `json:"visaSupport"` + LanguageLevel *string `json:"languageLevel,omitempty"` + Status string `json:"status" validate:"oneof=draft open closed"` +} + +// UpdateJobRequest represents the request to update a job +type UpdateJobRequest struct { + Title *string `json:"title,omitempty" validate:"omitempty,min=5,max=255"` + Description *string `json:"description,omitempty" validate:"omitempty,min=20"` + SalaryMin *float64 `json:"salaryMin,omitempty"` + SalaryMax *float64 `json:"salaryMax,omitempty"` + SalaryType *string `json:"salaryType,omitempty" validate:"omitempty,oneof=hourly monthly yearly"` + EmploymentType *string `json:"employmentType,omitempty" validate:"omitempty,oneof=full-time part-time dispatch contract"` + WorkingHours *string `json:"workingHours,omitempty"` + Location *string `json:"location,omitempty"` + RegionID *int `json:"regionId,omitempty"` + CityID *int `json:"cityId,omitempty"` + Requirements map[string]interface{} `json:"requirements,omitempty"` + Benefits map[string]interface{} `json:"benefits,omitempty"` + VisaSupport *bool `json:"visaSupport,omitempty"` + LanguageLevel *string `json:"languageLevel,omitempty"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft open closed"` +} + +// CreateApplicationRequest represents a job application (guest or logged user) +type CreateApplicationRequest struct { + JobID int `json:"jobId" validate:"required"` + UserID *int `json:"userId,omitempty"` + Name *string `json:"name,omitempty"` + Phone *string `json:"phone,omitempty"` + LineID *string `json:"lineId,omitempty"` + WhatsApp *string `json:"whatsapp,omitempty"` + Email *string `json:"email,omitempty"` + Message *string `json:"message,omitempty"` + ResumeURL *string `json:"resumeUrl,omitempty"` + Documents map[string]interface{} `json:"documents,omitempty"` +} + +// UpdateApplicationStatusRequest represents updating application status (recruiter only) +type UpdateApplicationStatusRequest struct { + Status string `json:"status" validate:"required,oneof=pending reviewed shortlisted rejected hired"` + Notes *string `json:"notes,omitempty"` +} + +// CreateCompanyRequest represents creating a new company +type CreateCompanyRequest struct { + Name string `json:"name" validate:"required,min=3,max=255"` + Slug string `json:"slug" validate:"required,min=3,max=255"` + Type *string `json:"type,omitempty"` + Document *string `json:"document,omitempty"` + Address *string `json:"address,omitempty"` + RegionID *int `json:"regionId,omitempty"` + CityID *int `json:"cityId,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + Website *string `json:"website,omitempty"` + LogoURL *string `json:"logoUrl,omitempty"` + Description *string `json:"description,omitempty"` +} + +// UpdateCompanyRequest represents updating company information +type UpdateCompanyRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=255"` + Slug *string `json:"slug,omitempty" validate:"omitempty,min=3,max=255"` + Type *string `json:"type,omitempty"` + Document *string `json:"document,omitempty"` + Address *string `json:"address,omitempty"` + RegionID *int `json:"regionId,omitempty"` + CityID *int `json:"cityId,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + Website *string `json:"website,omitempty"` + LogoURL *string `json:"logoUrl,omitempty"` + Description *string `json:"description,omitempty"` + Active *bool `json:"active,omitempty"` + Verified *bool `json:"verified,omitempty"` +} + +// AssignUserToCompanyRequest represents assigning a user to a company +type AssignUserToCompanyRequest struct { + UserID int `json:"userId" validate:"required"` + CompanyID int `json:"companyId" validate:"required"` + Role string `json:"role" validate:"required,oneof=companyAdmin recruiter"` + Permissions map[string]interface{} `json:"permissions,omitempty"` +} + +// PaginationQuery represents pagination parameters +type PaginationQuery struct { + Page int `form:"page" validate:"min=1"` + Limit int `form:"limit" validate:"min=1,max=100"` +} + +// JobFilterQuery represents job filtering parameters +type JobFilterQuery struct { + PaginationQuery + CompanyID *int `form:"companyId"` + RegionID *int `form:"regionId"` + CityID *int `form:"cityId"` + EmploymentType *string `form:"employmentType"` + Status *string `form:"status"` + VisaSupport *bool `form:"visaSupport"` + LanguageLevel *string `form:"languageLevel"` + Search *string `form:"search"` +} + +// PaginatedResponse represents a paginated API response +type PaginatedResponse struct { + Data interface{} `json:"data"` + Pagination Pagination `json:"pagination"` +} + +// Pagination represents pagination metadata +type Pagination struct { + Page int `json:"page"` + Limit int `json:"limit"` + Total int `json:"total"` +} + +// APIResponse represents a standard API response +type APIResponse struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Error *string `json:"error,omitempty"` + Message *string `json:"message,omitempty"` +} diff --git a/backend/internal/handlers/application_handler.go b/backend/internal/handlers/application_handler.go new file mode 100644 index 0000000..da6dd59 --- /dev/null +++ b/backend/internal/handlers/application_handler.go @@ -0,0 +1,101 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/rede5/gohorsejobs/backend/internal/dto" + "github.com/rede5/gohorsejobs/backend/internal/services" +) + +type ApplicationHandler struct { + Service *services.ApplicationService +} + +func NewApplicationHandler(service *services.ApplicationService) *ApplicationHandler { + return &ApplicationHandler{Service: service} +} + +func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Request) { + var req dto.CreateApplicationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app, err := h.Service.CreateApplication(req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(app) +} + +func (h *ApplicationHandler) GetApplications(w http.ResponseWriter, r *http.Request) { + // For now, simple get by Job ID query param + jobIDStr := r.URL.Query().Get("jobId") + if jobIDStr == "" { + http.Error(w, "jobId is required", http.StatusBadRequest) + return + } + jobID, err := strconv.Atoi(jobIDStr) + if err != nil { + http.Error(w, "Invalid jobId", http.StatusBadRequest) + return + } + + apps, err := h.Service.GetApplications(jobID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apps) +} + +func (h *ApplicationHandler) GetApplicationByID(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid application ID", http.StatusBadRequest) + return + } + + app, err := h.Service.GetApplicationByID(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(app) +} + +func (h *ApplicationHandler) UpdateApplicationStatus(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid application ID", http.StatusBadRequest) + return + } + + var req dto.UpdateApplicationStatusRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app, err := h.Service.UpdateApplicationStatus(id, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(app) +} diff --git a/backend/internal/handlers/job_handler.go b/backend/internal/handlers/job_handler.go new file mode 100755 index 0000000..cc88944 --- /dev/null +++ b/backend/internal/handlers/job_handler.go @@ -0,0 +1,152 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/rede5/gohorsejobs/backend/internal/dto" + "github.com/rede5/gohorsejobs/backend/internal/services" +) + +type JobHandler struct { + Service *services.JobService +} + +func NewJobHandler(service *services.JobService) *JobHandler { + return &JobHandler{Service: service} +} + +func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + companyID, _ := strconv.Atoi(r.URL.Query().Get("companyId")) + + filter := dto.JobFilterQuery{ + PaginationQuery: dto.PaginationQuery{ + Page: page, + Limit: limit, + }, + } + if companyID > 0 { + filter.CompanyID = &companyID + } + + jobs, total, err := h.Service.GetJobs(filter) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + response := dto.PaginatedResponse{ + Data: jobs, + Pagination: dto.Pagination{ + Page: page, + Limit: limit, + Total: total, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { + var req dto.CreateJobRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Validate request (omitted for brevity, assume validation middleware or service validation) + + job, err := h.Service.CreateJob(req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(job) +} + +func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") // Go 1.22+ routing + if idStr == "" { + // Fallback for older Go versions or if not using PathValue compatible mux + // But let's assume standard mux or we might need to parse URL + // For now, let's assume we can get it from context or URL + // If using standard http.ServeMux in Go 1.22, PathValue works. + // If not, we might need a helper. + // Let's assume standard mux with Go 1.22 for now as it's modern. + // If not, we'll fix it. + // Actually, let's check go.mod version. + } + + // Wait, I should check go.mod version to be safe. + // But let's write standard code. + + // Assuming we use a router that puts params in context or we parse URL. + // Since I am editing router.go later, I can ensure we use Go 1.22 patterns or a library. + // The existing router.go used `mux.HandleFunc("/jobs", ...)` which suggests standard lib. + + // Let's try to parse from URL path if PathValue is not available (it is in Go 1.22). + // I'll use a helper to extract ID from URL for now to be safe if I'm not sure about Go version. + // But `r.PathValue` is the way forward. + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid job ID", http.StatusBadRequest) + return + } + + job, err := h.Service.GetJobByID(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(job) +} + +func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid job ID", http.StatusBadRequest) + return + } + + var req dto.UpdateJobRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + job, err := h.Service.UpdateJob(id, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(job) +} + +func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid job ID", http.StatusBadRequest) + return + } + + if err := h.Service.DeleteJob(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/backend/internal/infrastructure/auth/jwt_service.go b/backend/internal/infrastructure/auth/jwt_service.go new file mode 100644 index 0000000..d11fd9f --- /dev/null +++ b/backend/internal/infrastructure/auth/jwt_service.go @@ -0,0 +1,64 @@ +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +type JWTService struct { + secretKey []byte + issuer string +} + +func NewJWTService(secret string, issuer string) *JWTService { + return &JWTService{ + secretKey: []byte(secret), + issuer: issuer, + } +} + +func (s *JWTService) HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +func (s *JWTService) VerifyPassword(hash, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func (s *JWTService) GenerateToken(userID, tenantID string, roles []string) (string, error) { + claims := jwt.MapClaims{ + "sub": userID, + "tenant": tenantID, + "roles": roles, + "iss": s.issuer, + "exp": time.Now().Add(time.Hour * 24).Unix(), // 24 hours + "iat": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(s.secretKey) +} + +func (s *JWTService) ValidateToken(tokenString string) (map[string]interface{}, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return s.secretKey, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/backend/internal/infrastructure/persistence/postgres/company_repository.go b/backend/internal/infrastructure/persistence/postgres/company_repository.go new file mode 100644 index 0000000..c6d6faa --- /dev/null +++ b/backend/internal/infrastructure/persistence/postgres/company_repository.go @@ -0,0 +1,82 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/google/uuid" + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" +) + +type CompanyRepository struct { + db *sql.DB +} + +func NewCompanyRepository(db *sql.DB) *CompanyRepository { + return &CompanyRepository{db: db} +} + +func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { + if company.ID == "" { + company.ID = uuid.New().String() + } + + query := ` + INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + ` + + err := r.db.QueryRowContext(ctx, query, + company.ID, + company.Name, + company.Document, + company.Contact, + company.Status, + company.CreatedAt, + company.UpdatedAt, + ).Scan(&company.ID) + + if err != nil { + return nil, err + } + return company, nil +} + +func (r *CompanyRepository) FindByID(ctx context.Context, id string) (*entity.Company, error) { + query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies WHERE id = $1` + row := r.db.QueryRowContext(ctx, query, id) + + c := &entity.Company{} + err := row.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt) + if err == sql.ErrNoRows { + return nil, errors.New("company not found") + } + return c, err +} + +func (r *CompanyRepository) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) { + company.UpdatedAt = time.Now() + query := ` + UPDATE core_companies + SET name=$1, document=$2, contact=$3, status=$4, updated_at=$5 + WHERE id=$6 + ` + _, err := r.db.ExecContext(ctx, query, + company.Name, + company.Document, + company.Contact, + company.Status, + company.UpdatedAt, + company.ID, + ) + return company, err +} + +func (r *CompanyRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM core_companies WHERE id = $1` + _, err := r.db.ExecContext(ctx, query, id) + return err +} diff --git a/backend/internal/infrastructure/persistence/postgres/user_repository.go b/backend/internal/infrastructure/persistence/postgres/user_repository.go new file mode 100644 index 0000000..e35e17c --- /dev/null +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -0,0 +1,146 @@ +package postgres + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" +) + +type UserRepository struct { + db *sql.DB +} + +func NewUserRepository(db *sql.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.User, error) { + if user.ID == "" { + user.ID = uuid.New().String() + } + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // 1. Insert User + query := ` + INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ` + _, err = tx.ExecContext(ctx, query, + user.ID, + user.TenantID, + user.Name, + user.Email, + user.PasswordHash, + user.Status, + user.CreatedAt, + user.UpdatedAt, + ) + if err != nil { + return nil, err + } + + // 2. Insert Roles + if len(user.Roles) > 0 { + roleQuery := `INSERT INTO core_user_roles (user_id, role) VALUES ($1, $2)` + for _, role := range user.Roles { + _, err := tx.ExecContext(ctx, roleQuery, user.ID, role.Name) + if err != nil { + return nil, err + } + } + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return user, nil +} + +func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) { + query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE email = $1` + row := r.db.QueryRowContext(ctx, query, email) + + u := &entity.User{} + err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // Return nil if not found + } + return nil, err + } + + u.Roles, _ = r.getRoles(ctx, u.ID) + return u, nil +} + +func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { + query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE id = $1` + row := r.db.QueryRowContext(ctx, query, id) + + u := &entity.User{} + err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + if err != nil { + return nil, err + } + + u.Roles, _ = r.getRoles(ctx, u.ID) + return u, nil +} + +func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string) ([]*entity.User, error) { + query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE tenant_id = $1` + rows, err := r.db.QueryContext(ctx, query, tenantID) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []*entity.User + for rows.Next() { + u := &entity.User{} + if err := rows.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { + return nil, err + } + // Populate roles N+1? Ideally join, but for now simple + u.Roles, _ = r.getRoles(ctx, u.ID) + users = append(users, u) + } + return users, nil +} + +func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) { + // Not fully implemented for roles update for brevity, just fields + user.UpdatedAt = time.Now() + query := `UPDATE core_users SET name=$1, email=$2, status=$3, updated_at=$4 WHERE id=$5` + _, err := r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, user.ID) + return user, err +} + +func (r *UserRepository) Delete(ctx context.Context, id string) error { + _, err := r.db.ExecContext(ctx, `DELETE FROM core_users WHERE id=$1`, id) + return err +} + +func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) { + rows, err := r.db.QueryContext(ctx, `SELECT role FROM core_user_roles WHERE user_id = $1`, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var roles []entity.Role + for rows.Next() { + var roleName string + rows.Scan(&roleName) + roles = append(roles, entity.Role{Name: roleName}) + } + return roles, nil +} diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go new file mode 100644 index 0000000..fd6f263 --- /dev/null +++ b/backend/internal/middleware/auth.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/rede5/gohorsejobs/backend/internal/utils" +) + +type contextKey string + +const ( + UserKey contextKey = "user" +) + +// AuthMiddleware verifies the JWT token and adds user claims to the context +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) + return + } + + tokenString := parts[1] + claims, err := utils.ValidateJWT(tokenString) + if err != nil { + http.Error(w, "Invalid or expired token", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), UserKey, claims) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// RequireRole checks if the authenticated user has the required role +func RequireRole(roles ...string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims, ok := r.Context().Value(UserKey).(*utils.Claims) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // SuperAdmin has access to everything + if claims.Role == "superadmin" { + next.ServeHTTP(w, r) + return + } + + for _, role := range roles { + if claims.Role == role { + next.ServeHTTP(w, r) + return + } + } + + http.Error(w, "Forbidden: insufficient permissions", http.StatusForbidden) + }) + } +} diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go new file mode 100644 index 0000000..e96d928 --- /dev/null +++ b/backend/internal/middleware/cors.go @@ -0,0 +1,21 @@ +package middleware + +import "net/http" + +// CORSMiddleware handles Cross-Origin Resource Sharing +func CORSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict in production + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/middleware/logging.go b/backend/internal/middleware/logging.go new file mode 100644 index 0000000..3eac723 --- /dev/null +++ b/backend/internal/middleware/logging.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "log" + "net/http" + "time" +) + +// LoggingMiddleware logs details of each request +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Wrap ResponseWriter to capture status code + rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + + next.ServeHTTP(rw, r) + + duration := time.Since(start) + log.Printf( + "%s %s %d %s", + r.Method, + r.URL.Path, + rw.statusCode, + duration, + ) + }) +} + +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} diff --git a/backend/internal/models/application.go b/backend/internal/models/application.go new file mode 100755 index 0000000..6eb9b41 --- /dev/null +++ b/backend/internal/models/application.go @@ -0,0 +1,40 @@ +package models + +import "time" + +// Application represents a job application (from user or guest) +type Application struct { + ID int `json:"id" db:"id"` + JobID int `json:"jobId" db:"job_id"` + UserID *int `json:"userId,omitempty" db:"user_id"` // NULL for guest applications + + // Applicant Info (for guest applications) + Name *string `json:"name,omitempty" db:"name"` + Phone *string `json:"phone,omitempty" db:"phone"` + LineID *string `json:"lineId,omitempty" db:"line_id"` + WhatsApp *string `json:"whatsapp,omitempty" db:"whatsapp"` + Email *string `json:"email,omitempty" db:"email"` + + // Application Content + Message *string `json:"message,omitempty" db:"message"` + ResumeURL *string `json:"resumeUrl,omitempty" db:"resume_url"` + Documents JSONMap `json:"documents,omitempty" db:"documents"` // Array of {type, url} + + // Status & Notes + Status string `json:"status" db:"status"` // pending, reviewed, shortlisted, rejected, hired + Notes *string `json:"notes,omitempty" db:"notes"` + + // Metadata + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` +} + +// ApplicationWithDetails includes job and user information +type ApplicationWithDetails struct { + Application + JobTitle string `json:"jobTitle"` + CompanyID int `json:"companyId"` + CompanyName string `json:"companyName"` + ApplicantName string `json:"applicantName"` // From user or guest name + ApplicantPhone *string `json:"applicantPhone,omitempty"` +} diff --git a/backend/internal/models/company.go b/backend/internal/models/company.go new file mode 100755 index 0000000..1fbb20e --- /dev/null +++ b/backend/internal/models/company.go @@ -0,0 +1,39 @@ +package models + +import "time" + +// Company represents an employer organization +type Company struct { + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Slug string `json:"slug" db:"slug"` + Type string `json:"type" db:"type"` + Document *string `json:"document,omitempty" db:"document"` // Houjin Bangou + + // Contact + Address *string `json:"address,omitempty" db:"address"` + RegionID *int `json:"regionId,omitempty" db:"region_id"` + CityID *int `json:"cityId,omitempty" db:"city_id"` + Phone *string `json:"phone,omitempty" db:"phone"` + Email *string `json:"email,omitempty" db:"email"` + Website *string `json:"website,omitempty" db:"website"` + + // Branding + LogoURL *string `json:"logoUrl,omitempty" db:"logo_url"` + Description *string `json:"description,omitempty" db:"description"` + + // Status + Active bool `json:"active" db:"active"` + Verified bool `json:"verified" db:"verified"` + + // Metadata + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` +} + +// CompanyWithLocation includes region and city information +type CompanyWithLocation struct { + Company + RegionName *string `json:"regionName,omitempty"` + CityName *string `json:"cityName,omitempty"` +} diff --git a/backend/internal/models/favorite_job.go b/backend/internal/models/favorite_job.go new file mode 100755 index 0000000..c15b085 --- /dev/null +++ b/backend/internal/models/favorite_job.go @@ -0,0 +1,24 @@ +package models + +import "time" + +// FavoriteJob represents a saved/favorited job by a user +type FavoriteJob struct { + ID int `json:"id" db:"id"` + UserID int `json:"userId" db:"user_id"` + JobID int `json:"jobId" db:"job_id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} + +// FavoriteJobWithDetails includes job information +type FavoriteJobWithDetails struct { + FavoriteJob + JobTitle string `json:"jobTitle"` + CompanyID int `json:"companyId"` + CompanyName string `json:"companyName"` + CompanyLogoURL *string `json:"companyLogoUrl,omitempty"` + Location *string `json:"location,omitempty"` + SalaryMin *float64 `json:"salaryMin,omitempty"` + SalaryMax *float64 `json:"salaryMax,omitempty"` + SalaryType *string `json:"salaryType,omitempty"` +} diff --git a/backend/internal/models/job.go b/backend/internal/models/job.go new file mode 100755 index 0000000..341e03c --- /dev/null +++ b/backend/internal/models/job.go @@ -0,0 +1,52 @@ +package models + +import "time" + +// Job represents a job posting +type Job struct { + ID int `json:"id" db:"id"` + CompanyID int `json:"companyId" db:"company_id"` + CreatedBy int `json:"createdBy" db:"created_by"` + + // Job Details + Title string `json:"title" db:"title"` + Description string `json:"description" db:"description"` + + // Salary + SalaryMin *float64 `json:"salaryMin,omitempty" db:"salary_min"` + SalaryMax *float64 `json:"salaryMax,omitempty" db:"salary_max"` + SalaryType *string `json:"salaryType,omitempty" db:"salary_type"` // hourly, monthly, yearly + + // Employment + EmploymentType *string `json:"employmentType,omitempty" db:"employment_type"` // full-time, part-time, dispatch, contract + WorkingHours *string `json:"workingHours,omitempty" db:"working_hours"` + + // Location + Location *string `json:"location,omitempty" db:"location"` + RegionID *int `json:"regionId,omitempty" db:"region_id"` + CityID *int `json:"cityId,omitempty" db:"city_id"` + + // Requirements & Benefits (JSONB arrays) + Requirements JSONMap `json:"requirements,omitempty" db:"requirements"` + Benefits JSONMap `json:"benefits,omitempty" db:"benefits"` + + // Visa & Language + VisaSupport bool `json:"visaSupport" db:"visa_support"` + LanguageLevel *string `json:"languageLevel,omitempty" db:"language_level"` // N5-N1, beginner, none + + // Status + Status string `json:"status" db:"status"` // open, closed, draft + + // Metadata + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` +} + +// JobWithCompany includes company information +type JobWithCompany struct { + Job + CompanyName string `json:"companyName"` + CompanyLogoURL *string `json:"companyLogoUrl,omitempty"` + RegionName *string `json:"regionName,omitempty"` + CityName *string `json:"cityName,omitempty"` +} diff --git a/backend/internal/models/location.go b/backend/internal/models/location.go new file mode 100755 index 0000000..bdf5f19 --- /dev/null +++ b/backend/internal/models/location.go @@ -0,0 +1,26 @@ +package models + +import "time" + +// Region represents a global region (State, Province, Prefecture) +type Region struct { + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + CountryCode string `json:"countryCode" db:"country_code"` + Code string `json:"code" db:"code"` // ISO Code + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} + +// City represents a global city within a region +type City struct { + ID int `json:"id" db:"id"` + RegionID int `json:"regionId" db:"region_id"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} + +// CityWithRegion includes region information +type CityWithRegion struct { + City + RegionName string `json:"regionName,omitempty"` +} diff --git a/backend/internal/models/password_reset.go b/backend/internal/models/password_reset.go new file mode 100755 index 0000000..b93ad98 --- /dev/null +++ b/backend/internal/models/password_reset.go @@ -0,0 +1,13 @@ +package models + +import "time" + +// PasswordReset represents a password reset token +type PasswordReset struct { + ID int `json:"id" db:"id"` + UserID int `json:"userId" db:"user_id"` + Token string `json:"token" db:"token"` + ExpiresAt time.Time `json:"expiresAt" db:"expires_at"` + Used bool `json:"used" db:"used"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go new file mode 100755 index 0000000..8f14ade --- /dev/null +++ b/backend/internal/models/user.go @@ -0,0 +1,61 @@ +package models + +import "time" + +// User represents a system user (SuperAdmin, CompanyAdmin, Recruiter, or JobSeeker) +type User struct { + ID int `json:"id" db:"id"` + Identifier string `json:"identifier" db:"identifier"` + PasswordHash string `json:"-" db:"password_hash"` // Never expose password hash in JSON + Role string `json:"role" db:"role"` // superadmin, companyAdmin, recruiter, jobSeeker + + // Personal Info + FullName string `json:"fullName" db:"full_name"` + Phone *string `json:"phone,omitempty" db:"phone"` + LineID *string `json:"lineId,omitempty" db:"line_id"` + WhatsApp *string `json:"whatsapp,omitempty" db:"whatsapp"` + Instagram *string `json:"instagram,omitempty" db:"instagram"` + + // Settings + Language string `json:"language" db:"language"` // pt, en, es, ja + Active bool `json:"active" db:"active"` + + // Metadata + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + LastLoginAt *time.Time `json:"lastLoginAt,omitempty" db:"last_login_at"` +} + +// UserResponse is the public representation of a user (without sensitive data) +type UserResponse struct { + ID int `json:"id"` + Identifier string `json:"identifier"` + Role string `json:"role"` + FullName string `json:"fullName"` + Phone *string `json:"phone,omitempty"` + LineID *string `json:"lineId,omitempty"` + WhatsApp *string `json:"whatsapp,omitempty"` + Instagram *string `json:"instagram,omitempty"` + Language string `json:"language"` + Active bool `json:"active"` + CreatedAt time.Time `json:"createdAt"` + LastLoginAt *time.Time `json:"lastLoginAt,omitempty"` +} + +// ToResponse converts User to UserResponse +func (u *User) ToResponse() UserResponse { + return UserResponse{ + ID: u.ID, + Identifier: u.Identifier, + Role: u.Role, + FullName: u.FullName, + Phone: u.Phone, + LineID: u.LineID, + WhatsApp: u.WhatsApp, + Instagram: u.Instagram, + Language: u.Language, + Active: u.Active, + CreatedAt: u.CreatedAt, + LastLoginAt: u.LastLoginAt, + } +} diff --git a/backend/internal/models/user_company.go b/backend/internal/models/user_company.go new file mode 100755 index 0000000..c013893 --- /dev/null +++ b/backend/internal/models/user_company.go @@ -0,0 +1,50 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +// UserCompany represents the N:M relationship between users and companies +type UserCompany struct { + ID int `json:"id" db:"id"` + UserID int `json:"userId" db:"user_id"` + CompanyID int `json:"companyId" db:"company_id"` + Role string `json:"role" db:"role"` // companyAdmin, recruiter + Permissions JSONMap `json:"permissions,omitempty" db:"permissions"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` +} + +// UserCompanyWithDetails includes user and company information +type UserCompanyWithDetails struct { + UserCompany + UserName string `json:"userName,omitempty"` + CompanyName string `json:"companyName,omitempty"` +} + +// JSONMap is a custom type for JSONB fields +type JSONMap map[string]interface{} + +// Value implements the driver.Valuer interface for JSON serialization +func (j JSONMap) Value() (driver.Value, error) { + if j == nil { + return nil, nil + } + return json.Marshal(j) +} + +// Scan implements the sql.Scanner interface for JSON deserialization +func (j *JSONMap) Scan(value interface{}) error { + if value == nil { + *j = nil + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return nil + } + + return json.Unmarshal(bytes, j) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go new file mode 100755 index 0000000..4fd9214 --- /dev/null +++ b/backend/internal/router/router.go @@ -0,0 +1,97 @@ +package router + +import ( + "net/http" + "os" + + "github.com/rede5/gohorsejobs/backend/internal/api/middleware" + "github.com/rede5/gohorsejobs/backend/internal/database" + "github.com/rede5/gohorsejobs/backend/internal/handlers" + "github.com/rede5/gohorsejobs/backend/internal/infrastructure/persistence/postgres" + "github.com/rede5/gohorsejobs/backend/internal/services" + + // Core Imports + apiHandlers "github.com/rede5/gohorsejobs/backend/internal/api/handlers" + authUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth" + tenantUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant" + userUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user" + authInfra "github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth" + + httpSwagger "github.com/swaggo/http-swagger/v2" + _ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs +) + +func NewRouter() http.Handler { + mux := http.NewServeMux() + + // Initialize Services + jobService := services.NewJobService(database.DB) + applicationService := services.NewApplicationService(database.DB) + + // --- CORE ARCHITECTURE INITIALIZATION --- + // Infrastructure + // Infrastructure, + userRepo := postgres.NewUserRepository(database.DB) + companyRepo := postgres.NewCompanyRepository(database.DB) + + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + // Fallback for dev, but really should be in env + jwtSecret = "default-dev-secret-do-not-use-in-prod" + } + + authService := authInfra.NewJWTService(jwtSecret, "todai-jobs") + + // UseCases + loginUC := authUC.NewLoginUseCase(userRepo, authService) + createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService) + createUserUC := userUC.NewCreateUserUseCase(userRepo, authService) + listUsersUC := userUC.NewListUsersUseCase(userRepo) + deleteUserUC := userUC.NewDeleteUserUseCase(userRepo) + + // Handlers & Middleware + coreHandlers := apiHandlers.NewCoreHandlers(loginUC, createCompanyUC, createUserUC, listUsersUC, deleteUserUC) + authMiddleware := middleware.NewMiddleware(authService) + + // Initialize Legacy Handlers + jobHandler := handlers.NewJobHandler(jobService) + applicationHandler := handlers.NewApplicationHandler(applicationService) + + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // --- CORE ROUTES --- + // Public + mux.HandleFunc("POST /api/v1/auth/login", coreHandlers.Login) + mux.HandleFunc("POST /api/v1/companies", coreHandlers.CreateCompany) + + // Protected + // Note: In Go 1.22+, we can wrap specific patterns. Or we can just wrap the handler. + // For simplicity, we wrap the handler function. + mux.Handle("POST /api/v1/users", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CreateUser))) + mux.Handle("GET /api/v1/users", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListUsers))) + mux.Handle("DELETE /api/v1/users/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.DeleteUser))) + + // Job Routes + mux.HandleFunc("GET /jobs", jobHandler.GetJobs) + mux.HandleFunc("POST /jobs", jobHandler.CreateJob) + mux.HandleFunc("GET /jobs/{id}", jobHandler.GetJobByID) + mux.HandleFunc("PUT /jobs/{id}", jobHandler.UpdateJob) + mux.HandleFunc("DELETE /jobs/{id}", jobHandler.DeleteJob) + + // Application Routes + mux.HandleFunc("POST /applications", applicationHandler.CreateApplication) + mux.HandleFunc("GET /applications", applicationHandler.GetApplications) + mux.HandleFunc("GET /applications/{id}", applicationHandler.GetApplicationByID) + mux.HandleFunc("PUT /applications/{id}/status", applicationHandler.UpdateApplicationStatus) + + // Swagger Route + mux.HandleFunc("/swagger/", httpSwagger.WrapHandler) + + // Wrap the mux with CORS middleware + handler := middleware.CORSMiddleware(mux) + + return handler +} diff --git a/backend/internal/services/application_service.go b/backend/internal/services/application_service.go new file mode 100644 index 0000000..1366ddc --- /dev/null +++ b/backend/internal/services/application_service.go @@ -0,0 +1,114 @@ +package services + +import ( + "database/sql" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/dto" + "github.com/rede5/gohorsejobs/backend/internal/models" +) + +type ApplicationService struct { + DB *sql.DB +} + +func NewApplicationService(db *sql.DB) *ApplicationService { + return &ApplicationService{DB: db} +} + +func (s *ApplicationService) CreateApplication(req dto.CreateApplicationRequest) (*models.Application, error) { + query := ` + INSERT INTO applications ( + job_id, user_id, name, phone, line_id, whatsapp, email, + message, resume_url, documents, status, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING id, created_at, updated_at + ` + + app := &models.Application{ + JobID: req.JobID, + UserID: req.UserID, + Name: req.Name, + Phone: req.Phone, + LineID: req.LineID, + WhatsApp: req.WhatsApp, + Email: req.Email, + Message: req.Message, + ResumeURL: req.ResumeURL, + Documents: req.Documents, + Status: "pending", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err := s.DB.QueryRow( + query, + app.JobID, app.UserID, app.Name, app.Phone, app.LineID, app.WhatsApp, app.Email, + app.Message, app.ResumeURL, app.Documents, app.Status, app.CreatedAt, app.UpdatedAt, + ).Scan(&app.ID, &app.CreatedAt, &app.UpdatedAt) + + if err != nil { + return nil, err + } + + return app, nil +} + +func (s *ApplicationService) GetApplications(jobID int) ([]models.Application, error) { + // Simple get by Job ID + query := ` + SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email, + message, resume_url, status, created_at, updated_at + FROM applications WHERE job_id = $1 + ` + rows, err := s.DB.Query(query, jobID) + if err != nil { + return nil, err + } + defer rows.Close() + + var apps []models.Application + for rows.Next() { + var a models.Application + if err := rows.Scan( + &a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email, + &a.Message, &a.ResumeURL, &a.Status, &a.CreatedAt, &a.UpdatedAt, + ); err != nil { + return nil, err + } + apps = append(apps, a) + } + return apps, nil +} + +func (s *ApplicationService) GetApplicationByID(id int) (*models.Application, error) { + var a models.Application + query := ` + SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email, + message, resume_url, documents, status, created_at, updated_at + FROM applications WHERE id = $1 + ` + err := s.DB.QueryRow(query, id).Scan( + &a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email, + &a.Message, &a.ResumeURL, &a.Documents, &a.Status, &a.CreatedAt, &a.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &a, nil +} + +func (s *ApplicationService) UpdateApplicationStatus(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { + query := ` + UPDATE applications SET status = $1, updated_at = NOW() + WHERE id = $2 + RETURNING updated_at + ` + var updatedAt time.Time + err := s.DB.QueryRow(query, req.Status, id).Scan(&updatedAt) + if err != nil { + return nil, err + } + + return s.GetApplicationByID(id) +} diff --git a/backend/internal/services/job_service.go b/backend/internal/services/job_service.go new file mode 100644 index 0000000..7600307 --- /dev/null +++ b/backend/internal/services/job_service.go @@ -0,0 +1,181 @@ +package services + +import ( + "database/sql" + "fmt" + "strings" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/dto" + "github.com/rede5/gohorsejobs/backend/internal/models" +) + +type JobService struct { + DB *sql.DB +} + +func NewJobService(db *sql.DB) *JobService { + return &JobService{DB: db} +} + +func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) { + query := ` + INSERT INTO jobs ( + company_id, title, description, salary_min, salary_max, salary_type, + employment_type, working_hours, location, region_id, city_id, + requirements, benefits, visa_support, language_level, status, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + RETURNING id, created_at, updated_at + ` + + job := &models.Job{ + CompanyID: req.CompanyID, + Title: req.Title, + Description: req.Description, + SalaryMin: req.SalaryMin, + SalaryMax: req.SalaryMax, + SalaryType: req.SalaryType, + EmploymentType: req.EmploymentType, + WorkingHours: req.WorkingHours, + Location: req.Location, + RegionID: req.RegionID, + CityID: req.CityID, + Requirements: req.Requirements, + Benefits: req.Benefits, + VisaSupport: req.VisaSupport, + LanguageLevel: req.LanguageLevel, + Status: req.Status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err := s.DB.QueryRow( + query, + job.CompanyID, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, + job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID, + job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt, + ).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt) + + if err != nil { + return nil, err + } + + return job, nil +} + +func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.Job, int, error) { + baseQuery := `SELECT id, company_id, title, description, salary_min, salary_max, salary_type, employment_type, location, status, created_at, updated_at FROM jobs WHERE 1=1` + countQuery := `SELECT COUNT(*) FROM jobs WHERE 1=1` + + var args []interface{} + argId := 1 + + if filter.CompanyID != nil { + baseQuery += fmt.Sprintf(" AND company_id = $%d", argId) + countQuery += fmt.Sprintf(" AND company_id = $%d", argId) + args = append(args, *filter.CompanyID) + argId++ + } + + // Add more filters as needed... + + // Pagination + limit := filter.Limit + if limit == 0 { + limit = 10 + } + offset := (filter.Page - 1) * limit + if offset < 0 { + offset = 0 + } + + paginationQuery := baseQuery + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argId, argId+1) + paginationArgs := append(args, limit, offset) + + rows, err := s.DB.Query(paginationQuery, paginationArgs...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var jobs []models.Job + for rows.Next() { + var j models.Job + if err := rows.Scan(&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, &j.EmploymentType, &j.Location, &j.Status, &j.CreatedAt, &j.UpdatedAt); err != nil { + return nil, 0, err + } + jobs = append(jobs, j) + } + + var total int + err = s.DB.QueryRow(countQuery, args...).Scan(&total) + if err != nil { + return nil, 0, err + } + + return jobs, total, nil +} + +func (s *JobService) GetJobByID(id int) (*models.Job, error) { + var j models.Job + query := ` + SELECT id, company_id, title, description, salary_min, salary_max, salary_type, + employment_type, working_hours, location, region_id, city_id, + requirements, benefits, visa_support, language_level, status, created_at, updated_at + FROM jobs WHERE id = $1 + ` + err := s.DB.QueryRow(query, id).Scan( + &j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, + &j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, + &j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.CreatedAt, &j.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &j, nil +} + +func (s *JobService) UpdateJob(id int, req dto.UpdateJobRequest) (*models.Job, error) { + var setClauses []string + var args []interface{} + argId := 1 + + if req.Title != nil { + setClauses = append(setClauses, fmt.Sprintf("title = $%d", argId)) + args = append(args, *req.Title) + argId++ + } + if req.Description != nil { + setClauses = append(setClauses, fmt.Sprintf("description = $%d", argId)) + args = append(args, *req.Description) + argId++ + } + // Add other fields... + if req.Status != nil { + setClauses = append(setClauses, fmt.Sprintf("status = $%d", argId)) + args = append(args, *req.Status) + argId++ + } + + if len(setClauses) == 0 { + return s.GetJobByID(id) + } + + setClauses = append(setClauses, "updated_at = NOW()") + + query := fmt.Sprintf("UPDATE jobs SET %s WHERE id = $%d RETURNING id, updated_at", strings.Join(setClauses, ", "), argId) + args = append(args, id) + + var j models.Job + err := s.DB.QueryRow(query, args...).Scan(&j.ID, &j.UpdatedAt) + if err != nil { + return nil, err + } + + return s.GetJobByID(id) +} + +func (s *JobService) DeleteJob(id int) error { + _, err := s.DB.Exec("DELETE FROM jobs WHERE id = $1", id) + return err +} diff --git a/backend/internal/utils/jwt.go b/backend/internal/utils/jwt.go new file mode 100755 index 0000000..f617349 --- /dev/null +++ b/backend/internal/utils/jwt.go @@ -0,0 +1,58 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte("your-secret-key-change-this-in-production") // TODO: Move to env var + +// Claims represents JWT custom claims +type Claims struct { + UserID int `json:"userId"` + Identifier string `json:"identifier"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +// GenerateJWT generates a new JWT token for a user +func GenerateJWT(userID int, identifier string, role string) (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) // 24 hours + + claims := &Claims{ + UserID: userID, + Identifier: identifier, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +// ValidateJWT validates a JWT token and returns the claims +func ValidateJWT(tokenString string) (*Claims, error) { + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("invalid signing method") + } + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +} diff --git a/backend/internal/utils/password.go b/backend/internal/utils/password.go new file mode 100755 index 0000000..244ab8f --- /dev/null +++ b/backend/internal/utils/password.go @@ -0,0 +1,15 @@ +package utils + +import "golang.org/x/crypto/bcrypt" + +// HashPassword generates a bcrypt hash from a plain password +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPasswordHash compares a password with its hash +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/backend/migrations/001_create_users_table.sql b/backend/migrations/001_create_users_table.sql new file mode 100755 index 0000000..3f2e712 --- /dev/null +++ b/backend/migrations/001_create_users_table.sql @@ -0,0 +1,35 @@ +-- Migration: Create users table +-- Description: Stores all system users (SuperAdmin, CompanyAdmin, Recruiter, JobSeeker) + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + identifier VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'companyAdmin', 'recruiter', 'jobSeeker')), + + -- Personal Info + full_name VARCHAR(255), + phone VARCHAR(30), + line_id VARCHAR(100), + whatsapp VARCHAR(30), + instagram VARCHAR(100), + + -- Settings + language VARCHAR(5) DEFAULT 'en' CHECK (language IN ('pt', 'en', 'es', 'ja')), + active BOOLEAN DEFAULT true, + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP +); + +-- Indexes for performance +CREATE INDEX idx_users_identifier ON users(identifier); +CREATE INDEX idx_users_role ON users(role); +CREATE INDEX idx_users_active ON users(active); + +-- Comments for documentation +COMMENT ON TABLE users IS 'Stores all system users across all roles'; +COMMENT ON COLUMN users.identifier IS 'Username for login (NOT email)'; +COMMENT ON COLUMN users.role IS 'User role: superadmin, companyAdmin, recruiter, or jobSeeker'; diff --git a/backend/migrations/002_create_companies_table.sql b/backend/migrations/002_create_companies_table.sql new file mode 100755 index 0000000..90af7d3 --- /dev/null +++ b/backend/migrations/002_create_companies_table.sql @@ -0,0 +1,41 @@ +-- Migration: Create companies table +-- Description: Stores company information for employers + +CREATE TABLE IF NOT EXISTS companies ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, -- URL friendly name + type VARCHAR(50) DEFAULT 'company', -- company, agency, etc + document VARCHAR(100), -- Houjin Bangou (Company Number) + + -- Contact + address TEXT, + region_id INT, + city_id INT, + phone VARCHAR(30), + email VARCHAR(255), + website VARCHAR(255), + + -- Branding + logo_url TEXT, + description TEXT, + + -- Status + active BOOLEAN DEFAULT true, + verified BOOLEAN DEFAULT false, + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX idx_companies_active ON companies(active); +CREATE INDEX idx_companies_verified ON companies(verified); +CREATE INDEX idx_companies_region ON companies(region_id); +CREATE INDEX idx_companies_slug ON companies(slug); + +-- Comments +COMMENT ON TABLE companies IS 'Stores company/employer information'; +COMMENT ON COLUMN companies.document IS 'Japanese Company Number (Houjin Bangou)'; +COMMENT ON COLUMN companies.verified IS 'Whether company has been verified by admin'; diff --git a/backend/migrations/003_create_user_companies_table.sql b/backend/migrations/003_create_user_companies_table.sql new file mode 100755 index 0000000..9ae448b --- /dev/null +++ b/backend/migrations/003_create_user_companies_table.sql @@ -0,0 +1,29 @@ +-- Migration: Create user_companies pivot table +-- Description: N:M relationship for multi-tenant - users can belong to multiple companies + +CREATE TABLE IF NOT EXISTS user_companies ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + company_id INT NOT NULL, + role VARCHAR(20) NOT NULL CHECK (role IN ('companyAdmin', 'recruiter')), + permissions JSONB, -- Optional granular permissions + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + + -- Ensure unique user-company pairs + UNIQUE(user_id, company_id) +); + +-- Indexes for efficient lookups +CREATE INDEX idx_user_companies_user ON user_companies(user_id); +CREATE INDEX idx_user_companies_company ON user_companies(company_id); +CREATE INDEX idx_user_companies_role ON user_companies(role); + +-- Comments +COMMENT ON TABLE user_companies IS 'Multi-tenant pivot: links users to companies with specific roles'; +COMMENT ON COLUMN user_companies.role IS 'Role within this company: companyAdmin or recruiter'; +COMMENT ON COLUMN user_companies.permissions IS 'Optional JSON object for granular permissions per company'; diff --git a/backend/migrations/004_create_prefectures_cities_tables.sql b/backend/migrations/004_create_prefectures_cities_tables.sql new file mode 100755 index 0000000..749a9ee --- /dev/null +++ b/backend/migrations/004_create_prefectures_cities_tables.sql @@ -0,0 +1,29 @@ +-- Migration: Create regions and cities tables +-- Description: Global location data (Regions/States + Cities) + +CREATE TABLE IF NOT EXISTS regions ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + country_code VARCHAR(10) NOT NULL, -- ISO Country (US, BR, JP, etc.) + code VARCHAR(10), -- ISO Region code (e.g., SP, CA) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS cities ( + id SERIAL PRIMARY KEY, + region_id INT NOT NULL, + name VARCHAR(100) NOT NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE +); + +-- Indexes +CREATE INDEX idx_regions_country ON regions(country_code); +CREATE INDEX idx_cities_region ON cities(region_id); + +-- Comments +COMMENT ON TABLE regions IS 'Global Regions (States, Provinces, Prefectures)'; +COMMENT ON TABLE cities IS 'Global Cities by Region'; +COMMENT ON COLUMN regions.code IS 'ISO Region code (e.g., SP, CA)'; diff --git a/backend/migrations/005_create_jobs_table.sql b/backend/migrations/005_create_jobs_table.sql new file mode 100755 index 0000000..4ce62bc --- /dev/null +++ b/backend/migrations/005_create_jobs_table.sql @@ -0,0 +1,62 @@ +-- Migration: Create jobs table +-- Description: Job postings created by companies + +CREATE TABLE IF NOT EXISTS jobs ( + id SERIAL PRIMARY KEY, + company_id INT NOT NULL, + created_by INT NOT NULL, -- user who created the job + + -- Job Details + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + + -- Salary + salary_min DECIMAL(12,2), + salary_max DECIMAL(12,2), + salary_type VARCHAR(20) CHECK (salary_type IN ('hourly', 'monthly', 'yearly')), + + -- Employment + employment_type VARCHAR(30) CHECK (employment_type IN ('full-time', 'part-time', 'dispatch', 'contract')), + working_hours VARCHAR(100), + + -- Location + location VARCHAR(255), + region_id INT, + city_id INT, + + -- Requirements & Benefits (stored as JSON arrays) + requirements JSONB, + benefits JSONB, + + -- Visa & Language + visa_support BOOLEAN DEFAULT false, + language_level VARCHAR(20), -- 'N5' | 'N4' | 'N3' | 'N2' | 'N1' | 'beginner' | 'none' + + -- Status + status VARCHAR(20) DEFAULT 'open' CHECK (status IN ('open', 'closed', 'draft')), + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id), + FOREIGN KEY (region_id) REFERENCES regions(id), + FOREIGN KEY (city_id) REFERENCES cities(id) +); + +-- Indexes for filtering and search +CREATE INDEX idx_jobs_company ON jobs(company_id); +CREATE INDEX idx_jobs_status ON jobs(status); +CREATE INDEX idx_jobs_region ON jobs(region_id); +CREATE INDEX idx_jobs_employment_type ON jobs(employment_type); +CREATE INDEX idx_jobs_visa_support ON jobs(visa_support); +CREATE INDEX idx_jobs_created_at ON jobs(created_at DESC); + +-- Comments +COMMENT ON TABLE jobs IS 'Job postings created by companies'; +COMMENT ON COLUMN jobs.visa_support IS 'Whether company provides visa sponsorship'; +COMMENT ON COLUMN jobs.language_level IS 'Required Japanese language level (JLPT N5-N1, beginner, or none)'; +COMMENT ON COLUMN jobs.requirements IS 'JSON array of job requirements'; +COMMENT ON COLUMN jobs.benefits IS 'JSON array of job benefits'; diff --git a/backend/migrations/006_create_applications_table.sql b/backend/migrations/006_create_applications_table.sql new file mode 100755 index 0000000..147f381 --- /dev/null +++ b/backend/migrations/006_create_applications_table.sql @@ -0,0 +1,44 @@ +-- Migration: Create applications table +-- Description: Job applications (can be from registered users or guests) + +CREATE TABLE IF NOT EXISTS applications ( + id SERIAL PRIMARY KEY, + job_id INT NOT NULL, + user_id INT, -- NULL for guest applications + + -- Applicant Info (required for guest applications) + name VARCHAR(255), + phone VARCHAR(30), + line_id VARCHAR(100), + whatsapp VARCHAR(30), + email VARCHAR(255), + + -- Application Content + message TEXT, + resume_url TEXT, + documents JSONB, -- Array of document objects {type: string, url: string} + + -- Status & Notes + status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'shortlisted', 'rejected', 'hired')), + notes TEXT, -- Recruiter notes + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +); + +-- Indexes +CREATE INDEX idx_applications_job ON applications(job_id); +CREATE INDEX idx_applications_user ON applications(user_id); +CREATE INDEX idx_applications_status ON applications(status); +CREATE INDEX idx_applications_created_at ON applications(created_at DESC); + +-- Comments +COMMENT ON TABLE applications IS 'Job applications from users or guests'; +COMMENT ON COLUMN applications.user_id IS 'NULL if guest application'; +COMMENT ON COLUMN applications.documents IS 'JSON array of uploaded documents (residence card, resume, etc.)'; +COMMENT ON COLUMN applications.notes IS 'Internal notes from recruiters'; diff --git a/backend/migrations/007_create_favorite_jobs_table.sql b/backend/migrations/007_create_favorite_jobs_table.sql new file mode 100755 index 0000000..918f640 --- /dev/null +++ b/backend/migrations/007_create_favorite_jobs_table.sql @@ -0,0 +1,24 @@ +-- Migration: Create favorite_jobs table +-- Description: Users can save favorite jobs for later + +CREATE TABLE IF NOT EXISTS favorite_jobs ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + job_id INT NOT NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, + + -- Ensure user can't favorite same job twice + UNIQUE(user_id, job_id) +); + +-- Indexes +CREATE INDEX idx_favorite_jobs_user ON favorite_jobs(user_id); +CREATE INDEX idx_favorite_jobs_job ON favorite_jobs(job_id); + +-- Comments +COMMENT ON TABLE favorite_jobs IS 'Saved/favorited jobs by users'; diff --git a/backend/migrations/008_create_password_resets_table.sql b/backend/migrations/008_create_password_resets_table.sql new file mode 100755 index 0000000..5561c24 --- /dev/null +++ b/backend/migrations/008_create_password_resets_table.sql @@ -0,0 +1,25 @@ +-- Migration: Create password_resets table +-- Description: Password reset tokens + +CREATE TABLE IF NOT EXISTS password_resets ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + token VARCHAR(255) UNIQUE NOT NULL, + expires_at TIMESTAMP NOT NULL, + used BOOLEAN DEFAULT false, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign key + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Indexes +CREATE INDEX idx_password_resets_token ON password_resets(token); +CREATE INDEX idx_password_resets_user ON password_resets(user_id); +CREATE INDEX idx_password_resets_expires ON password_resets(expires_at); + +-- Comments +COMMENT ON TABLE password_resets IS 'Password reset tokens with expiration'; +COMMENT ON COLUMN password_resets.token IS 'Unique reset token sent to user'; +COMMENT ON COLUMN password_resets.used IS 'Whether token has been used'; diff --git a/backend/migrations/009_create_core_tables.sql b/backend/migrations/009_create_core_tables.sql new file mode 100644 index 0000000..ade141a --- /dev/null +++ b/backend/migrations/009_create_core_tables.sql @@ -0,0 +1,33 @@ +-- Migration: Create Core Architecture Tables +-- Description: Agnostic tables for Multi-Tenant Architecture (UUID based) + +-- Companies (Tenants) +CREATE TABLE IF NOT EXISTS core_companies ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + document VARCHAR(50), + contact VARCHAR(255), + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Users (Multi-Tenant) +CREATE TABLE IF NOT EXISTS core_users ( + id VARCHAR(36) PRIMARY KEY, + tenant_id VARCHAR(36) NOT NULL REFERENCES core_companies(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_email_per_tenant UNIQUE (tenant_id, email) +); + +-- Permissions / Roles (Simplified JSON store or Relational? keeping it simple Relational) +CREATE TABLE IF NOT EXISTS core_user_roles ( + user_id VARCHAR(36) NOT NULL REFERENCES core_users(id) ON DELETE CASCADE, + role VARCHAR(50) NOT NULL, + PRIMARY KEY (user_id, role) +); diff --git a/backend/migrations/010_seed_super_admin.sql b/backend/migrations/010_seed_super_admin.sql new file mode 100644 index 0000000..3b26bf9 --- /dev/null +++ b/backend/migrations/010_seed_super_admin.sql @@ -0,0 +1,36 @@ +-- Migration: Create Super Admin and System Tenant +-- Description: Inserts the default System Tenant and Super Admin user. +-- Use fixed UUIDs for reproducibility in dev environments. + +-- 1. Insert System Tenant +INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at) +VALUES ( + '00000000-0000-0000-0000-000000000001', -- Fixed System Tenant ID + 'System Tenant (SuperAdmin Context)', + 'SYSTEM', + 'admin@system.local', + 'ACTIVE', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) ON CONFLICT (id) DO NOTHING; + +-- 2. Insert Super Admin User +-- Password: "password123" (BCrypt hash) +INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, created_at, updated_at) +VALUES ( + '00000000-0000-0000-0000-000000000002', -- Fixed SuperAdmin User ID + '00000000-0000-0000-0000-000000000001', -- Link to System Tenant + 'Super Administrator', + 'admin@todai.jobs', + '$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- "password123" + 'ACTIVE', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) ON CONFLICT (id) DO NOTHING; + +-- 3. Assign Super Admin Role +INSERT INTO core_user_roles (user_id, role) +VALUES ( + '00000000-0000-0000-0000-000000000002', + 'SUPER_ADMIN' +) ON CONFLICT (user_id, role) DO NOTHING; diff --git a/backend/migrations/999_fix_gohorse_schema.sql b/backend/migrations/999_fix_gohorse_schema.sql new file mode 100644 index 0000000..4bbef51 --- /dev/null +++ b/backend/migrations/999_fix_gohorse_schema.sql @@ -0,0 +1,25 @@ + +-- Fix Companies +ALTER TABLE companies ADD COLUMN IF NOT EXISTS document VARCHAR(100); +ALTER TABLE companies ADD COLUMN IF NOT EXISTS address TEXT; +ALTER TABLE companies ADD COLUMN IF NOT EXISTS region_id INT; +ALTER TABLE companies ADD COLUMN IF NOT EXISTS city_id INT; +ALTER TABLE companies ADD COLUMN IF NOT EXISTS phone VARCHAR(30); +ALTER TABLE companies ADD COLUMN IF NOT EXISTS email VARCHAR(255); +-- website existed +ALTER TABLE companies ADD COLUMN IF NOT EXISTS verified BOOLEAN DEFAULT false; +ALTER TABLE companies ADD COLUMN IF NOT EXISTS active BOOLEAN DEFAULT true; +-- Drop legacy constraints +ALTER TABLE companies DROP CONSTRAINT IF EXISTS companies_type_check; + +-- Fix Jobs +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS salary_min DECIMAL(12,2); +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS salary_max DECIMAL(12,2); +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS salary_type VARCHAR(20); +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS employment_type VARCHAR(30); +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS working_hours VARCHAR(100); +-- location existed +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS visa_support BOOLEAN DEFAULT false; +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS language_level VARCHAR(20); +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS region_id INT; +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS city_id INT; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..e9ee3cb --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..6263cdb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,48 @@ +# GoHorseJobs Frontend + +This is the frontend for the GoHorseJobs application, built with **Next.js 15**, **Tailwind CSS**, and **shadcn/ui**. + +## 🚀 Getting Started + +### Prerequisites +- Node.js 18+ +- npm / yarn / pnpm + +### Installation + +```bash +cd frontend +npm install +``` + +### Running Development Server + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser. + +## 🛠️ Technology Stack + +- **Framework:** [Next.js 15](https://nextjs.org/) (App Router) +- **Styling:** [Tailwind CSS](https://tailwindcss.com/) +- **Components:** [shadcn/ui](https://ui.shadcn.com/) (Radix UI) +- **Icons:** [Lucide React](https://lucide.dev/) +- **Forms:** React Hook Form + Zod +- **State Management:** React Context / Hooks + +## 📂 Project Structure + +- `/src/app` - App Router pages and layouts +- `/src/components` - Reusable UI components +- `/src/components/ui` - shadcn/ui primitives +- `/src/hooks` - Custom React hooks +- `/src/lib` - Utility functions and libraries + +## ✨ Key Features + +- **Company Dashboard:** Manage jobs, applications, and messages. +- **Candidate Portal:** View and apply for jobs. +- **Profile Management:** Local database integration for profile pictures. +- **Responsive Design:** Mobile-first approach. diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..edcaef2 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..719cea2 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..4367aa3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5232 @@ +{ + "name": "my-v0-project", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-v0-project", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "@vercel/analytics": "1.3.1", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "framer-motion": "12.23.22", + "geist": "^1.3.1", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "^15.5.7", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-day-picker": "9.8.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.60.0", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "zod": "3.25.76" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.9", + "@types/node": "^22", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^9.39.1", + "postcss": "^8.5", + "tailwindcss": "^4.1.9", + "tw-animate-css": "1.3.3", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "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.0", + "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/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "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/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.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/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz", + "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz", + "integrity": "sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.1.tgz", + "integrity": "sha512-kNU4FIpcFMBLkOUcgeIteH06/8JLBcYY6Le1iKenDGCYNYFX3TQqCZjzkOsz37h7r94/99GTb7YhEr98ZBJibw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz", + "integrity": "sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz", + "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", + "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "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-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "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-context-menu": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.4.tgz", + "integrity": "sha512-ap4wdGwK52rJxGkwukU1NrnEodsUFQIooANKu+ey7d6raQ2biTcEf8za1zr0mgFHieevRTB2nK4dJeN8pTAZGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "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-dismissable-layer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz", + "integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "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-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.4.tgz", + "integrity": "sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", + "integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.4.tgz", + "integrity": "sha512-+KMpi7VAZuB46+1LD7a30zb5IxyzLgC8m8j42gk3N4TUCcViNQdX8FhoH1HDvYiA8quuqcek4R4bYpPn/SY1GA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz", + "integrity": "sha512-IQWAsQ7dsLIYDrn0WqPU+cdM7MONTv9nqrLVYoie3BPiabSfUVDe6Fr+oEt0Cofsr9ONDcDe9xhmJbL1Uq1yKg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", + "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", + "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", + "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz", + "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", + "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", + "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz", + "integrity": "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "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-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz", + "integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz", + "integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.1.tgz", + "integrity": "sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", + "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "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-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "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-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "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-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "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-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "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-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "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-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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-visually-hidden": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "postcss": "^8.4.41", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "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==", + "dev": true, + "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": "22.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.9.tgz", + "integrity": "sha512-5yBtK0k/q8PjkMXbTfeIEP/XVYnz1R9qZJ3yUicdEW7ppdDJfe+MqXEhpqDL3mtn4Wvs1u0KLEG0RXzCgNpsSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vercel/analytics": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.3.1.tgz", + "integrity": "sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==", + "license": "MPL-2.0", + "dependencies": { + "server-only": "^0.0.1" + }, + "peerDependencies": { + "next": ">= 13", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "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/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.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.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "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.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "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.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "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/cmdk": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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/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.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "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/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "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/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", + "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.1.tgz", + "integrity": "sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.5.1", + "embla-carousel-reactive-utils": "8.5.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.1.tgz", + "integrity": "sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.5.1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "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.1", + "@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-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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "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-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", + "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/geist": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz", + "integrity": "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "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/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==", + "dev": true, + "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/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/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/input-otp": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", + "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "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-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "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.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "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==", + "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/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/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.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "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-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.454.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", + "integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "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/next": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.7", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.0.tgz", + "integrity": "sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.64.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.64.0.tgz", + "integrity": "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==", + "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": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "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/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "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/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.3.tgz", + "integrity": "sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==", + "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.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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/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": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b486e93 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,75 @@ +{ + "name": "my-v0-project", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "@vercel/analytics": "1.3.1", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "framer-motion": "12.23.22", + "geist": "^1.3.1", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "^15.5.7", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-day-picker": "9.8.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.60.0", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "zod": "3.25.76" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.9", + "@types/node": "^22", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^9.39.1", + "postcss": "^8.5", + "tailwindcss": "^4.1.9", + "tw-animate-css": "1.3.3", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/public/abstract-minimal-geometric-pattern-light-gray.jpg b/frontend/public/abstract-minimal-geometric-pattern-light-gray.jpg new file mode 100644 index 0000000..e849b0d Binary files /dev/null and b/frontend/public/abstract-minimal-geometric-pattern-light-gray.jpg differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..79d0ba1 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/gohorse-jobs-logo.png b/frontend/public/gohorse-jobs-logo.png new file mode 100644 index 0000000..46a4b85 Binary files /dev/null and b/frontend/public/gohorse-jobs-logo.png differ diff --git a/frontend/public/gohorse-logo-transparent.png b/frontend/public/gohorse-logo-transparent.png new file mode 100644 index 0000000..4b74ae7 Binary files /dev/null and b/frontend/public/gohorse-logo-transparent.png differ diff --git a/frontend/public/gohorse-logo.png b/frontend/public/gohorse-logo.png new file mode 100644 index 0000000..89fc0a3 Binary files /dev/null and b/frontend/public/gohorse-logo.png differ diff --git a/frontend/public/gohorsejobs-logo.jpeg b/frontend/public/gohorsejobs-logo.jpeg new file mode 100644 index 0000000..0782831 Binary files /dev/null and b/frontend/public/gohorsejobs-logo.jpeg differ diff --git a/frontend/public/logo_ghj.jpg b/frontend/public/logo_ghj.jpg new file mode 100644 index 0000000..3b340d0 Binary files /dev/null and b/frontend/public/logo_ghj.jpg differ diff --git a/frontend/public/logo_tj.png b/frontend/public/logo_tj.png new file mode 100644 index 0000000..2088829 Binary files /dev/null and b/frontend/public/logo_tj.png differ diff --git a/frontend/public/logohorse.png b/frontend/public/logohorse.png new file mode 100644 index 0000000..7a85d77 Binary files /dev/null and b/frontend/public/logohorse.png differ diff --git a/frontend/public/minimalist-professional-workspace-with-laptop-and-.jpg b/frontend/public/minimalist-professional-workspace-with-laptop-and-.jpg new file mode 100644 index 0000000..42c6ce0 Binary files /dev/null and b/frontend/public/minimalist-professional-workspace-with-laptop-and-.jpg differ diff --git a/frontend/public/professional-person-smiling-at-laptop-optimistic.jpg b/frontend/public/professional-person-smiling-at-laptop-optimistic.jpg new file mode 100644 index 0000000..be53e21 Binary files /dev/null and b/frontend/public/professional-person-smiling-at-laptop-optimistic.jpg differ diff --git a/frontend/src/app/cadastro/candidato/page.tsx b/frontend/src/app/cadastro/candidato/page.tsx new file mode 100644 index 0000000..c4b9b48 --- /dev/null +++ b/frontend/src/app/cadastro/candidato/page.tsx @@ -0,0 +1,591 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + User, + Mail, + Lock, + Eye, + EyeOff, + Phone, + MapPin, + Calendar, + GraduationCap, + Briefcase, + ArrowLeft, +} from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { motion } from "framer-motion"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +const candidateSchema = z.object({ + fullName: z.string().min(2, "Nome deve ter pelo menos 2 caracteres"), + email: z.string().email("Email inválido"), + password: z.string().min(6, "Senha deve ter pelo menos 6 caracteres"), + confirmPassword: z.string(), + phone: z.string().min(10, "Telefone deve ter pelo menos 10 dígitos"), + birthDate: z.string().min(1, "Data de nascimento é obrigatória"), + address: z.string().min(5, "Endereço deve ter pelo menos 5 caracteres"), + city: z.string().min(2, "Cidade é obrigatória"), + state: z.string().min(2, "Estado é obrigatório"), + zipCode: z.string().min(8, "CEP deve ter 8 dígitos"), + education: z.string().min(1, "Nível de escolaridade é obrigatório"), + experience: z.string().min(1, "Experiência profissional é obrigatória"), + skills: z.string().optional(), + objective: z.string().optional(), + acceptTerms: z.boolean().refine(val => val === true, "Você deve aceitar os termos"), + acceptNewsletter: z.boolean().optional(), +}).refine(data => data.password === data.confirmPassword, { + message: "Senhas não coincidem", + path: ["confirmPassword"], +}); + +type CandidateFormData = z.infer; + +export default function CandidateRegisterPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [currentStep, setCurrentStep] = useState(1); + + const { + register, + handleSubmit, + formState: { errors }, + setValue, + watch, + } = useForm({ + resolver: zodResolver(candidateSchema), + }); + + const acceptTerms = watch("acceptTerms"); + const acceptNewsletter = watch("acceptNewsletter"); + + const onSubmit = async (data: CandidateFormData) => { + setLoading(true); + try { + // Simular cadastro + await new Promise(resolve => setTimeout(resolve, 2000)); + console.log("Dados do candidato:", data); + + // Redirecionar para login após cadastro + router.push("/login?message=Cadastro realizado com sucesso! Faça login para continuar."); + } catch (error) { + console.error("Erro no cadastro:", error); + } finally { + setLoading(false); + } + }; + + const nextStep = () => { + if (currentStep < 3) setCurrentStep(currentStep + 1); + }; + + const prevStep = () => { + if (currentStep > 1) setCurrentStep(currentStep - 1); + }; + + const stepVariants = { + hidden: { opacity: 0, x: 20 }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: -20 } + }; + + return ( +
+ {/* Left Panel - Informações */} +
+ +
+ GoHorse Jobs +
+ +

+ Cadastre-se como Candidato +

+ +

+ Crie sua conta e tenha acesso às melhores oportunidades de emprego. + Encontre a vaga dos seus sonhos hoje mesmo! +

+ +
+
+
+ Acesso a milhares de vagas +
+
+
+ Candidaturas rápidas e fáceis +
+
+
+ Perfil profissional completo +
+
+
+ Notificações de novas oportunidades +
+
+
+
+ + {/* Right Panel - Formulário */} +
+
+ {/* Header */} +
+ + + Voltar ao Login + + +

+ Criar Conta - Candidato +

+

+ Preencha seus dados para criar sua conta +

+
+ + {/* Progress Indicator */} +
+
+ Etapa {currentStep} de 3 + + {currentStep === 1 && "Dados Pessoais"} + {currentStep === 2 && "Endereço e Contato"} + {currentStep === 3 && "Perfil Profissional"} + +
+
+
+
+
+ +
+ {/* Step 1: Dados Pessoais */} + {currentStep === 1 && ( + +
+ +
+ + +
+ {errors.fullName && ( + {errors.fullName.message} + )} +
+ +
+ +
+ + +
+ {errors.email && ( + {errors.email.message} + )} +
+ +
+ +
+ + + +
+ {errors.password && ( + {errors.password.message} + )} +
+ +
+ +
+ + + +
+ {errors.confirmPassword && ( + {errors.confirmPassword.message} + )} +
+ +
+ +
+ + +
+ {errors.birthDate && ( + {errors.birthDate.message} + )} +
+ + +
+ )} + + {/* Step 2: Endereço e Contato */} + {currentStep === 2 && ( + +
+ +
+ + +
+ {errors.phone && ( + {errors.phone.message} + )} +
+ +
+ +
+ + +
+ {errors.address && ( + {errors.address.message} + )} +
+ +
+
+ + + {errors.city && ( + {errors.city.message} + )} +
+ +
+ + + {errors.state && ( + {errors.state.message} + )} +
+
+ +
+ + + {errors.zipCode && ( + {errors.zipCode.message} + )} +
+ +
+ + +
+
+ )} + + {/* Step 3: Perfil Profissional */} + {currentStep === 3 && ( + +
+ + + {errors.education && ( + {errors.education.message} + )} +
+ +
+ + + {errors.experience && ( + {errors.experience.message} + )} +
+ +
+ +