diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..406ca0508a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Dependencies
+
+node_modules/
+backend/node_modules/
+frontend/node_modules/
+
+# Environment variables
+
+.env
+*.env
+backend/.env
+frontend/.env
+
+# Build files
+
+dist/
+build/
+.vite/
+
+# Logs
+
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# OS files
+
+.DS_Store
+Thumbs.db
+
+# IDE
+
+.vscode/
+.idea/
diff --git a/backend/docs/admin.docs.js b/backend/docs/admin.docs.js
new file mode 100644
index 0000000000..d7dd7aedb5
--- /dev/null
+++ b/backend/docs/admin.docs.js
@@ -0,0 +1,1453 @@
+/**
+ * Admin API documentation (OpenAPI).
+ * Keep route/controller files free of Swagger comments.
+ *
+ * All /api/admin routes use router-level:
+ * - userAuthMiddleware (cookieAuth)
+ * - authorizeRoles("admin")
+ *
+ * Sections: Dashboard, Users, Venues, Bookings, Payments
+ */
+
+/**
+ * @openapi
+ * /api/admin/dashboard/stats:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Dashboard stats
+ * description: >
+ * Returns marketplace overview counts and total revenue from confirmed paid bookings.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Dashboard stats fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Dashboard stats fetched successfully
+ * data:
+ * type: object
+ * properties:
+ * totalUsers:
+ * type: integer
+ * totalProviders:
+ * type: integer
+ * totalVenues:
+ * type: integer
+ * activeVenues:
+ * type: integer
+ * confirmedPaidBookings:
+ * type: integer
+ * totalRevenue:
+ * type: number
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied. You do not have the permission to perform this action.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/dashboard/recent-activity:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Recent activity
+ * description: >
+ * Returns the 5 most recent marketplace users, venues, and bookings.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Recent activity fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Recent activity fetched successfully
+ * data:
+ * type: object
+ * properties:
+ * users:
+ * type: array
+ * items:
+ * type: object
+ * venues:
+ * type: array
+ * items:
+ * type: object
+ * bookings:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/users:
+ * get:
+ * tags:
+ * - Admin
+ * summary: List users
+ * description: >
+ * Paginated marketplace user list. Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * - in: query
+ * name: role
+ * schema:
+ * type: string
+ * description: Filter by role value in the user's roles array
+ * - in: query
+ * name: isActive
+ * schema:
+ * type: string
+ * enum: ["true", "false"]
+ * - in: query
+ * name: search
+ * schema:
+ * type: string
+ * description: Search name, email, or phone
+ * responses:
+ * "200":
+ * description: Users fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Users fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * description: Sanitized user (password and OTP fields excluded)
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/users/{id}:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Get user by ID
+ * description: >
+ * Returns a marketplace user plus booking and venue counts.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: User fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: User fetched successfully
+ * data:
+ * type: object
+ * properties:
+ * user:
+ * type: object
+ * bookingCount:
+ * type: integer
+ * venueCount:
+ * type: integer
+ * "400":
+ * description: Invalid user ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid user ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/users/{id}/activate:
+ * patch:
+ * tags:
+ * - Admin
+ * summary: Activate user
+ * description: >
+ * Sets `isActive` to true. Platform operator accounts cannot be managed here.
+ * Requires a valid `token` cookie and the `admin` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: User activated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: User activated successfully
+ * data:
+ * type: object
+ * description: Sanitized user
+ * "400":
+ * description: Invalid user ID, or platform operator account
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Platform operator accounts cannot be managed here
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/users/{id}/deactivate:
+ * patch:
+ * tags:
+ * - Admin
+ * summary: Deactivate user
+ * description: >
+ * Sets `isActive` to false. Cannot deactivate your own account or platform operator accounts.
+ * Requires a valid `token` cookie and the `admin` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: User deactivated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: User deactivated successfully
+ * data:
+ * type: object
+ * description: Sanitized user
+ * "400":
+ * description: Invalid user ID, self-deactivation, or platform operator account
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: You cannot deactivate your own account
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/venues:
+ * get:
+ * tags:
+ * - Admin
+ * summary: List venues
+ * description: >
+ * Paginated venue list with owner populated. Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * - in: query
+ * name: isActive
+ * schema:
+ * type: string
+ * enum: ["true", "false"]
+ * - in: query
+ * name: city
+ * schema:
+ * type: string
+ * - in: query
+ * name: ownerId
+ * schema:
+ * type: string
+ * - in: query
+ * name: search
+ * schema:
+ * type: string
+ * description: Search title, city, or address
+ * responses:
+ * "200":
+ * description: Venues fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venues fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/venues/{id}:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Get venue by ID
+ * description: >
+ * Returns a venue with owner details. Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: Venue fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue fetched successfully
+ * data:
+ * type: object
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/venues/{id}/activate:
+ * patch:
+ * tags:
+ * - Admin
+ * summary: Activate venue
+ * description: >
+ * Sets venue `isActive` to true. Requires a valid `token` cookie and the `admin` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: Venue activated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue activated successfully
+ * data:
+ * type: object
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/venues/{id}/deactivate:
+ * patch:
+ * tags:
+ * - Admin
+ * summary: Deactivate venue
+ * description: >
+ * Sets venue `isActive` to false. Requires a valid `token` cookie and the `admin` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: Venue deactivated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue deactivated successfully
+ * data:
+ * type: object
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/bookings:
+ * get:
+ * tags:
+ * - Admin
+ * summary: List bookings
+ * description: >
+ * Paginated booking list with user, venue, and availability populated.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * - in: query
+ * name: bookingStatus
+ * schema:
+ * type: string
+ * - in: query
+ * name: paymentStatus
+ * schema:
+ * type: string
+ * - in: query
+ * name: venueId
+ * schema:
+ * type: string
+ * - in: query
+ * name: userId
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: Bookings fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Bookings fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/bookings/{id}:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Get booking by ID
+ * description: >
+ * Returns a booking with user, venue, and availability populated.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * "200":
+ * description: Booking fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Booking fetched successfully
+ * data:
+ * type: object
+ * "400":
+ * description: Invalid booking ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid booking ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "404":
+ * description: Booking not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Booking not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/payments/orders:
+ * get:
+ * tags:
+ * - Admin
+ * summary: List payment orders
+ * description: >
+ * Paginated Razorpay payment orders. Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * - in: query
+ * name: status
+ * schema:
+ * type: string
+ * description: Filter by payment order status (e.g. created, completed)
+ * responses:
+ * "200":
+ * description: Payment orders fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Payment orders fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/payments/history:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Payment history
+ * description: >
+ * Paginated list of bookings with `paymentStatus` equal to `paid`.
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * responses:
+ * "200":
+ * description: Payment history fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Payment history fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/admin/payments/abandoned:
+ * get:
+ * tags:
+ * - Admin
+ * summary: Abandoned payment orders
+ * description: >
+ * Paginated payment orders with status `created` older than the given hours cutoff (default 24).
+ * Requires a valid `token` cookie and the `admin` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: query
+ * name: page
+ * schema:
+ * type: integer
+ * default: 1
+ * - in: query
+ * name: limit
+ * schema:
+ * type: integer
+ * default: 10
+ * maximum: 100
+ * - in: query
+ * name: hours
+ * schema:
+ * type: integer
+ * default: 24
+ * description: Minimum age in hours for an order to be considered abandoned
+ * responses:
+ * "200":
+ * description: Abandoned payment orders fetched successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Abandoned payment orders fetched successfully
+ * count:
+ * type: integer
+ * page:
+ * type: integer
+ * limit:
+ * type: integer
+ * hours:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "403":
+ * description: Authenticated user is not an admin
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
diff --git a/backend/docs/auth.docs.js b/backend/docs/auth.docs.js
new file mode 100644
index 0000000000..378d83c23f
--- /dev/null
+++ b/backend/docs/auth.docs.js
@@ -0,0 +1,889 @@
+/**
+ * Auth API documentation (OpenAPI).
+ * Keep route/controller files free of Swagger comments.
+ */
+
+/**
+ * @openapi
+ * /api/auth/login:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Login
+ * description: >
+ * Authenticates a verified, active user with email and password.
+ * On success, sets an HTTP-only JWT cookie named `token` (7-day max age).
+ * This endpoint is public and does not require an existing cookie.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - email
+ * - password
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * password:
+ * type: string
+ * format: password
+ * example: Password1
+ * responses:
+ * "200":
+ * description: Login successful; `token` cookie is set
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Login successful
+ * user:
+ * type: object
+ * properties:
+ * id:
+ * type: string
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * roles:
+ * type: array
+ * items:
+ * type: string
+ * profileImage:
+ * type: string
+ * "400":
+ * description: >
+ * Missing fields, invalid email format, email not verified,
+ * or invalid password
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid password
+ * "403":
+ * description: Account has been disabled
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Your account has been disabled. Please contact support.
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/register:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Register
+ * description: >
+ * Creates a new customer account with name, email, phone, and password.
+ * Sends a 6-digit email OTP for verification. Does not return a JWT or set a cookie.
+ * This endpoint is public.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - name
+ * - email
+ * - phone
+ * - password
+ * properties:
+ * name:
+ * type: string
+ * example: Rahul Sharma
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * phone:
+ * type: string
+ * description: Indian 10-digit mobile number starting with 6-9
+ * example: "9876543210"
+ * password:
+ * type: string
+ * format: password
+ * description: At least 8 characters with uppercase, lowercase, and a number
+ * example: Password1
+ * responses:
+ * "201":
+ * description: Registration successful; verification OTP sent by email
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Registration successful. Please check your email for the verification OTP.
+ * "400":
+ * description: >
+ * Missing fields, invalid email or phone, weak password,
+ * or email/phone already exists
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: All fields are required
+ * "500":
+ * description: Registration failed due to a server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Registration failed. Please try again.
+ */
+
+/**
+ * @openapi
+ * /api/auth/verify-email:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Verify email
+ * description: >
+ * Verifies a registered user's email using the OTP sent during registration or resend.
+ * This endpoint is public and does not set a cookie.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - email
+ * - otp
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * otp:
+ * type: string
+ * example: "123456"
+ * responses:
+ * "200":
+ * description: Email verified successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Email verified successfully
+ * "400":
+ * description: >
+ * Missing email or OTP, email already verified, invalid OTP, or expired OTP
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid OTP
+ * "401":
+ * description: User not found for the given email (returned as invalid email or OTP)
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid email or OTP
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/resend-otp:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Resend OTP
+ * description: >
+ * Generates and emails a new verification OTP for an unverified user.
+ * This endpoint is public and does not set a cookie.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - email
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * responses:
+ * "200":
+ * description: OTP resent successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: OTP resent successfully. Please check your email.
+ * "400":
+ * description: Email is missing or already verified
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Email is already verified
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/forgot-password:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Forgot password
+ * description: >
+ * Sends a password-reset OTP to the user's email.
+ * This endpoint is public and does not set a cookie.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - email
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * responses:
+ * "200":
+ * description: Password-reset OTP sent to email
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: OTP sent to your email. Please check your inbox.
+ * "400":
+ * description: Email is required
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Email is required
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/reset-password:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Reset password
+ * description: >
+ * Resets the user password after validating the email OTP from forgot-password.
+ * This endpoint is public and does not set a cookie.
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - email
+ * - otp
+ * - newPassword
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * example: rahul@example.com
+ * otp:
+ * type: string
+ * example: "123456"
+ * newPassword:
+ * type: string
+ * format: password
+ * description: At least 8 characters with uppercase, lowercase, and a number
+ * example: NewPassword1
+ * responses:
+ * "200":
+ * description: Password reset successful
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Password reset successful
+ * "400":
+ * description: >
+ * Missing fields, weak password, invalid OTP, or expired OTP
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid OTP
+ * "404":
+ * description: User not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: User not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/logout:
+ * post:
+ * tags:
+ * - Auth
+ * summary: Logout
+ * description: >
+ * Clears the HTTP-only `token` cookie. This route does not use auth middleware,
+ * so it is public and does not require a valid cookie to succeed.
+ * responses:
+ * "200":
+ * description: Logout successful; `token` cookie cleared
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Logout successful
+ * "500":
+ * description: Logout failed
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Logout failed. Please try again.
+ */
+
+/**
+ * @openapi
+ * /api/auth/me:
+ * get:
+ * tags:
+ * - Auth
+ * summary: Get current user
+ * description: >
+ * Returns the authenticated user's profile.
+ * Requires a valid HTTP-only `token` cookie (auth middleware).
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Current user profile
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * user:
+ * type: object
+ * properties:
+ * id:
+ * type: string
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * phone:
+ * type: string
+ * roles:
+ * type: array
+ * items:
+ * type: string
+ * profileImage:
+ * type: string
+ * bio:
+ * type: string
+ * address:
+ * type: string
+ * dob:
+ * type: string
+ * format: date-time
+ * nullable: true
+ * gender:
+ * type: string
+ * enum: [male, female, other]
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * isEmailVerified:
+ * type: boolean
+ * isActive:
+ * type: boolean
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Account deactivated
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Your account has been deactivated. Please contact support.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/me:
+ * patch:
+ * tags:
+ * - Auth
+ * summary: Update current user profile
+ * description: >
+ * Partially updates the authenticated user's profile fields via JSON.
+ * Profile image upload is a separate endpoint (`PATCH /api/auth/me/avatar`)
+ * and is not part of this route. Requires a valid HTTP-only `token` cookie.
+ * security:
+ * - cookieAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * name:
+ * type: string
+ * phone:
+ * type: string
+ * example: "9876543210"
+ * bio:
+ * type: string
+ * address:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * dob:
+ * type: string
+ * format: date
+ * nullable: true
+ * gender:
+ * type: string
+ * enum: [male, female, other]
+ * responses:
+ * "200":
+ * description: Profile updated
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Profile updated
+ * user:
+ * type: object
+ * properties:
+ * id:
+ * type: string
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * phone:
+ * type: string
+ * roles:
+ * type: array
+ * items:
+ * type: string
+ * profileImage:
+ * type: string
+ * bio:
+ * type: string
+ * address:
+ * type: string
+ * dob:
+ * type: string
+ * format: date-time
+ * nullable: true
+ * gender:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * isEmailVerified:
+ * type: boolean
+ * isActive:
+ * type: boolean
+ * "400":
+ * description: >
+ * No valid fields, validation error, or duplicate phone number
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: No valid profile fields to update
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Account disabled or deactivated
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Your account has been disabled. Please contact support.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/auth/become-provider:
+ * patch:
+ * tags:
+ * - Auth
+ * summary: Become provider
+ * description: >
+ * Adds the `provider` role to the authenticated customer.
+ * Requires a valid HTTP-only `token` cookie. No request body.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Provider role added
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Provider account created successfully
+ * roles:
+ * type: array
+ * items:
+ * type: string
+ * example: [customer, provider]
+ * "400":
+ * description: User is already a provider
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: You are already a provider
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Account deactivated
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Your account has been deactivated. Please contact support.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
diff --git a/backend/docs/availability.docs.js b/backend/docs/availability.docs.js
new file mode 100644
index 0000000000..65bd2667aa
--- /dev/null
+++ b/backend/docs/availability.docs.js
@@ -0,0 +1,429 @@
+/**
+ * Availability API documentation (OpenAPI).
+ * Keep route/controller files free of Swagger comments.
+ */
+
+/**
+ * @openapi
+ * /api/availability/create:
+ * post:
+ * tags:
+ * - Availability
+ * summary: Create availability slot
+ * description: >
+ * Creates an availability slot for a venue owned by the authenticated provider.
+ * Requires a valid `token` cookie and the `provider` role.
+ * Times use 12-hour format such as `09:00 AM`.
+ * security:
+ * - cookieAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - venueId
+ * - date
+ * - slotLabel
+ * - startTime
+ * - endTime
+ * properties:
+ * venueId:
+ * type: string
+ * description: MongoDB ObjectId of the venue
+ * date:
+ * type: string
+ * format: date
+ * slotLabel:
+ * type: string
+ * enum: [morning, evening, night, fullday]
+ * startTime:
+ * type: string
+ * example: "09:00 AM"
+ * endTime:
+ * type: string
+ * example: "12:00 PM"
+ * responses:
+ * "201":
+ * description: Availability created successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Availability created successfully
+ * data:
+ * type: object
+ * description: Created availability slot document
+ * "400":
+ * description: >
+ * Missing fields, invalid venue ID/date/time, past date,
+ * end time not after start time, or slot already exists
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Please fill all required fields
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: You can only manage your own venue
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/availability/{venueId}:
+ * get:
+ * tags:
+ * - Availability
+ * summary: List venue availability
+ * description: >
+ * Returns all availability slots for a venue, sorted by date and start time.
+ * Response may be served from Redis cache when available.
+ * This endpoint is public and does not require authentication.
+ * parameters:
+ * - in: path
+ * name: venueId
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue
+ * responses:
+ * "200":
+ * description: Availability slots retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * count:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * _id:
+ * type: string
+ * venueId:
+ * type: string
+ * date:
+ * type: string
+ * format: date-time
+ * slotLabel:
+ * type: string
+ * enum: [morning, evening, night, fullday]
+ * startTime:
+ * type: string
+ * endTime:
+ * type: string
+ * isBooked:
+ * type: boolean
+ * bookingId:
+ * type: string
+ * nullable: true
+ * isActive:
+ * type: boolean
+ * createdAt:
+ * type: string
+ * format: date-time
+ * updatedAt:
+ * type: string
+ * format: date-time
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/availability/deactivate/{slotId}:
+ * patch:
+ * tags:
+ * - Availability
+ * summary: Deactivate availability slot
+ * description: >
+ * Sets `isActive` to false for a slot on a venue owned by the authenticated provider.
+ * Booked slots cannot be deactivated. Requires a valid `token` cookie and the `provider` role.
+ * No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: slotId
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the availability slot
+ * responses:
+ * "200":
+ * description: Slot deactivated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Slot deactivated successfully
+ * data:
+ * type: object
+ * description: Updated availability slot document
+ * "400":
+ * description: Invalid slot ID, or slot is booked
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Booked slots cannot be deactivated
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized
+ * "404":
+ * description: Slot or venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Slot not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/availability/activate/{slotId}:
+ * patch:
+ * tags:
+ * - Availability
+ * summary: Activate availability slot
+ * description: >
+ * Sets `isActive` to true for a slot on a venue owned by the authenticated provider.
+ * Requires a valid `token` cookie and the `provider` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: slotId
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the availability slot
+ * responses:
+ * "200":
+ * description: Slot activated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Slot activated successfully
+ * data:
+ * type: object
+ * description: Updated availability slot document
+ * "400":
+ * description: Invalid slot ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid slot ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized
+ * "404":
+ * description: Slot or venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Slot not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
diff --git a/backend/docs/bookings.docs.js b/backend/docs/bookings.docs.js
new file mode 100644
index 0000000000..ca39c1d84c
--- /dev/null
+++ b/backend/docs/bookings.docs.js
@@ -0,0 +1,252 @@
+/**
+ * Bookings API documentation (OpenAPI).
+ * Keep route/controller files free of Swagger comments.
+ *
+ * Note: Booking creation is not handled under /api/bookings.
+ * Bookings are created via the Payments module (payment verification).
+ */
+
+/**
+ * @openapi
+ * /api/bookings/my-bookings:
+ * get:
+ * tags:
+ * - Bookings
+ * summary: List my bookings
+ * description: >
+ * Returns all bookings for the authenticated user, sorted by newest first.
+ * Each booking populates `venueId` and `availabilityId`.
+ * Requires a valid `token` cookie. No request body.
+ * There is no `POST /api/bookings` endpoint; bookings are created through Payments.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: User bookings retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Fetched all bookings
+ * count:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * _id:
+ * type: string
+ * bookingReference:
+ * type: string
+ * userId:
+ * type: string
+ * venueId:
+ * type: object
+ * description: Populated venue document
+ * availabilityId:
+ * type: object
+ * description: Populated availability slot document
+ * amount:
+ * type: number
+ * bookingStatus:
+ * type: string
+ * enum: [confirmed, cancelled]
+ * paymentMethod:
+ * type: string
+ * nullable: true
+ * razorpayOrderId:
+ * type: string
+ * nullable: true
+ * paymentId:
+ * type: string
+ * nullable: true
+ * paymentStatus:
+ * type: string
+ * enum: [pending, paid, failed, refunded]
+ * contactPhone:
+ * type: string
+ * bookedAt:
+ * type: string
+ * format: date-time
+ * cancelledAt:
+ * type: string
+ * format: date-time
+ * nullable: true
+ * createdAt:
+ * type: string
+ * format: date-time
+ * updatedAt:
+ * type: string
+ * format: date-time
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Account deactivated
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Your account has been deactivated. Please contact support.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/bookings/provider-bookings:
+ * get:
+ * tags:
+ * - Bookings
+ * summary: List provider venue bookings
+ * description: >
+ * Returns all bookings for venues owned by the authenticated provider,
+ * sorted by newest first. Populates `userId` (name, email, phone),
+ * `venueId`, and `availabilityId`.
+ * Requires a valid `token` cookie and the `provider` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Provider bookings retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * count:
+ * type: integer
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * _id:
+ * type: string
+ * bookingReference:
+ * type: string
+ * userId:
+ * type: object
+ * description: Populated user subset (name, email, phone)
+ * properties:
+ * name:
+ * type: string
+ * email:
+ * type: string
+ * phone:
+ * type: string
+ * venueId:
+ * type: object
+ * description: Populated venue document
+ * availabilityId:
+ * type: object
+ * description: Populated availability slot document
+ * amount:
+ * type: number
+ * bookingStatus:
+ * type: string
+ * enum: [confirmed, cancelled]
+ * paymentMethod:
+ * type: string
+ * nullable: true
+ * razorpayOrderId:
+ * type: string
+ * nullable: true
+ * paymentId:
+ * type: string
+ * nullable: true
+ * paymentStatus:
+ * type: string
+ * enum: [pending, paid, failed, refunded]
+ * contactPhone:
+ * type: string
+ * bookedAt:
+ * type: string
+ * format: date-time
+ * cancelledAt:
+ * type: string
+ * format: date-time
+ * nullable: true
+ * createdAt:
+ * type: string
+ * format: date-time
+ * updatedAt:
+ * type: string
+ * format: date-time
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Account deactivated, or authenticated user is not a provider
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied. You do not have the permission to perform this action.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
diff --git a/backend/docs/venues.docs.js b/backend/docs/venues.docs.js
new file mode 100644
index 0000000000..5ea7ea377e
--- /dev/null
+++ b/backend/docs/venues.docs.js
@@ -0,0 +1,969 @@
+/**
+ * Venues API documentation (OpenAPI).
+ * Keep route/controller files free of Swagger comments.
+ */
+
+/**
+ * @openapi
+ * /api/venues:
+ * get:
+ * tags:
+ * - Venues
+ * summary: List active venues
+ * description: >
+ * Returns all active venues, sorted by newest first.
+ * Response may be served from Redis cache when available.
+ * This endpoint is public and does not require authentication.
+ * The controller does not accept query parameters.
+ * responses:
+ * "200":
+ * description: Active venues retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * count:
+ * type: integer
+ * example: 3
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * _id:
+ * type: string
+ * ownerId:
+ * type: string
+ * title:
+ * type: string
+ * description:
+ * type: string
+ * category:
+ * type: string
+ * enum:
+ * - wedding
+ * - corporate
+ * - birthday
+ * - party
+ * - function
+ * - photoshoot
+ * - other
+ * images:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * url:
+ * type: string
+ * public_id:
+ * type: string
+ * coverImage:
+ * type: object
+ * properties:
+ * url:
+ * type: string
+ * public_id:
+ * type: string
+ * venueType:
+ * type: string
+ * enum: [offline, online, hybrid]
+ * indoorOutdoor:
+ * type: string
+ * enum: [indoor, outdoor, both]
+ * price:
+ * type: number
+ * pricingUnit:
+ * type: string
+ * enum: [perhour, perday]
+ * capacity:
+ * type: number
+ * amenities:
+ * type: array
+ * items:
+ * type: string
+ * rules:
+ * type: array
+ * items:
+ * type: string
+ * address:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * pincode:
+ * type: string
+ * location:
+ * type: object
+ * properties:
+ * latitude:
+ * type: number
+ * nullable: true
+ * longitude:
+ * type: number
+ * nullable: true
+ * averageRating:
+ * type: number
+ * totalReviews:
+ * type: number
+ * isActive:
+ * type: boolean
+ * createdAt:
+ * type: string
+ * format: date-time
+ * updatedAt:
+ * type: string
+ * format: date-time
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/venues/{venueId}:
+ * get:
+ * tags:
+ * - Venues
+ * summary: Get venue by ID
+ * description: >
+ * Returns a single active venue by ID.
+ * Response may be served from Redis cache when available.
+ * This endpoint is public and does not require authentication.
+ * parameters:
+ * - in: path
+ * name: venueId
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue
+ * responses:
+ * "200":
+ * description: Venue retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * data:
+ * type: object
+ * properties:
+ * _id:
+ * type: string
+ * ownerId:
+ * type: string
+ * title:
+ * type: string
+ * description:
+ * type: string
+ * category:
+ * type: string
+ * enum:
+ * - wedding
+ * - corporate
+ * - birthday
+ * - party
+ * - function
+ * - photoshoot
+ * - other
+ * images:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * url:
+ * type: string
+ * public_id:
+ * type: string
+ * coverImage:
+ * type: object
+ * properties:
+ * url:
+ * type: string
+ * public_id:
+ * type: string
+ * venueType:
+ * type: string
+ * enum: [offline, online, hybrid]
+ * indoorOutdoor:
+ * type: string
+ * enum: [indoor, outdoor, both]
+ * price:
+ * type: number
+ * pricingUnit:
+ * type: string
+ * enum: [perhour, perday]
+ * capacity:
+ * type: number
+ * amenities:
+ * type: array
+ * items:
+ * type: string
+ * rules:
+ * type: array
+ * items:
+ * type: string
+ * address:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * pincode:
+ * type: string
+ * location:
+ * type: object
+ * properties:
+ * latitude:
+ * type: number
+ * nullable: true
+ * longitude:
+ * type: number
+ * nullable: true
+ * averageRating:
+ * type: number
+ * totalReviews:
+ * type: number
+ * isActive:
+ * type: boolean
+ * createdAt:
+ * type: string
+ * format: date-time
+ * updatedAt:
+ * type: string
+ * format: date-time
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "404":
+ * description: Venue not found or not active
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/venues/create:
+ * post:
+ * tags:
+ * - Venues
+ * summary: Create venue
+ * description: >
+ * Creates a new venue for the authenticated provider.
+ * Requires a valid `token` cookie and the `provider` role.
+ * Accepts multipart/form-data with venue fields and up to 5 image files
+ * under the field name `images`. At least one image is required.
+ * `amenities` and `rules` must be JSON array strings when provided.
+ * security:
+ * - cookieAuth: []
+ * requestBody:
+ * required: true
+ * content:
+ * multipart/form-data:
+ * schema:
+ * type: object
+ * required:
+ * - title
+ * - description
+ * - category
+ * - price
+ * - capacity
+ * - address
+ * - images
+ * properties:
+ * title:
+ * type: string
+ * description:
+ * type: string
+ * category:
+ * type: string
+ * description: Venue category (validated/normalized by the controller)
+ * venueType:
+ * type: string
+ * indoorOutdoor:
+ * type: string
+ * price:
+ * type: number
+ * pricingUnit:
+ * type: string
+ * capacity:
+ * type: integer
+ * amenities:
+ * type: string
+ * description: JSON string of a string array, e.g. '["wifi","parking"]'
+ * rules:
+ * type: string
+ * description: JSON string of a string array
+ * address:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * pincode:
+ * type: string
+ * latitude:
+ * type: number
+ * longitude:
+ * type: number
+ * images:
+ * type: array
+ * items:
+ * type: string
+ * format: binary
+ * description: One to five image files (multer field name `images`, max 5)
+ * responses:
+ * "201":
+ * description: Venue created successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue created successfully.
+ * data:
+ * type: object
+ * description: Created venue document
+ * "400":
+ * description: >
+ * Missing required fields, invalid price/capacity/coordinates/category,
+ * invalid amenities/rules JSON, or no images uploaded
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Please fill all required fields.
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Authenticated user is not a provider
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied. You do not have the permission to perform this action.
+ * "500":
+ * description: Failed to create venue
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Failed to create venue. Please try again.
+ */
+
+/**
+ * @openapi
+ * /api/venues/update/{id}:
+ * put:
+ * tags:
+ * - Venues
+ * summary: Update venue
+ * description: >
+ * Updates an existing venue owned by the authenticated provider.
+ * Requires a valid `token` cookie and the `provider` role.
+ * Accepts multipart/form-data. All form fields are optional; only provided
+ * fields are updated. Optional new images use multer field name `images`
+ * (up to 5). If new images are uploaded, they replace existing venue images.
+ * `amenities` and `rules` must be JSON array strings when provided.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue to update
+ * requestBody:
+ * required: true
+ * content:
+ * multipart/form-data:
+ * schema:
+ * type: object
+ * properties:
+ * title:
+ * type: string
+ * description:
+ * type: string
+ * category:
+ * type: string
+ * venueType:
+ * type: string
+ * indoorOutdoor:
+ * type: string
+ * price:
+ * type: number
+ * capacity:
+ * type: integer
+ * amenities:
+ * type: string
+ * description: JSON string of a string array
+ * rules:
+ * type: string
+ * description: JSON string of a string array
+ * address:
+ * type: string
+ * city:
+ * type: string
+ * state:
+ * type: string
+ * pincode:
+ * type: string
+ * latitude:
+ * type: number
+ * longitude:
+ * type: number
+ * images:
+ * type: array
+ * items:
+ * type: string
+ * format: binary
+ * description: Optional replacement images (multer field name `images`, max 5)
+ * responses:
+ * "200":
+ * description: Venue updated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue updated successfully
+ * data:
+ * type: object
+ * description: Updated venue document
+ * "400":
+ * description: >
+ * Invalid venue ID, category, price, capacity, coordinates,
+ * or amenities/rules JSON
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/venues/deactivate/{id}:
+ * patch:
+ * tags:
+ * - Venues
+ * summary: Deactivate venue
+ * description: >
+ * Soft-deletes a venue by setting `isActive` to false.
+ * There is no hard-delete venue endpoint in this API.
+ * Requires a valid `token` cookie and the `provider` role.
+ * Only the venue owner can deactivate it. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue to deactivate
+ * responses:
+ * "200":
+ * description: Venue deactivated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue deactivated successfully
+ * data:
+ * type: object
+ * description: Updated venue document with isActive set to false
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/venues/my-venues:
+ * get:
+ * tags:
+ * - Venues
+ * summary: List my venues
+ * description: >
+ * Returns all venues owned by the authenticated provider, including inactive ones,
+ * sorted by newest first. Requires a valid `token` cookie and the `provider` role.
+ * security:
+ * - cookieAuth: []
+ * responses:
+ * "200":
+ * description: Provider venues retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * data:
+ * type: array
+ * items:
+ * type: object
+ * description: Venue document
+ * count:
+ * type: integer
+ * example: 2
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Authenticated user is not a provider
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied. You do not have the permission to perform this action.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ */
+
+/**
+ * @openapi
+ * /api/venues/provider/{id}:
+ * get:
+ * tags:
+ * - Venues
+ * summary: Get owned venue by ID
+ * description: >
+ * Returns a single venue by ID for the authenticated provider who owns it.
+ * Unlike the public detail route, this can return inactive venues owned by the provider.
+ * Requires a valid `token` cookie and the `provider` role.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue
+ * responses:
+ * "200":
+ * description: Venue retrieved successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * data:
+ * type: object
+ * description: Venue document
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied.
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found.
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
+
+/**
+ * @openapi
+ * /api/venues/activate/{id}:
+ * patch:
+ * tags:
+ * - Venues
+ * summary: Activate venue
+ * description: >
+ * Sets `isActive` to true for a venue owned by the authenticated provider.
+ * Requires a valid `token` cookie and the `provider` role. No request body.
+ * security:
+ * - cookieAuth: []
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: string
+ * description: MongoDB ObjectId of the venue to activate
+ * responses:
+ * "200":
+ * description: Venue activated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: true
+ * message:
+ * type: string
+ * example: Venue activated successfully
+ * data:
+ * type: object
+ * description: Updated venue document with isActive set to true
+ * "400":
+ * description: Invalid venue ID
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Invalid venue ID
+ * "401":
+ * description: Missing, invalid, or expired token cookie
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Unauthorized access. Please login.
+ * "403":
+ * description: Not a provider, or not the venue owner
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Access denied
+ * "404":
+ * description: Venue not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Venue not found
+ * "500":
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * example: false
+ * message:
+ * type: string
+ * example: Something went wrong. Please try again later.
+ */
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000000..e8c6a94eac
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,2705 @@
+{
+ "name": "backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "backend",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "cloudinary": "^2.10.0",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "express-validator": "^7.3.2",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.7.0",
+ "multer": "^2.2.0",
+ "nodemailer": "^9.0.0",
+ "razorpay": "^2.9.6",
+ "redis": "^6.1.0",
+ "sib-api-v3-sdk": "^8.5.0",
+ "swagger-jsdoc": "^6.3.0",
+ "swagger-ui-express": "^5.0.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+ },
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
+ "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15",
+ "js-yaml": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/philsturgeon"
+ }
+ },
+ "node_modules/@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
+ "license": "MIT"
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
+ "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "14.0.1",
+ "@apidevtools/openapi-schemas": "^2.1.0",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "ajv": "^8.17.1",
+ "ajv-draft-04": "^1.0.0",
+ "call-me-maybe": "^1.0.2"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
+ "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@redis/bloom": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.1.0.tgz",
+ "integrity": "sha512-Rzascjd9J9bJsM45T/Z9CTg1QY/B63B6YO8QorLVMeXnbBDsKiSCVR/+GQ061hYPk8FpTzWmPY8tAv2sT+JEtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.0.0"
+ },
+ "peerDependencies": {
+ "@redis/client": "^6.1.0"
+ }
+ },
+ "node_modules/@redis/client": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/client/-/client-6.1.0.tgz",
+ "integrity": "sha512-7u1LefkezJF0HESlhO7ZFLEPfyY+NejP3SGv+Z4pGaT3oM5GVVLa0u3f4rDLUrcw+SRo8IlX9Y8JAONeDdg1Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "cluster-key-slot": "1.1.2"
+ },
+ "engines": {
+ "node": ">= 20.0.0"
+ },
+ "peerDependencies": {
+ "@node-rs/xxhash": "^1.1.0",
+ "@opentelemetry/api": ">=1 <2"
+ },
+ "peerDependenciesMeta": {
+ "@node-rs/xxhash": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@redis/json": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/json/-/json-6.1.0.tgz",
+ "integrity": "sha512-/GFjQA6bu5pG9ClCJAI5Xx4bNXe7UTpxBBlIupBNTrn1+nY860apGnYJuaSCDV2BmEbTidpa7O2qa28oxKx+rg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.0.0"
+ },
+ "peerDependencies": {
+ "@redis/client": "^6.1.0"
+ }
+ },
+ "node_modules/@redis/search": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/search/-/search-6.1.0.tgz",
+ "integrity": "sha512-kS5agg+3yZbrdrt8omrew7FLCD8eOm7tarG1CROekPBRe+QGDR9aOpnHIQaYsYi6wPRTH70nQiF06AIjgURefQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.0.0"
+ },
+ "peerDependencies": {
+ "@redis/client": "^6.1.0"
+ }
+ },
+ "node_modules/@redis/time-series": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.1.0.tgz",
+ "integrity": "sha512-uIDBtV8MmG/xpJsRqbGSO4iX6ryj37MLMP82lRpFvI7ykAVe5GyqgxigEbU+uZNv9kDPNMKw3dvI/S/J1BNBzA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.0.0"
+ },
+ "peerDependencies": {
+ "@redis/client": "^6.1.0"
+ }
+ },
+ "node_modules/@scarf/scarf": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
+ "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/axios/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/axios/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
+ "license": "MIT"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cloudinary": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz",
+ "integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.23"
+ },
+ "engines": {
+ "node": ">=9"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
+ "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/concat-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-validator": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz",
+ "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.18.1",
+ "validator": "~13.15.23"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/express/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz",
+ "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/formidable": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
+ "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
+ "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
+ "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.1.1",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
+ "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^9.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz",
+ "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.7.0.tgz",
+ "integrity": "sha512-pkrLZ6U41pD4Ai0ju/FYL7o5I5k+rV3RZINQTG937hbhnLGKRuqqYm1Dlt/kTQ+M4FHijzV6JawzsdHKRGt7QA==",
+ "license": "MIT",
+ "dependencies": {
+ "kareem": "3.3.0",
+ "mongodb": "~7.2",
+ "mpath": "0.9.0",
+ "mquery": "6.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
+ "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz",
+ "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.0.tgz",
+ "integrity": "sha512-tbPTid7d/p9jAA8CRZ3iomvrMaST0o6NYuY7v6JQZHpPRZ61mLFSPKYd7342NtOFuej9/+L48SOIxwfu2uDvtw==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "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/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
+ "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/razorpay": {
+ "version": "2.9.6",
+ "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.6.tgz",
+ "integrity": "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redis": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-6.1.0.tgz",
+ "integrity": "sha512-0kvUPM8RHP/ZMa0xYaDTcG5e8tIGW6kz6MToVT0V8iOnk6bkXp2jncGRGe2bZEk41lZwiDspUqjZCSk5ohjcKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@redis/bloom": "6.1.0",
+ "@redis/client": "6.1.0",
+ "@redis/json": "6.1.0",
+ "@redis/search": "6.1.0",
+ "@redis/time-series": "6.1.0"
+ },
+ "engines": {
+ "node": ">= 20.0.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "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==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sib-api-v3-sdk": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/sib-api-v3-sdk/-/sib-api-v3-sdk-8.5.0.tgz",
+ "integrity": "sha512-6Ratp5kLN/rEEvk4XVIQ4L8IrCIrcfE9m1HjvHz/WepC+CVXPsjOlgRcK/jQjpN5kC+dmhDAqrTo1OtnF6i1wA==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT",
+ "dependencies": {
+ "querystring": "0.2.0",
+ "superagent": "3.7.0"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/superagent": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.7.0.tgz",
+ "integrity": "sha512-/8trxO6NbLx4YXb7IeeFTSmsQ35pQBiTBsLNvobZx7qBzBeHYvKCyIIhW2gNcWbLzYxPAjdgFbiepd8ypwC0Gw==",
+ "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net",
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.2.0",
+ "cookiejar": "^2.1.0",
+ "debug": "^3.1.0",
+ "extend": "^3.0.0",
+ "form-data": "^2.3.1",
+ "formidable": "^1.1.1",
+ "methods": "^1.1.1",
+ "mime": "^1.4.1",
+ "qs": "^6.5.1",
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/superagent/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/swagger-jsdoc": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz",
+ "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^12.1.0",
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "11.1.0",
+ "lodash.mergewith": "^4.6.2",
+ "yaml": "2.0.0-1"
+ },
+ "bin": {
+ "swagger-jsdoc": "bin/swagger-jsdoc.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/swagger-ui-dist": {
+ "version": "5.32.10",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.10.tgz",
+ "integrity": "sha512-RTrAPrp/J5H/H8JvRBOpR3qCsIhntOweFwaasR0TD2Y6a/7VUYYFnjyB3MFe1niAg3VkEQGT9pVZ0r225jvJJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@scarf/scarf": "=1.4.0"
+ }
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
+ "license": "MIT",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/validator": {
+ "version": "13.15.35",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
+ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000000..e9ba7f6df0
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "backend",
+ "version": "1.0.0",
+ "main": "index.js",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node src/index.js",
+ "dev": "nodemon src/index.js",
+ "seed:admin": "node scripts/seedAdmin.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "cloudinary": "^2.10.0",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "express-validator": "^7.3.2",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.7.0",
+ "multer": "^2.2.0",
+ "nodemailer": "^9.0.0",
+ "razorpay": "^2.9.6",
+ "redis": "^6.1.0",
+ "sib-api-v3-sdk": "^8.5.0",
+ "swagger-jsdoc": "^6.3.0",
+ "swagger-ui-express": "^5.0.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+}
diff --git a/backend/scripts/seedAdmin.js b/backend/scripts/seedAdmin.js
new file mode 100644
index 0000000000..fb1a27691e
--- /dev/null
+++ b/backend/scripts/seedAdmin.js
@@ -0,0 +1,93 @@
+import dotenv from "dotenv";
+import mongoose from "mongoose";
+import bcrypt from "bcrypt";
+import userModel from "../src/models/userModel.js";
+
+dotenv.config();
+
+const seedAdmin = async () => {
+ const { ADMIN_NAME, ADMIN_EMAIL, ADMIN_PHONE, ADMIN_PASSWORD, MONGO_URI } =
+ process.env;
+
+ if (!ADMIN_NAME || !ADMIN_EMAIL || !ADMIN_PHONE || !ADMIN_PASSWORD) {
+ console.error(
+ "Missing required env vars: ADMIN_NAME, ADMIN_EMAIL, ADMIN_PHONE, ADMIN_PASSWORD"
+ );
+ process.exit(1);
+ }
+
+ if (!MONGO_URI) {
+ console.error("Missing required env var: MONGO_URI");
+ process.exit(1);
+ }
+
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
+ if (!passwordRegex.test(ADMIN_PASSWORD)) {
+ console.error(
+ "ADMIN_PASSWORD must be at least 8 characters and include uppercase, lowercase, and a number"
+ );
+ process.exit(1);
+ }
+
+ const phoneRegex = /^[6-9]\d{9}$/;
+ if (!phoneRegex.test(ADMIN_PHONE)) {
+ console.error("ADMIN_PHONE must be a valid 10-digit Indian mobile number");
+ process.exit(1);
+ }
+
+ try {
+ await mongoose.connect(MONGO_URI);
+ console.log("Database connected");
+
+ const existingAdmin = await userModel.findOne({
+ email: ADMIN_EMAIL.toLowerCase().trim(),
+ roles: "admin",
+ });
+
+ if (existingAdmin) {
+ console.log(`Admin already exists: ${existingAdmin.email}`);
+ await mongoose.disconnect();
+ process.exit(0);
+ }
+
+ const existingEmail = await userModel.findOne({
+ email: ADMIN_EMAIL.toLowerCase().trim(),
+ });
+
+ if (existingEmail) {
+ if (!existingEmail.roles.includes("admin")) {
+ existingEmail.roles.push("admin");
+ existingEmail.isEmailVerified = true;
+ existingEmail.isActive = true;
+ await existingEmail.save();
+ console.log(`Admin role added to existing user: ${existingEmail.email}`);
+ } else {
+ console.log(`Admin already exists: ${existingEmail.email}`);
+ }
+ await mongoose.disconnect();
+ process.exit(0);
+ }
+
+ const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
+
+ const admin = await userModel.create({
+ name: ADMIN_NAME.trim(),
+ email: ADMIN_EMAIL.toLowerCase().trim(),
+ phone: ADMIN_PHONE.trim(),
+ password: hashedPassword,
+ roles: ["admin"],
+ isEmailVerified: true,
+ isActive: true,
+ });
+
+ console.log(`Admin created successfully: ${admin.email}`);
+ await mongoose.disconnect();
+ process.exit(0);
+ } catch (error) {
+ console.error("Seed admin error:", error.message);
+ await mongoose.disconnect();
+ process.exit(1);
+ }
+};
+
+seedAdmin();
diff --git a/backend/src/config/cloudinary.js b/backend/src/config/cloudinary.js
new file mode 100644
index 0000000000..6ea3fc0199
--- /dev/null
+++ b/backend/src/config/cloudinary.js
@@ -0,0 +1,12 @@
+import {v2 as cloudinary} from 'cloudinary';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+cloudinary.config({
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
+ api_key: process.env.CLOUDINARY_API_KEY,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+});
+
+export default cloudinary;
\ No newline at end of file
diff --git a/backend/src/config/db.js b/backend/src/config/db.js
new file mode 100644
index 0000000000..de0c4dd081
--- /dev/null
+++ b/backend/src/config/db.js
@@ -0,0 +1,13 @@
+import mongoose from "mongoose";
+
+const connection = async () => {
+ try {
+ await mongoose.connect(process.env.MONGO_URI);
+ console.log("Database connected successfully");
+ } catch (error) {
+ console.error("Database connection failed:", error);
+ process.exit(1);
+ };
+}
+
+export default connection;
\ No newline at end of file
diff --git a/backend/src/config/redis.js b/backend/src/config/redis.js
new file mode 100644
index 0000000000..edf2dfe823
--- /dev/null
+++ b/backend/src/config/redis.js
@@ -0,0 +1,33 @@
+import { createClient } from "redis";
+
+let redisClient = null;
+
+const connectRedis = async () => {
+ try {
+ if (!redisClient) {
+ redisClient = createClient({
+ username: process.env.REDIS_USERNAME,
+ password: process.env.REDIS_PASSWORD,
+ socket: {
+ host: process.env.REDIS_HOST,
+ port: Number(process.env.REDIS_PORT),
+ },
+ });
+
+ redisClient.on("error", (err) => {
+ console.error("Redis Client Error:", err);
+ });
+ }
+
+ if (!redisClient.isOpen) {
+ await redisClient.connect();
+ }
+
+ console.log("Redis connected successfully");
+ } catch (error) {
+ console.error("Redis connection failed:", error);
+ }
+};
+
+export { connectRedis, redisClient };
+export { redisClient as default };
diff --git a/backend/src/config/swagger.js b/backend/src/config/swagger.js
new file mode 100644
index 0000000000..c36344fe11
--- /dev/null
+++ b/backend/src/config/swagger.js
@@ -0,0 +1,50 @@
+import path from "path";
+import { fileURLToPath } from "url";
+import swaggerJsdoc from "swagger-jsdoc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const options = {
+ definition: {
+ openapi: "3.0.0",
+ info: {
+ title: "BookMyVenue API",
+ version: "1.0.0",
+ description:
+ "REST API for BookMyVenue, a MERN-based venue booking platform. Supports customer, provider, and admin workflows with JWT cookie authentication, venue management, availability scheduling, bookings, and Razorpay payment integration.",
+ },
+ servers: [
+ {
+ url: "http://localhost:5000",
+ description: "Local development server",
+ },
+ ],
+ tags: [
+ { name: "Auth", description: "Authentication and user profile" },
+ { name: "Venues", description: "Venue listing and management" },
+ { name: "Availability", description: "Venue availability slots" },
+ { name: "Bookings", description: "Booking listings" },
+ { name: "Payments", description: "Razorpay payment and order flow" },
+ { name: "Admin", description: "Admin dashboard and management" },
+ ],
+ components: {
+ securitySchemes: {
+ cookieAuth: {
+ type: "apiKey",
+ in: "cookie",
+ name: "token",
+ description:
+ "JWT access token stored in an HTTP-only cookie named `token`. Log in via POST /api/auth/login first, then authenticated requests send this cookie automatically.",
+ },
+ },
+ },
+ },
+ // Scan documentation-only modules (not routes/controllers).
+ // Forward slashes required so glob matching works on Windows.
+ apis: [path.join(__dirname, "../../docs/**/*.docs.js").replace(/\\/g, "/")],
+};
+
+const swaggerSpec = swaggerJsdoc(options);
+
+export default swaggerSpec;
diff --git a/backend/src/controllers/adminBookingController.js b/backend/src/controllers/adminBookingController.js
new file mode 100644
index 0000000000..555de03dab
--- /dev/null
+++ b/backend/src/controllers/adminBookingController.js
@@ -0,0 +1,101 @@
+import bookingModel from "../models/bookingModel.js";
+import parsePagination from "../utils/parsePagination.js";
+import mongoose from "mongoose";
+
+const buildBookingFilter = (query) => {
+ const filter = {};
+
+ if (query.bookingStatus) {
+ filter.bookingStatus = query.bookingStatus;
+ }
+
+ if (query.paymentStatus) {
+ filter.paymentStatus = query.paymentStatus;
+ }
+
+ if (query.venueId) {
+ filter.venueId = query.venueId;
+ }
+
+ if (query.userId) {
+ filter.userId = query.userId;
+ }
+
+ return filter;
+};
+
+const getBookings = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+ const filter = buildBookingFilter(req.query);
+
+ const [bookings, count] = await Promise.all([
+ bookingModel
+ .find(filter)
+ .populate("userId", "name email phone")
+ .populate("venueId", "title city coverImage")
+ .populate("availabilityId")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ bookingModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Bookings fetched successfully",
+ count,
+ page,
+ limit,
+ data: bookings,
+ });
+ } catch (error) {
+ console.error("Admin get bookings error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getBookingById = async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid booking ID",
+ });
+ }
+
+ const booking = await bookingModel
+ .findById(id)
+ .populate("userId", "name email phone profileImage")
+ .populate("venueId")
+ .populate("availabilityId");
+
+ if (!booking) {
+ return res.status(404).json({
+ success: false,
+ message: "Booking not found",
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "Booking fetched successfully",
+ data: booking,
+ });
+ } catch (error) {
+ console.error("Admin get booking by id error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+export { getBookings, getBookingById };
diff --git a/backend/src/controllers/adminDashboardController.js b/backend/src/controllers/adminDashboardController.js
new file mode 100644
index 0000000000..06be7bf970
--- /dev/null
+++ b/backend/src/controllers/adminDashboardController.js
@@ -0,0 +1,107 @@
+import userModel from "../models/userModel.js";
+import venueModel from "../models/venueModel.js";
+import bookingModel from "../models/bookingModel.js";
+import sanitizeUser from "../utils/sanitizeUser.js";
+import { MARKETPLACE_USER_FILTER, withMarketplaceUserFilter } from "../utils/marketplaceUserFilter.js";
+
+const RECENT_LIMIT = 5;
+
+const getDashboardStats = async (req, res) => {
+ try {
+ const [
+ totalUsers,
+ totalProviders,
+ totalVenues,
+ activeVenues,
+ confirmedPaidBookings,
+ revenueResult,
+ ] = await Promise.all([
+ userModel.countDocuments(MARKETPLACE_USER_FILTER),
+ userModel.countDocuments(withMarketplaceUserFilter({ roles: "provider" })),
+ venueModel.countDocuments(),
+ venueModel.countDocuments({ isActive: true }),
+ bookingModel.countDocuments({
+ bookingStatus: "confirmed",
+ paymentStatus: "paid",
+ }),
+ bookingModel.aggregate([
+ {
+ $match: {
+ bookingStatus: "confirmed",
+ paymentStatus: "paid",
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ totalRevenue: { $sum: "$amount" },
+ },
+ },
+ ]),
+ ]);
+
+ const totalRevenue = revenueResult[0]?.totalRevenue ?? 0;
+
+ return res.status(200).json({
+ success: true,
+ message: "Dashboard stats fetched successfully",
+ data: {
+ totalUsers,
+ totalProviders,
+ totalVenues,
+ activeVenues,
+ confirmedPaidBookings,
+ totalRevenue,
+ },
+ });
+ } catch (error) {
+ console.error("Admin dashboard stats error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getRecentActivity = async (req, res) => {
+ try {
+ const [users, venues, bookings] = await Promise.all([
+ userModel
+ .find(MARKETPLACE_USER_FILTER)
+ .select("-password -otp -otpExpiresAt")
+ .sort({ createdAt: -1 })
+ .limit(RECENT_LIMIT),
+ venueModel
+ .find()
+ .populate("ownerId", "name email profileImage")
+ .sort({ createdAt: -1 })
+ .limit(RECENT_LIMIT),
+ bookingModel
+ .find()
+ .populate("userId", "name email")
+ .populate("venueId", "title city")
+ .sort({ createdAt: -1 })
+ .limit(RECENT_LIMIT),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Recent activity fetched successfully",
+ data: {
+ users: users.map(sanitizeUser),
+ venues,
+ bookings,
+ },
+ });
+ } catch (error) {
+ console.error("Admin recent activity error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+export { getDashboardStats, getRecentActivity };
diff --git a/backend/src/controllers/adminPaymentController.js b/backend/src/controllers/adminPaymentController.js
new file mode 100644
index 0000000000..dd3745be81
--- /dev/null
+++ b/backend/src/controllers/adminPaymentController.js
@@ -0,0 +1,130 @@
+import paymentOrderModel from "../models/paymentOrderModel.js";
+import bookingModel from "../models/bookingModel.js";
+import parsePagination from "../utils/parsePagination.js";
+
+const getPaymentOrders = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+ const filter = {};
+
+ if (req.query.status) {
+ filter.status = req.query.status;
+ }
+
+ const [orders, count] = await Promise.all([
+ paymentOrderModel
+ .find(filter)
+ .populate("userId", "name email")
+ .populate("venueId", "title city")
+ .populate("availabilityId", "date slotLabel startTime endTime")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ paymentOrderModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Payment orders fetched successfully",
+ count,
+ page,
+ limit,
+ data: orders,
+ });
+ } catch (error) {
+ console.error("Admin get payment orders error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getPaymentHistory = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+
+ const filter = { paymentStatus: "paid" };
+
+ const [bookings, count] = await Promise.all([
+ bookingModel
+ .find(filter)
+ .select(
+ "paymentId razorpayOrderId amount paymentStatus bookingReference bookingStatus bookedAt createdAt userId venueId availabilityId"
+ )
+ .populate("userId", "name email phone")
+ .populate("venueId", "title city")
+ .populate("availabilityId", "date slotLabel startTime endTime")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ bookingModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Payment history fetched successfully",
+ count,
+ page,
+ limit,
+ data: bookings,
+ });
+ } catch (error) {
+ console.error("Admin get payment history error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getAbandonedPayments = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+
+ let hours = parseInt(req.query.hours, 10);
+ if (Number.isNaN(hours) || hours < 1) {
+ hours = 24;
+ }
+
+ const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
+
+ const filter = {
+ status: "created",
+ createdAt: { $lt: cutoff },
+ };
+
+ const [orders, count] = await Promise.all([
+ paymentOrderModel
+ .find(filter)
+ .populate("userId", "name email")
+ .populate("venueId", "title city")
+ .populate("availabilityId", "date slotLabel startTime endTime")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ paymentOrderModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Abandoned payment orders fetched successfully",
+ count,
+ page,
+ limit,
+ hours,
+ data: orders,
+ });
+ } catch (error) {
+ console.error("Admin get abandoned payments error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+export { getPaymentOrders, getPaymentHistory, getAbandonedPayments };
diff --git a/backend/src/controllers/adminUserController.js b/backend/src/controllers/adminUserController.js
new file mode 100644
index 0000000000..b1ae913535
--- /dev/null
+++ b/backend/src/controllers/adminUserController.js
@@ -0,0 +1,215 @@
+import mongoose from "mongoose";
+import userModel from "../models/userModel.js";
+import venueModel from "../models/venueModel.js";
+import bookingModel from "../models/bookingModel.js";
+import parsePagination from "../utils/parsePagination.js";
+import sanitizeUser from "../utils/sanitizeUser.js";
+import {
+ withMarketplaceUserFilter,
+ isPlatformOperator,
+ buildSearchRegex,
+} from "../utils/marketplaceUserFilter.js";
+
+const OPERATOR_ACCOUNT_MESSAGE =
+ "Platform operator accounts cannot be managed here";
+
+const buildUserFilter = (query) => {
+ const filter = {};
+
+ if (query.role) {
+ filter.roles = query.role;
+ }
+
+ if (query.isActive !== undefined && query.isActive !== "") {
+ if (query.isActive === "true") {
+ filter.isActive = true;
+ } else if (query.isActive === "false") {
+ filter.isActive = false;
+ }
+ }
+
+ if (query.search?.trim()) {
+ const searchRegex = buildSearchRegex(query.search);
+ filter.$or = [
+ { name: searchRegex },
+ { email: searchRegex },
+ { phone: searchRegex },
+ ];
+ }
+
+ return withMarketplaceUserFilter(filter);
+};
+
+const getUsers = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+ const filter = buildUserFilter(req.query);
+
+ const [users, count] = await Promise.all([
+ userModel
+ .find(filter)
+ .select("-password -otp -otpExpiresAt")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ userModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Users fetched successfully",
+ count,
+ page,
+ limit,
+ data: users.map(sanitizeUser),
+ });
+ } catch (error) {
+ console.error("Admin get users error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getUserById = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid user ID",
+ });
+ }
+
+ const user = await userModel
+ .findOne(withMarketplaceUserFilter({ _id: id }))
+ .select("-password -otp -otpExpiresAt");
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ const [bookingCount, venueCount] = await Promise.all([
+ bookingModel.countDocuments({ userId: user._id }),
+ venueModel.countDocuments({ ownerId: user._id }),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "User fetched successfully",
+ data: {
+ user: sanitizeUser(user),
+ bookingCount,
+ venueCount,
+ },
+ });
+ } catch (error) {
+ console.error("Admin get user by id error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const activateUser = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid user ID",
+ });
+ }
+ const user = await userModel.findById(id);
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ if (isPlatformOperator(user)) {
+ return res.status(400).json({
+ success: false,
+ message: OPERATOR_ACCOUNT_MESSAGE,
+ });
+ }
+
+ user.isActive = true;
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "User activated successfully",
+ data: sanitizeUser(user),
+ });
+ } catch (error) {
+ console.error("Admin activate user error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const deactivateUser = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid user ID",
+ });
+ }
+
+ if (req.user._id.toString() === id) {
+ return res.status(400).json({
+ success: false,
+ message: "You cannot deactivate your own account",
+ });
+ }
+
+ const user = await userModel.findById(id);
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ if (isPlatformOperator(user)) {
+ return res.status(400).json({
+ success: false,
+ message: OPERATOR_ACCOUNT_MESSAGE,
+ });
+ }
+
+ user.isActive = false;
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "User deactivated successfully",
+ data: sanitizeUser(user),
+ });
+ } catch (error) {
+ console.error("Admin deactivate user error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+export { getUsers, getUserById, activateUser, deactivateUser };
diff --git a/backend/src/controllers/adminVenueController.js b/backend/src/controllers/adminVenueController.js
new file mode 100644
index 0000000000..5df748ec13
--- /dev/null
+++ b/backend/src/controllers/adminVenueController.js
@@ -0,0 +1,187 @@
+import venueModel from "../models/venueModel.js";
+import parsePagination from "../utils/parsePagination.js";
+import { buildSearchRegex } from "../utils/marketplaceUserFilter.js";
+import { invalidateActiveVenuesCache, invalidateActiveVenueCache, } from "../utils/cache.js";
+import mongoose from "mongoose";
+
+const buildVenueFilter = (query) => {
+ const filter = {};
+
+ if (query.isActive !== undefined && query.isActive !== "") {
+ if (query.isActive === "true") {
+ filter.isActive = true;
+ } else if (query.isActive === "false") {
+ filter.isActive = false;
+ }
+ }
+
+ if (query.city?.trim()) {
+ filter.city = buildSearchRegex(query.city);
+ }
+
+ if (query.ownerId) {
+ filter.ownerId = query.ownerId;
+ }
+
+ if (query.search?.trim()) {
+ const searchRegex = buildSearchRegex(query.search);
+ filter.$or = [
+ { title: searchRegex },
+ { city: searchRegex },
+ { address: searchRegex },
+ ];
+ }
+
+ return filter;
+};
+
+const getVenues = async (req, res) => {
+ try {
+ const { page, limit, skip } = parsePagination(req.query);
+ const filter = buildVenueFilter(req.query);
+
+ const [venues, count] = await Promise.all([
+ venueModel
+ .find(filter)
+ .populate("ownerId", "name email profileImage")
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit),
+ venueModel.countDocuments(filter),
+ ]);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venues fetched successfully",
+ count,
+ page,
+ limit,
+ data: venues,
+ });
+ } catch (error) {
+ console.error("Admin get venues error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const getVenueById = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel
+ .findById(id)
+ .populate("ownerId", "name email profileImage phone");
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue fetched successfully",
+ data: venue,
+ });
+ } catch (error) {
+ console.error("Admin get venue by id error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const activateVenue = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ venue.isActive = true;
+ await venue.save();
+
+ await invalidateActiveVenuesCache();
+ await invalidateActiveVenueCache(id);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue activated successfully",
+ data: venue,
+ });
+ } catch (error) {
+ console.error("Admin activate venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+const deactivateVenue = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ venue.isActive = false;
+ await venue.save();
+
+ await invalidateActiveVenuesCache();
+ await invalidateActiveVenueCache(id);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue deactivated successfully",
+ data: venue,
+ });
+ } catch (error) {
+ console.error("Admin deactivate venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+export { getVenues, getVenueById, activateVenue, deactivateVenue };
diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js
new file mode 100644
index 0000000000..8cd8c00131
--- /dev/null
+++ b/backend/src/controllers/authController.js
@@ -0,0 +1,770 @@
+import bcrypt from "bcrypt";
+import userModel from "../models/userModel.js";
+import sendEmail from "../utils/sendEmail.js";
+import generateOtp from "../utils/generateOtp.js";
+import uploadToCloudinary from "../utils/uploadToCloudinary.js";
+import deleteFromCloudinary from "../utils/deleteFromCloudinary.js";
+import jwt from "jsonwebtoken";
+
+const register = async (req, res) => {
+ try {
+ const { name, email, phone, password } = req.body;
+
+ if (!name || !email || !phone || !password) {
+ return res.status(400).json({
+ success: false,
+ message: "All fields are required"
+ });
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid email format"
+ });
+ }
+
+ const phoneRegex = /^[6-9]\d{9}$/;
+
+ if (!phoneRegex.test(phone)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid phone number"
+ });
+ }
+
+ const passwordRegex =
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
+
+ if (!passwordRegex.test(password)) {
+ return res.status(400).json({
+ success: false,
+ message:
+ "Password must contain uppercase, lowercase and number",
+ });
+ }
+ if (password.length < 8) {
+ return res.status(400).json({
+ success: false,
+ message: "Password must be at least 8 characters long"
+ });
+ }
+
+ const existingEmail = await userModel.findOne({ email });
+ if (existingEmail) {
+ return res.status(400).json({
+ success: false,
+ message: "Email already exists"
+ });
+ }
+
+ const existingPhone = await userModel.findOne({ phone });
+ if (existingPhone) {
+ return res.status(400).json({
+ success: false,
+ message: "Phone number already exists"
+ });
+ }
+
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ const { otp, expiresAt } = generateOtp();
+
+
+ await sendEmail(email, otp,);
+ await userModel.create({
+ name,
+ email,
+ phone,
+ password: hashedPassword,
+ otp,
+ otpExpiresAt: expiresAt,
+ });
+
+
+ return res.status(201).json({
+ success: true,
+ message: "Registration successful. Please check your email for the verification OTP.",
+ });
+ } catch (error) {
+ console.error("Registration error:", error);
+ return res.status(500).json({
+ success: false,
+ message: "Registration failed. Please try again.",
+ });
+ }
+
+}
+
+//verify the regsterd users through otp
+
+const verifyEmail = async (req, res) => {
+ try {
+ const { email, otp } = req.body;
+
+ if (!email || !otp) {
+ return res.status(400).json({
+ success: false,
+ message: "Email and OTP are required"
+ });
+ }
+
+ const user = await userModel.findOne({ email });
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid email or OTP"
+ });
+ }
+
+ if (user.isEmailVerified) {
+ return res.status(400).json({
+ success: false,
+ message: "Email is already verified"
+ });
+ }
+
+ if (user.otp !== otp) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid OTP"
+ });
+ }
+
+ if (user.otpExpiresAt < new Date()) {
+ return res.status(400).json({
+ success: false,
+ message: "OTP has expired"
+ });
+ }
+
+ user.isEmailVerified = true;
+ user.otp = null;
+ user.otpExpiresAt = null;
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Email verified successfully",
+ });
+ } catch (error) {
+ console.error("Email verification error:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+
+}
+
+
+//resend otp in verify email page
+
+const resendOtp = async (req, res) => {
+ try {
+ const { email } = req.body;
+ if (!email) {
+ return res.status(400).json({
+ success: false,
+ message: "Email is required"
+ });
+ }
+
+ const user = await userModel.findOne({ email });
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ if (user.isEmailVerified) {
+ return res.status(400).json({
+ success: false,
+ message: "Email is already verified"
+ });
+ }
+
+ const { otp, expiresAt } = generateOtp();
+ user.otp = otp;
+ user.otpExpiresAt = expiresAt;
+ await user.save();
+
+ await sendEmail(email, otp);
+
+ return res.status(200).json({
+ success: true,
+ message: "OTP resent successfully. Please check your email.",
+ });
+ } catch (error) {
+ console.error("Resend OTP error:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+}
+
+//login of the user
+const login = async (req, res) => {
+
+ try {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({
+ success: false,
+ message: "Email and password are required"
+ });
+ }
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!emailRegex.test(email)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid email format"
+ });
+ }
+
+ const user = await userModel.findOne({ email });
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ if (!user.isEmailVerified) {
+ return res.status(400).json({
+ success: false,
+ message: "Please verify your email first"
+ });
+ }
+
+ if (!user.isActive) {
+ return res.status(403).json({
+ success: false,
+ message: "Your account has been disabled. Please contact support."
+ });
+ }
+
+ const isMatch = await bcrypt.compare(password, user.password);
+
+ if (!isMatch) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid password"
+ });
+ }
+
+ user.lastLogin = new Date();
+ await user.save();
+
+
+ const token = jwt.sign(
+ {
+ id: user._id,
+ roles: user.roles,
+ },
+ process.env.JWT_SECRET,
+ { expiresIn: process.env.JWT_EXPIRES_IN }
+ );
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite:
+ process.env.NODE_ENV === "production"
+ ? "none"
+ : "lax",
+ maxAge: 7 * 24 * 60 * 60 * 1000,
+ });
+
+
+ return res.status(200).json({
+ success: true,
+ message: "Login successful",
+ user: {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ roles: user.roles,
+ profileImage: user.profileImage,
+ },
+ });
+ } catch (error) {
+ console.error("Login error:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+
+
+}
+
+//change password
+const forgotPassword = async (req, res) => {
+
+ try {
+ const { email } = req.body;
+
+ if (!email) {
+ return res.status(400).json({
+ success: false,
+ message: "Email is required"
+ });
+ }
+
+ const user = await userModel.findOne({ email });
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ const { otp, expiresAt } = generateOtp();
+ user.otp = otp;
+ user.otpExpiresAt = expiresAt;
+ await user.save();
+
+ await sendEmail(user.email, otp, "reset");
+
+ return res.status(200).json({
+ success: true,
+ message: "OTP sent to your email. Please check your inbox.",
+ });
+ }
+ catch (error) {
+ console.error("Forgot password error:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+
+}
+
+const resetPassword = async (req, res) => {
+ try {
+ const { email, otp, newPassword } = req.body;
+
+ if (!email || !otp || !newPassword) {
+ return res.status(400).json({
+ success: false,
+ message: "Email, OTP and new password are required"
+ });
+ }
+
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
+ if (!passwordRegex.test(newPassword)) {
+ return res.status(400).json({
+ success: false,
+ message: "Password must contain 8 characters, uppercase, lowercase and number"
+ });
+ }
+
+ const user = await userModel.findOne({ email });
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ if (user.otp !== otp) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid OTP"
+ });
+ }
+
+ if (user.otpExpiresAt < new Date()) {
+ return res.status(400).json({
+ success: false,
+ message: "OTP has expired"
+ });
+ }
+
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
+ user.password = hashedPassword;
+ user.otp = null;
+ user.otpExpiresAt = null;
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Password reset successful",
+ });
+ } catch (error) {
+ console.error("Reset password error:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+
+
+
+
+
+const PHONE_REGEX = /^[6-9]\d{9}$/;
+const GENDER_VALUES = ["male", "female", "other"];
+
+//logged in user
+
+const getMe = async (req, res) => {
+ try {
+ const user = req.user;
+
+ return res.status(200).json({
+ success: true,
+ user: {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ phone: user.phone,
+ roles: user.roles,
+ profileImage: user.profileImage,
+ bio: user.bio,
+ address: user.address,
+ dob: user.dob,
+ gender: user.gender,
+ city: user.city,
+ state: user.state,
+ isEmailVerified: user.isEmailVerified,
+ isActive: user.isActive,
+ },
+ });
+ } catch (error) {
+ console.error("Error in getMe:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+const updateMe = async (req, res) => {
+ try {
+ const user = req.user;
+
+ if (!user.isActive) {
+ return res.status(403).json({
+ success: false,
+ message:
+ "Your account has been disabled. Please contact support.",
+ });
+ }
+
+ const { name, phone, bio, address, city, state, dob, gender } = req.body;
+
+ const hasProfileUpdate =
+ name !== undefined ||
+ phone !== undefined ||
+ bio !== undefined ||
+ address !== undefined ||
+ city !== undefined ||
+ state !== undefined ||
+ dob !== undefined ||
+ gender !== undefined;
+
+ if (!hasProfileUpdate) {
+ return res.status(400).json({
+ success: false,
+ message: "No valid profile fields to update",
+ });
+ }
+
+ if (name !== undefined) {
+ const trimmedName = String(name).trim();
+ if (!trimmedName) {
+ return res.status(400).json({
+ success: false,
+ message: "Name cannot be empty",
+ });
+ }
+ if (trimmedName.length > 100) {
+ return res.status(400).json({
+ success: false,
+ message: "Name must be 100 characters or fewer",
+ });
+ }
+ user.name = trimmedName;
+ }
+
+ if (phone !== undefined) {
+ const trimmedPhone = String(phone).trim();
+ if (!PHONE_REGEX.test(trimmedPhone)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid phone number",
+ });
+ }
+ if (trimmedPhone !== user.phone) {
+ const existingPhone = await userModel.findOne({ phone: trimmedPhone });
+ if (
+ existingPhone &&
+ existingPhone._id.toString() !== user._id.toString()
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Phone number already exists",
+ });
+ }
+ }
+ user.phone = trimmedPhone;
+ }
+
+ if (bio !== undefined) {
+ const trimmedBio = String(bio).trim();
+ if (trimmedBio.length > 500) {
+ return res.status(400).json({
+ success: false,
+ message: "Bio must be 500 characters or fewer",
+ });
+ }
+ user.bio = trimmedBio;
+ }
+
+ if (address !== undefined) {
+ const trimmedAddress = String(address).trim();
+ if (trimmedAddress.length > 300) {
+ return res.status(400).json({
+ success: false,
+ message: "Address must be 300 characters or fewer",
+ });
+ }
+ user.address = trimmedAddress;
+ }
+
+ if (city !== undefined) {
+ const trimmedCity = String(city).trim();
+ if (trimmedCity.length > 100) {
+ return res.status(400).json({
+ success: false,
+ message: "City must be 100 characters or fewer",
+ });
+ }
+ user.city = trimmedCity;
+ }
+
+ if (state !== undefined) {
+ const trimmedState = String(state).trim();
+ if (trimmedState.length > 100) {
+ return res.status(400).json({
+ success: false,
+ message: "State must be 100 characters or fewer",
+ });
+ }
+ user.state = trimmedState;
+ }
+
+ if (gender !== undefined) {
+ const trimmedGender = String(gender).trim().toLowerCase();
+ if (!GENDER_VALUES.includes(trimmedGender)) {
+ return res.status(400).json({
+ success: false,
+ message: "Gender must be male, female, or other",
+ });
+ }
+ user.gender = trimmedGender;
+ }
+
+ if (dob !== undefined) {
+ if (dob === null || dob === "") {
+ user.dob = null;
+ } else {
+ const parsedDob = new Date(dob);
+ if (Number.isNaN(parsedDob.getTime())) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid date of birth",
+ });
+ }
+ if (parsedDob > new Date()) {
+ return res.status(400).json({
+ success: false,
+ message: "Date of birth cannot be in the future",
+ });
+ }
+ user.dob = parsedDob;
+ }
+ }
+
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Profile updated",
+ user: {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ phone: user.phone,
+ roles: user.roles,
+ profileImage: user.profileImage,
+ bio: user.bio,
+ address: user.address,
+ dob: user.dob,
+ gender: user.gender,
+ city: user.city,
+ state: user.state,
+ isEmailVerified: user.isEmailVerified,
+ isActive: user.isActive,
+ },
+ });
+ } catch (error) {
+ if (error.code === 11000) {
+ return res.status(400).json({
+ success: false,
+ message: "Phone number already exists",
+ });
+ }
+
+ console.error("Error in updateMe:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+const updateProfileImage = async (req, res) => {
+ try {
+ const user = req.user;
+
+ if (!user.isActive) {
+ return res.status(403).json({
+ success: false,
+ message:
+ "Your account has been disabled. Please contact support.",
+ });
+ }
+
+ if (!req.file) {
+ return res.status(400).json({
+ success: false,
+ message: "Please upload a profile image",
+ });
+ }
+
+ if (user.profileImagePublicId) {
+ await deleteFromCloudinary(user.profileImagePublicId);
+ }
+
+ const result = await uploadToCloudinary(req.file.buffer);
+
+ user.profileImage = result.secure_url;
+ user.profileImagePublicId = result.public_id;
+
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Profile image updated",
+ user: {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ phone: user.phone,
+ roles: user.roles,
+ profileImage: user.profileImage,
+ bio: user.bio,
+ address: user.address,
+ dob: user.dob,
+ gender: user.gender,
+ city: user.city,
+ state: user.state,
+ isEmailVerified: user.isEmailVerified,
+ isActive: user.isActive,
+ },
+ });
+ } catch (error) {
+ console.error("Error in updateProfileImage:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+const logout = (req, res) => {
+ try {
+ res.clearCookie("token", {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ });
+
+ return res.status(200).json({
+ success: true,
+ message: "Logout successful",
+ });
+ } catch (error) {
+ console.error("Logout error:", error);
+ return res.status(500).json({
+ success: false,
+ message: "Logout failed. Please try again.",
+ });
+ }
+}
+
+
+//become provider , initially custumer then provider in one click
+
+const becomeProvider = async (req, res) => {
+ try {
+
+ const user = req.user;
+
+ if (user.roles.includes("provider")) {
+ return res.status(400).json({
+ success: false,
+ message: "You are already a provider",
+ });
+ }
+
+ user.roles.push("provider");
+
+ await user.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Provider account created successfully",
+ roles: user.roles,
+ });
+
+ } catch (error) {
+ console.error("Become provider error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+
+export {
+ register,
+ verifyEmail,
+ resendOtp,
+ login,
+ forgotPassword,
+ resetPassword,
+ getMe,
+ updateMe,
+ updateProfileImage,
+ logout,
+ becomeProvider,
+};
\ No newline at end of file
diff --git a/backend/src/controllers/bookingController.js b/backend/src/controllers/bookingController.js
new file mode 100644
index 0000000000..c084a920c0
--- /dev/null
+++ b/backend/src/controllers/bookingController.js
@@ -0,0 +1,68 @@
+import venueAvailabilityModel from "../models/venueAvailabilityModel.js";
+import venueModel from "../models/venueModel.js";
+import bookingModel from "../models/bookingModel.js";
+
+
+
+//get users all bookings for normal users profile
+
+const getMyBookings = async (req, res) => {
+ try {
+
+ const bookings = await bookingModel.find({
+ userId: req.user._id
+ })
+ .populate("venueId")
+ .populate("availabilityId")
+ .sort({ createdAt: -1 })
+
+ return res.status(200).json({
+ success: true,
+ message:"Fetched all bookings",
+ count: bookings.length,
+ data: bookings,
+ });
+
+ } catch (error) {
+ console.error("Error fetching bookings:",error);
+ return res.status(500).json({
+ success:false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+
+//bookings for provider venues(fetch for provider)
+
+const getProviderBookings = async (req,res)=>{
+ try {
+ const venues = await venueModel.find({
+ ownerId: req.user._id,
+ });
+
+ const venueIds = venues.map((venue)=> venue._id);
+
+ const bookings = await bookingModel.find({
+ venueId:{$in: venueIds},
+ })
+ .populate("userId", "name email phone")
+ .populate("venueId")
+ .populate("availabilityId")
+ .sort({ createdAt: -1 });
+
+ return res.status(200).json({
+ success: true,
+ count: bookings.length,
+ data: bookings,
+ });
+ } catch (error) {
+ console.error("Error fetching provider bookings:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+}
+export { getMyBookings, getProviderBookings }
\ No newline at end of file
diff --git a/backend/src/controllers/paymentController.js b/backend/src/controllers/paymentController.js
new file mode 100644
index 0000000000..c2a40147d8
--- /dev/null
+++ b/backend/src/controllers/paymentController.js
@@ -0,0 +1,399 @@
+import venueModel from '../models/venueModel.js';
+import venueAvailabilityModel from '../models/venueAvailabilityModel.js';
+import bookingModel from '../models/bookingModel.js';
+import paymentOrderModel from '../models/paymentOrderModel.js';
+import Razorpay from 'razorpay';
+import crypto from 'crypto';
+import { invalidateVenueAvailabilityCache } from '../utils/cache.js';
+
+const razorpayInstance = new Razorpay({
+ key_id: process.env.RAZORPAY_KEY_ID,
+ key_secret: process.env.RAZORPAY_KEY_SECRET,
+});
+
+const isSlotExpired = (slot) => {
+ const now = new Date();
+
+ const slotDate = new Date(slot.date);
+
+ const [time, period] = slot.endTime.split(" ");
+
+ let [hours, minutes] = time.split(":").map(Number);
+
+ if (period === "PM" && hours !== 12) {
+ hours += 12;
+ }
+
+ if (period === "AM" && hours === 12) {
+ hours = 0;
+ }
+
+ slotDate.setHours(hours, minutes, 0, 0);
+
+ return slotDate < now;
+};
+
+const createOrder = async (req, res) => {
+ try {
+ const { venueId, availabilityId } = req.body;
+ if (!venueId || !availabilityId) {
+ return res.status(400).json({
+ success: false,
+ message: "Venue ID and Availability ID are required",
+ });
+ }
+
+ const venue = await venueModel.findById(venueId);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (!venue.isActive) {
+ return res.status(400).json({
+ success: false,
+ message: "Venue is inactive",
+ });
+ }
+ if (venue.ownerId.toString() === req.user._id.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "you cannot book your own venue",
+ });
+ }
+
+ const slot = await venueAvailabilityModel.findById(availabilityId);
+
+ if (!slot) {
+ return res.status(404).json({
+ success: false,
+ message: "Availability slot not found",
+ });
+ }
+
+ if (!slot.isActive) {
+ return res.status(400).json({
+ success: false,
+ message: "This slot is inactive",
+ });
+ }
+
+ if (slot.isBooked) {
+ return res.status(400).json({
+ success: false,
+ message: "This slot is already booked",
+ });
+ }
+ if (isSlotExpired(slot)) {
+ return res.status(400).json({
+ success: false,
+ message: "This slot has already expired",
+ });
+ }
+
+ if (slot.venueId.toString() !== venue._id.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "Selected slot does not belong to this venue",
+ });
+ }
+
+ const amountInPaise = venue.price * 100;
+
+ const options = {
+ amount: amountInPaise,
+ currency: "INR",
+ receipt: `BMV_${Date.now()}`
+ };
+
+ const order = await razorpayInstance.orders.create(options);
+
+ await paymentOrderModel.create({
+ userId: req.user._id,
+ venueId: venue._id,
+ availabilityId: slot._id,
+ razorpayOrderId: order.id,
+ amountInPaise,
+ });
+
+ return res.status(200).json({
+ success: true,
+ message: "Order created successfully",
+ order,
+ });
+ } catch (error) {
+ console.error("Create order error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+
+const verifyPayment = async (req, res) => {
+ try {
+
+ const {
+ razorpay_order_id,
+ razorpay_payment_id,
+ razorpay_signature,
+ venueId,
+ availabilityId,
+ } = req.body;
+
+ if (
+ !razorpay_order_id ||
+ !razorpay_payment_id ||
+ !razorpay_signature ||
+ !venueId ||
+ !availabilityId
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Missing required payment details",
+ });
+ }
+
+ const paymentOrder = await paymentOrderModel.findOne({
+ razorpayOrderId: razorpay_order_id,
+ });
+
+ if (!paymentOrder) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid or unknown payment order",
+ });
+ }
+
+ const existingByPaymentId = await bookingModel.findOne({
+ paymentId: razorpay_payment_id,
+ paymentStatus: "paid",
+ });
+
+ if (existingByPaymentId) {
+ if (existingByPaymentId.razorpayOrderId === razorpay_order_id) {
+ return res.status(200).json({
+ success: true,
+ message: "Payment already verified",
+ data: existingByPaymentId,
+ });
+ }
+
+ return res.status(400).json({
+ success: false,
+ message: "This payment has already been used for another booking",
+ });
+ }
+
+ if (paymentOrder.status === "completed") {
+ const existingBooking = await bookingModel.findOne({
+ razorpayOrderId: razorpay_order_id,
+ paymentStatus: "paid",
+ });
+
+ if (existingBooking) {
+ return res.status(200).json({
+ success: true,
+ message: "Payment already verified",
+ data: existingBooking,
+ });
+ }
+ }
+
+ if (paymentOrder.userId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "This payment order does not belong to you",
+ });
+ }
+
+ if (paymentOrder.venueId.toString() !== venueId.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "Payment order does not match the selected venue",
+ });
+ }
+
+ if (paymentOrder.availabilityId.toString() !== availabilityId.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "Payment order does not match the selected slot",
+ });
+ }
+
+ const generatedSignature = crypto
+ .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET)
+ .update(`${razorpay_order_id}|${razorpay_payment_id}`)
+ .digest("hex");
+
+ if (generatedSignature !== razorpay_signature) {
+ return res.status(400).json({
+ success: false,
+ message: "Payment verification failed",
+ });
+ }
+
+ let razorpayOrder;
+ try {
+ razorpayOrder = await razorpayInstance.orders.fetch(razorpay_order_id);
+ } catch {
+ return res.status(400).json({
+ success: false,
+ message: "Unable to verify payment order with Razorpay",
+ });
+ }
+
+ if (razorpayOrder.amount !== paymentOrder.amountInPaise) {
+ return res.status(400).json({
+ success: false,
+ message: "Payment amount does not match the expected order amount",
+ });
+ }
+
+ const venue = await venueModel.findById(venueId);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (!venue.isActive) {
+ return res.status(400).json({
+ success: false,
+ message: "Venue is inactive",
+ });
+ }
+
+ if (venue.ownerId.toString() === req.user._id.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "You cannot book your own venue",
+ });
+ }
+
+ const slot = await venueAvailabilityModel.findById(availabilityId);
+
+ if (!slot) {
+ return res.status(404).json({
+ success: false,
+ message: "Availability slot not found",
+ });
+ }
+
+ if (!slot.isActive) {
+ return res.status(400).json({
+ success: false,
+ message: "This slot is inactive",
+ });
+ }
+
+ if (slot.isBooked) {
+ return res.status(400).json({
+ success: false,
+ message: "This slot is already booked",
+ });
+ }
+
+ if (slot.venueId.toString() !== venue._id.toString()) {
+ return res.status(400).json({
+ success: false,
+ message: "Selected slot does not belong to this venue",
+ });
+ }
+
+ const bookingAmount = paymentOrder.amountInPaise / 100;
+
+ const bookingReference =
+ "BMV-" + Date.now().toString().slice(-6);
+
+ const booking = await bookingModel.create({
+ bookingReference,
+ userId: req.user._id,
+ venueId,
+ availabilityId,
+ amount: bookingAmount,
+ contactPhone: req.user.phone,
+ bookingStatus: "confirmed",
+ paymentStatus: "paid",
+ paymentMethod: "razorpay",
+ razorpayOrderId: razorpay_order_id,
+ paymentId: razorpay_payment_id,
+ });
+
+ const updatedSlot =
+ await venueAvailabilityModel.findOneAndUpdate(
+ {
+ _id: availabilityId,
+ isBooked: false,
+ isActive: true,
+ },
+ {
+ $set: {
+ isBooked: true,
+ bookingId: booking._id,
+ },
+ },
+ {
+ new: true,
+ }
+ );
+
+ if (!updatedSlot) {
+ await bookingModel.findByIdAndDelete(booking._id);
+
+ return res.status(400).json({
+ success: false,
+ message: "This slot was already booked by another user",
+ });
+ }
+
+ paymentOrder.status = "completed";
+ await paymentOrder.save();
+
+ await invalidateVenueAvailabilityCache(venueId);
+
+ return res.status(201).json({
+ success: true,
+ message: "Payment verified and venue booked successfully",
+ data: booking,
+ });
+
+ } catch (error) {
+ console.error("Verify payment error:", error);
+
+ if (error.code === 11000 && error.keyPattern?.paymentId) {
+ const existingBooking = await bookingModel.findOne({
+ paymentId: req.body.razorpay_payment_id,
+ paymentStatus: "paid",
+ });
+
+ if (existingBooking) {
+ return res.status(200).json({
+ success: true,
+ message: "Payment already verified",
+ data: existingBooking,
+ });
+ }
+
+ return res.status(400).json({
+ success: false,
+ message: "This payment has already been used for another booking",
+ });
+ }
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+
+export { createOrder, verifyPayment }
diff --git a/backend/src/controllers/venueAvailabilityController.js b/backend/src/controllers/venueAvailabilityController.js
new file mode 100644
index 0000000000..72f875177c
--- /dev/null
+++ b/backend/src/controllers/venueAvailabilityController.js
@@ -0,0 +1,285 @@
+import mongoose from "mongoose";
+import venueAvailabilityModel from "../models/venueAvailabilityModel.js";
+import venueModel from "../models/venueModel.js";
+import { isValidSlotTime, parseSlotTimeToMinutes, } from "../utils/parseSlotTime.js";
+import { getCache, setCache, getVenueAvailabilityCacheKey, invalidateVenueAvailabilityCache, VENUE_AVAILABILITY_CACHE_TTL, } from "../utils/cache.js";
+
+const createAvailability = async (req, res) => {
+ try {
+ const {
+ venueId, date, slotLabel, startTime, endTime, } = req.body;
+
+ if (!venueId || !date || !slotLabel || !startTime || !endTime) {
+ return res.status(400).json({
+ success: false,
+ message: "Please fill all required fields",
+ });
+ }
+
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(venueId);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ // Ownership check
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "You can only manage your own venue",
+ });
+ }
+ const selectedDate = new Date(date);
+
+ if (Number.isNaN(selectedDate.getTime())) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid date",
+ });
+ }
+
+ const today = new Date();
+
+ today.setHours(0, 0, 0, 0);
+ selectedDate.setHours(0, 0, 0, 0);
+
+ if (selectedDate < today) {
+ return res.status(400).json({
+ success: false,
+ message: "Cannot create availability for past dates",
+ });
+ }
+ const startMinutes = parseSlotTimeToMinutes(startTime);
+ const endMinutes = parseSlotTimeToMinutes(endTime);
+
+ if (!isValidSlotTime(startTime) || !isValidSlotTime(endTime)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid time format",
+ });
+ }
+
+ if (startMinutes >= endMinutes) {
+ return res.status(400).json({
+ success: false,
+ message: "End time must be after start time",
+ });
+ }
+
+ const existingSlot = await venueAvailabilityModel.findOne({
+ venueId, date, slotLabel,
+ });
+
+ if (existingSlot) {
+ return res.status(400).json({
+ success: false,
+ message: "Slot already exists for this date",
+ });
+ }
+
+
+
+ const availability =
+ await venueAvailabilityModel.create({
+ venueId, date, slotLabel, startTime, endTime,
+ });
+
+ await invalidateVenueAvailabilityCache(venueId);
+
+ return res.status(201).json({
+ success: true,
+ message: "Availability created successfully",
+ data: availability,
+ });
+
+ } catch (error) {
+ console.error("Create availability error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later."
+ });
+ }
+};
+
+
+const getVenueAvailability = async (req, res) => {
+ try {
+
+ const { venueId } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const cacheKey = getVenueAvailabilityCacheKey(venueId);
+ const cached = await getCache(cacheKey);
+ if (cached) {
+ return res.status(200).json(cached);
+ }
+
+ const availability = await venueAvailabilityModel.find({ venueId, }).sort({
+ date: 1,
+ startTime: 1,
+ });
+
+ const payload = {
+ success: true,
+ count: availability.length,
+ data: availability,
+ };
+
+ await setCache(cacheKey, payload, VENUE_AVAILABILITY_CACHE_TTL);
+
+ return res.status(200).json(payload);
+
+ } catch (error) {
+ console.error("Get availability error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later."
+ });
+ }
+};
+
+//deactivate venue slotes of if not available by provider
+
+const deactivateAvailability = async (req, res) => {
+ try {
+
+
+ const { slotId } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(slotId)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid slot ID",
+ });
+ }
+ const slot = await venueAvailabilityModel.findById(slotId);
+
+ if (!slot) {
+ return res.status(404).json({
+ success: false,
+ message: "Slot not found",
+ });
+ }
+
+ const venue = await venueModel.findById(slot.venueId);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Unauthorized",
+
+ })
+ }
+
+
+ if (slot.isBooked) {
+ return res.status(400).json({
+ success: false,
+ message: "Booked slots cannot be deactivated",
+ });
+ }
+ slot.isActive = false;
+ await slot.save();
+
+ await invalidateVenueAvailabilityCache(slot.venueId);
+
+ return res.status(200).json({
+ success: true,
+ message: "Slot deactivated successfully",
+ data: slot,
+ });
+
+ } catch (error) {
+ console.error("Deactivated slot error", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later."
+ });
+ }
+};
+
+//acivate that
+
+const activateAvailability = async (req, res) => {
+ try {
+ const { slotId } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(slotId)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid slot ID",
+ });
+ }
+
+ const slot = await venueAvailabilityModel.findById(slotId);
+
+ if (!slot) {
+ return res.status(404).json({
+ success: false,
+ message: "Slot not found",
+ });
+ }
+
+ const venue = await venueModel.findById(slot.venueId);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Unauthorized",
+ });
+ }
+
+ slot.isActive = true;
+ await slot.save();
+
+ await invalidateVenueAvailabilityCache(slot.venueId);
+
+ return res.status(200).json({
+ success: true,
+ message: "Slot activated successfully",
+ data: slot,
+ });
+ } catch (error) {
+ console.error("Activate slot error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later."
+ });
+ }
+};
+
+
+
+export { createAvailability, getVenueAvailability, deactivateAvailability, activateAvailability };
\ No newline at end of file
diff --git a/backend/src/controllers/venueController.js b/backend/src/controllers/venueController.js
new file mode 100644
index 0000000000..1c056347f5
--- /dev/null
+++ b/backend/src/controllers/venueController.js
@@ -0,0 +1,706 @@
+import venueModel from "../models/venueModel.js";
+import uploadToCloudinary from "../utils/uploadToCloudinary.js";
+import deleteFromCloudinary from "../utils/deleteFromCloudinary.js";
+import { resolveVenueCategory } from "../utils/venueCategory.js";
+import { getCache, setCache, getActiveVenueCacheKey, invalidateActiveVenuesCache, invalidateActiveVenueCache, ACTIVE_VENUES_CACHE_KEY, ACTIVE_VENUES_CACHE_TTL, } from "../utils/cache.js";
+import mongoose from "mongoose";
+
+
+const parseJsonArrayField = (value, fieldName) => {
+ if (!value) {
+ return { data: [] };
+ }
+
+ try {
+ const parsed = JSON.parse(value);
+
+ if (!Array.isArray(parsed)) {
+ return {
+ error: `Invalid ${fieldName}. Expected a JSON array.`,
+ };
+ }
+
+ return { data: parsed };
+ } catch {
+ return {
+ error: `Invalid ${fieldName}. Please provide valid JSON.`,
+ };
+ }
+};
+
+//provider creates venue
+const createVenue = async (req, res) => {
+ const uploadedImages = [];
+
+ try {
+ const {
+ title,
+ description,
+ category,
+ venueType,
+ indoorOutdoor,
+ price,
+ pricingUnit,
+ capacity,
+ amenities,
+ rules,
+ address,
+ city,
+ state,
+ pincode,
+ latitude,
+ longitude,
+ } = req.body;
+
+ if (
+ !title ||
+ !description ||
+ !category ||
+ !price ||
+ !capacity ||
+ !address
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Please fill all required fields.",
+ });
+ }
+
+ // Validate numeric fields
+ const parsedPrice = Number(price);
+ const parsedCapacity = Number(capacity);
+
+ if (Number.isNaN(parsedPrice) || parsedPrice <= 0) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid price.",
+ });
+ }
+
+ if (Number.isNaN(parsedCapacity) || parsedCapacity <= 0 || !Number.isInteger(parsedCapacity)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid capacity.",
+ });
+ }
+
+ let parsedLatitude;
+ let parsedLongitude;
+
+ if (latitude !== undefined && latitude !== "") {
+ parsedLatitude = Number(latitude);
+
+ if (
+ Number.isNaN(parsedLatitude) ||
+ parsedLatitude < -90 ||
+ parsedLatitude > 90
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid latitude.",
+ });
+ }
+ }
+
+ if (longitude !== undefined && longitude !== "") {
+ parsedLongitude = Number(longitude);
+
+ if (
+ Number.isNaN(parsedLongitude) ||
+ parsedLongitude < -180 ||
+ parsedLongitude > 180
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid longitude.",
+ });
+ }
+ }
+
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: "Please upload at least one image.",
+ });
+ }
+
+ // Upload images
+ for (const file of req.files) {
+ const result = await uploadToCloudinary(file.buffer);
+
+ uploadedImages.push({
+ url: result.secure_url,
+ public_id: result.public_id,
+ });
+ }
+
+ const parsedAmenities = parseJsonArrayField(
+ amenities,
+ "amenities"
+ );
+
+ if (parsedAmenities.error) {
+ return res.status(400).json({
+ success: false,
+ message: parsedAmenities.error,
+ });
+ }
+
+ const parsedRules = parseJsonArrayField(
+ rules,
+ "rules"
+ );
+
+ if (parsedRules.error) {
+ return res.status(400).json({
+ success: false,
+ message: parsedRules.error,
+ });
+ }
+
+ const normalizedCategory = resolveVenueCategory(category);
+
+ if (!normalizedCategory) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue category.",
+ });
+ }
+
+ const venuePayload = {
+ ownerId: req.user._id,
+ title,
+ description,
+ category: normalizedCategory,
+ venueType,
+ indoorOutdoor,
+ price: parsedPrice,
+ capacity: parsedCapacity,
+ amenities: parsedAmenities.data,
+ rules: parsedRules.data,
+ address,
+ city,
+ state,
+ pincode,
+ location: {
+ latitude: parsedLatitude,
+ longitude: parsedLongitude,
+ },
+ images: uploadedImages,
+ coverImage: uploadedImages[0],
+ };
+
+ if (pricingUnit !== undefined && pricingUnit !== "") {
+ venuePayload.pricingUnit = pricingUnit;
+ }
+
+ const venue = await venueModel.create(venuePayload);
+
+ await invalidateActiveVenuesCache();
+
+ return res.status(201).json({
+ success: true,
+ message: "Venue created successfully.",
+ data: venue,
+ });
+ } catch (error) {
+ console.error("Create venue error:", error);
+
+ // delete uploaded images from cloudinary if error occurs
+ for (const image of uploadedImages) {
+ try {
+ await deleteFromCloudinary(image.public_id);
+ } catch (cleanupError) {
+ console.error(
+ "Cloudinary cleanup failed:",
+ cleanupError
+ );
+ }
+ }
+
+ return res.status(500).json({
+ success: false,
+ message: "Failed to create venue. Please try again.",
+ });
+ }
+};
+
+//fetch owners all venues
+
+const getMyVenues = async (req, res) => {
+ try {
+ const venues = await venueModel.find({ ownerId: req.user._id })
+ .sort({ createdAt: -1 });
+
+ return res.status(200).json({
+ success: true,
+ data: venues,
+ count: venues.length,
+ });
+ } catch (error) {
+ console.error("Error fetching my venues:", error);
+ return res.status(500).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+//show details of a venue for provider
+
+const getVenueById = async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found.",
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied.",
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ data: venue,
+ });
+ } catch (error) {
+ console.error("Error fetching venue details:", error);
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+//update my-venue
+
+const updateVenue = async (req, res) => {
+ const newlyUploadedImages = [];
+
+ try {
+ const { id } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied",
+ });
+ }
+
+ const {
+ title,
+ description,
+ category,
+ venueType,
+ indoorOutdoor,
+ price,
+ pricingUnit,
+ capacity,
+ amenities,
+ rules,
+ address,
+ city,
+ state,
+ pincode,
+ latitude,
+ longitude,
+ } = req.body;
+
+ if (title !== undefined) venue.title = title;
+ if (description !== undefined) venue.description = description;
+
+ if (category !== undefined) {
+ const normalizedCategory = resolveVenueCategory(category);
+
+ if (!normalizedCategory) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue category.",
+ });
+ }
+
+ venue.category = normalizedCategory;
+ }
+ if (venueType !== undefined) venue.venueType = venueType;
+ if (indoorOutdoor !== undefined) venue.indoorOutdoor = indoorOutdoor;
+ if (address !== undefined) venue.address = address;
+ if (city !== undefined) venue.city = city;
+ if (state !== undefined) venue.state = state;
+ if (pincode !== undefined) venue.pincode = pincode;
+
+ // Price validation
+ if (price !== undefined) {
+ const parsedPrice = Number(price);
+
+ if (Number.isNaN(parsedPrice) || parsedPrice <= 0) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid price.",
+ });
+ }
+
+ venue.price = parsedPrice;
+ }
+
+ // Capacity validation
+ if (capacity !== undefined) {
+ const parsedCapacity = Number(capacity);
+
+ if (
+ Number.isNaN(parsedCapacity) ||
+ parsedCapacity <= 0 ||
+ !Number.isInteger(parsedCapacity)
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid capacity.",
+ });
+ }
+
+ venue.capacity = parsedCapacity;
+ }
+
+ // Amenities
+ if (amenities !== undefined) {
+ const parsedAmenities = parseJsonArrayField(
+ amenities,
+ "amenities"
+ );
+
+ if (parsedAmenities.error) {
+ return res.status(400).json({
+ success: false,
+ message: parsedAmenities.error,
+ });
+ }
+
+ venue.amenities = parsedAmenities.data;
+ }
+
+ // Rules
+ if (rules !== undefined) {
+ const parsedRules = parseJsonArrayField(
+ rules,
+ "rules"
+ );
+
+ if (parsedRules.error) {
+ return res.status(400).json({
+ success: false,
+ message: parsedRules.error,
+ });
+ }
+
+ venue.rules = parsedRules.data;
+ }
+
+ // Location validation
+ if (latitude !== undefined && latitude !== "") {
+ const parsedLatitude = Number(latitude);
+
+ if (
+ Number.isNaN(parsedLatitude) ||
+ parsedLatitude < -90 ||
+ parsedLatitude > 90
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid latitude.",
+ });
+ }
+
+ venue.location.latitude = parsedLatitude;
+ }
+
+ if (longitude !== undefined && longitude !== "") {
+ const parsedLongitude = Number(longitude);
+
+ if (
+ Number.isNaN(parsedLongitude) ||
+ parsedLongitude < -180 ||
+ parsedLongitude > 180
+ ) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid longitude.",
+ });
+ }
+
+ venue.location.longitude = parsedLongitude;
+ }
+
+ // Upload new images FIRST
+ if (req.files && req.files.length > 0) {
+
+ for (const file of req.files) {
+ const result = await uploadToCloudinary(file.buffer);
+
+ newlyUploadedImages.push({
+ url: result.secure_url,
+ public_id: result.public_id,
+ });
+ }
+
+ // Delete old images only after successful upload
+ for (const image of venue.images) {
+ await deleteFromCloudinary(image.public_id);
+ }
+
+ venue.images = newlyUploadedImages;
+ venue.coverImage = newlyUploadedImages[0];
+ }
+
+ await venue.save();
+
+ await invalidateActiveVenuesCache();
+ await invalidateActiveVenueCache(id);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue updated successfully",
+ data: venue,
+ });
+
+ } catch (error) {
+
+ // Cleanup newly uploaded images if save fails
+ for (const image of newlyUploadedImages) {
+ try {
+ await deleteFromCloudinary(image.public_id);
+ } catch (cleanupError) {
+ console.error(
+ "Cloudinary cleanup failed:",
+ cleanupError
+ );
+ }
+ }
+
+ console.error("Update venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+//deactivate venue already listed
+
+const deactivateVenue = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found"
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied",
+ });
+ }
+
+ venue.isActive = false;
+
+ await venue.save();
+
+ await invalidateActiveVenuesCache();
+ await invalidateActiveVenueCache(id);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue deactivated successfully",
+ data: venue,
+ });
+
+
+ } catch (error) {
+ console.error("Deactivate venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+//acivate owner venue which listed
+
+const activateVenue = async (req, res) => {
+ try {
+ const { id } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(id)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const venue = await venueModel.findById(id);
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ if (venue.ownerId.toString() !== req.user._id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied",
+ });
+ }
+
+ venue.isActive = true;
+
+ await venue.save();
+
+ await invalidateActiveVenuesCache();
+ await invalidateActiveVenueCache(id);
+
+ return res.status(200).json({
+ success: true,
+ message: "Venue activated successfully",
+ data: venue,
+ });
+
+ } catch (error) {
+ console.error("Activate venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+};
+
+//get venues for users/public
+
+const getAllVenues = async (req, res) => {
+ try {
+ const cached = await getCache(ACTIVE_VENUES_CACHE_KEY);
+ if (cached) {
+ return res.status(200).json(cached);
+ }
+ console.log("❌ Redis MISS");
+
+ const venues = await venueModel.find({
+ isActive: true,
+ }).select("-__v").sort({ createdAt: -1 });
+
+ const payload = {
+ success: true,
+ count: venues.length,
+ data: venues,
+ };
+
+ await setCache(ACTIVE_VENUES_CACHE_KEY, payload, ACTIVE_VENUES_CACHE_TTL);
+ console.log("Saved data to Redis");
+ return res.status(200).json(payload);
+
+ } catch (error) {
+ console.error("Get venues error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+}
+
+//get venue details for public/users
+
+const getPublicVenueById = async (req, res) => {
+ try {
+ const { venueId } = req.params;
+ if (!mongoose.Types.ObjectId.isValid(venueId)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid venue ID",
+ });
+ }
+
+ const cacheKey = getActiveVenueCacheKey(venueId);
+ const cached = await getCache(cacheKey);
+ if (cached) {
+ return res.status(200).json(cached);
+ }
+
+ const venue = await venueModel.findOne({
+ _id: venueId,
+ isActive: true,
+ });
+
+ if (!venue) {
+ return res.status(404).json({
+ success: false,
+ message: "Venue not found",
+ });
+ }
+
+ const payload = {
+ success: true,
+ data: venue,
+ };
+
+ await setCache(cacheKey, payload, ACTIVE_VENUES_CACHE_TTL);
+
+ return res.status(200).json(payload);
+ } catch (error) {
+ console.error("Get venue error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+}
+export {
+ createVenue, getMyVenues, getVenueById, updateVenue, deactivateVenue, activateVenue,
+ getAllVenues, getPublicVenueById
+};
\ No newline at end of file
diff --git a/backend/src/index.js b/backend/src/index.js
new file mode 100644
index 0000000000..584e3abbd7
--- /dev/null
+++ b/backend/src/index.js
@@ -0,0 +1,65 @@
+import express from "express";
+import cors from "cors";
+import connection from "./config/db.js";
+import { connectRedis } from "./config/redis.js";
+import dotenv from "dotenv";
+import authRouter from "./routes/authRoutes.js";
+import venueRouter from "./routes/venueRoutes.js";
+import cookieParser from "cookie-parser";
+import venueAvailabilityRoutes from './routes/venueAvailabilityRoutes.js'
+import bookingRoutes from './routes/bookingRoutes.js'
+import paymentRoutes from './routes/paymentRoutes.js'
+import adminRouter from "./routes/adminRoutes.js"
+import swaggerUi from "swagger-ui-express";
+import swaggerSpec from "./config/swagger.js";
+
+
+dotenv.config();
+const app = express();
+
+//middleware
+const allowedOrigins = [
+ "http://localhost:5173",
+ process.env.CLIENT_URL,
+];
+
+app.use(
+ cors({
+ origin: allowedOrigins,
+ credentials: true,
+ })
+);
+
+app.use(express.json());
+app.use(cookieParser());
+
+//db
+connection();
+
+//redis
+connectRedis();
+
+app.use("/api/auth", authRouter);
+app.use("/api/venues", venueRouter);
+app.use('/api/availability', venueAvailabilityRoutes);
+app.use("/api/bookings", bookingRoutes);
+app.use("/api/payments", paymentRoutes);
+app.use("/api/admin", adminRouter);
+
+if (process.env.ENABLE_SWAGGER === "true") {
+ app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
+}
+
+const PORT = process.env.PORT || 5000;
+
+app.get("/", (req, res) => {
+ res.status(200).json({
+ success: true,
+ message: "BookMyVenue API is running",
+ environment: process.env.NODE_ENV
+ });
+});
+
+app.listen(PORT, () => {
+ console.log(`Server is running on port ${PORT}`);
+});
\ No newline at end of file
diff --git a/backend/src/middleware/authorizeRoles.js b/backend/src/middleware/authorizeRoles.js
new file mode 100644
index 0000000000..41773ed283
--- /dev/null
+++ b/backend/src/middleware/authorizeRoles.js
@@ -0,0 +1,34 @@
+const authorizeRoles = (...allowedRoles) => {
+ return (req, res, next) => {
+ try {
+ const userRoles = req.user.roles;
+
+ if (!userRoles || userRoles.length === 0) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied. No roles assigned.",
+ });
+ }
+
+ const hasAccess = userRoles.some(role => allowedRoles.includes(role));
+
+ if (!hasAccess) {
+ return res.status(403).json({
+ success: false,
+ message: "Access denied. You do not have the permission to perform this action.",
+ });
+ }
+
+ next();
+ } catch (error) {
+ console.error("Role authorization error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Something went wrong. Please try again later.",
+ });
+ }
+ };
+};
+
+export default authorizeRoles;
\ No newline at end of file
diff --git a/backend/src/middleware/upload.js b/backend/src/middleware/upload.js
new file mode 100644
index 0000000000..74dad18844
--- /dev/null
+++ b/backend/src/middleware/upload.js
@@ -0,0 +1,9 @@
+import multer from "multer";
+
+const storage = multer.memoryStorage();
+
+const upload = multer({ storage: storage,
+ limits: { fileSize: 5 * 1024 * 1024 }, // mx 5MB
+});
+
+export default upload;
diff --git a/backend/src/middleware/userAuthMiddleware.js b/backend/src/middleware/userAuthMiddleware.js
new file mode 100644
index 0000000000..9e6d06dd35
--- /dev/null
+++ b/backend/src/middleware/userAuthMiddleware.js
@@ -0,0 +1,46 @@
+import jwt from "jsonwebtoken";
+import userModel from "../models/userModel.js";
+
+const authMiddleware = async (req, res, next) => {
+ try {
+ const token = req.cookies.token;
+
+ if (!token) {
+ return res.status(401).json({
+ success: false,
+ message: "Unauthorized access. Please login.",
+ });
+ }
+
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+
+ const user = await userModel.findById(decoded.id)
+ .select("-password -otp -otpExpiresAt");
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: "Unauthorized access. User not found.",
+ });
+ }
+
+ if (!user.isActive) {
+ return res.status(403).json({
+ success: false,
+ message: "Your account has been deactivated. Please contact support.",
+ });
+ }
+
+ req.user = user;
+ next();
+ } catch (error) {
+ console.error("Error in authMiddleware:", error);
+ return res.status(401).json({
+ success: false,
+ message: "Unauthorized access. Invalid token.",
+ });
+ }
+};
+
+
+export default authMiddleware;
\ No newline at end of file
diff --git a/backend/src/models/bookingModel.js b/backend/src/models/bookingModel.js
new file mode 100644
index 0000000000..e30882864d
--- /dev/null
+++ b/backend/src/models/bookingModel.js
@@ -0,0 +1,77 @@
+import mongoose from "mongoose";
+
+
+const bookingSchema = new mongoose.Schema(
+ {
+ bookingReference: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ },
+ venueId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Venue",
+ required: true,
+ },
+ availabilityId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "VenueAvailability",
+ required: true,
+ },
+ amount: {
+ type: Number,
+ required: true,
+ },
+ bookingStatus: {
+ type: String,
+ enum: ["confirmed", "cancelled"],
+ default: "confirmed",
+ },
+ paymentMethod: {
+ type: String,
+ default: null,
+ },
+ razorpayOrderId: {
+ type: String,
+ default: null,
+ },
+ paymentId: {
+ type: String,
+ default: null,
+ unique: true,
+ sparse: true,
+ },
+
+ paymentStatus: {
+ type: String,
+ enum: ["pending", "paid", "failed", "refunded"],
+ default: "pending",
+ },
+
+ contactPhone: {
+ type: String,
+ required: true,
+ },
+
+ bookedAt: {
+ type: Date,
+ default: Date.now,
+ },
+
+ cancelledAt: {
+ type: Date,
+ default: null,
+ },
+ },
+ {
+ timestamps: true,
+ }
+)
+
+const bookingModel = mongoose.model("Bookings", bookingSchema);
+export default bookingModel;
\ No newline at end of file
diff --git a/backend/src/models/paymentOrderModel.js b/backend/src/models/paymentOrderModel.js
new file mode 100644
index 0000000000..de7ac6b2ee
--- /dev/null
+++ b/backend/src/models/paymentOrderModel.js
@@ -0,0 +1,42 @@
+import mongoose from "mongoose";
+
+const paymentOrderSchema = new mongoose.Schema(
+ {
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ },
+ venueId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Venue",
+ required: true,
+ },
+ availabilityId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "VenueAvailability",
+ required: true,
+ },
+ razorpayOrderId: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ amountInPaise: {
+ type: Number,
+ required: true,
+ },
+ status: {
+ type: String,
+ enum: ["created", "completed"],
+ default: "created",
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+const paymentOrderModel = mongoose.model("PaymentOrder", paymentOrderSchema);
+
+export default paymentOrderModel;
diff --git a/backend/src/models/userModel.js b/backend/src/models/userModel.js
new file mode 100644
index 0000000000..5884499a43
--- /dev/null
+++ b/backend/src/models/userModel.js
@@ -0,0 +1,115 @@
+import mongoose from "mongoose";
+
+const userSchema = new mongoose.Schema(
+ {
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ trim: true,
+ },
+
+ phone: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true,
+ },
+
+ password: {
+ type: String,
+ required: true,
+ },
+
+ isEmailVerified: {
+ type: Boolean,
+ default: false,
+ },
+
+ otp: {
+ type: String,
+ default: null,
+ },
+
+ otpExpiresAt: {
+ type: Date,
+ default: null,
+ },
+
+ roles: {
+ type: [String],
+ enum: ["customer", "provider", "admin"],
+ default: ["customer"],
+ },
+
+ profileImage: {
+ type: String,
+ default: "",
+ },
+
+ profileImagePublicId: {
+ type: String,
+ default: "",
+ },
+
+ bio: {
+ type: String,
+ default: "",
+ },
+
+ address: {
+ type: String,
+ default: "",
+ },
+
+ dob: {
+ type: Date,
+ default: null,
+ },
+
+ gender: {
+ type: String,
+ enum: ["male", "female", "other"],
+ default: "other",
+ },
+
+ city: {
+ type: String,
+ default: "",
+ },
+
+ state: {
+ type: String,
+ default: "",
+ },
+
+ providerProfileCompleted: {
+ type: Boolean,
+ default: false,
+ },
+
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+
+ lastLogin: {
+ type: Date,
+ default: null,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+const userModel = mongoose.model("User", userSchema);
+
+export default userModel;
\ No newline at end of file
diff --git a/backend/src/models/venueAvailabilityModel.js b/backend/src/models/venueAvailabilityModel.js
new file mode 100644
index 0000000000..081df07143
--- /dev/null
+++ b/backend/src/models/venueAvailabilityModel.js
@@ -0,0 +1,56 @@
+import mongoose from "mongoose";
+
+const venueAvailabilitySchema = new mongoose.Schema(
+ {
+ venueId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Venue",
+ required: true,
+ },
+
+ date: {
+ type: Date,
+ required: true,
+ },
+
+ slotLabel: {
+ type: String,
+ enum: ["morning", "evening", "night", "fullday"],
+ required: true,
+ },
+
+ startTime: {
+ type: String,
+ required: true,
+ },
+
+ endTime: {
+ type: String,
+ required: true,
+ },
+
+ isBooked: {
+ type: Boolean,
+ default: false,
+ },
+
+ bookingId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Booking",
+ default: null,
+ },
+
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+const venueAvailabilityModel = mongoose.model("VenueAvailability",venueAvailabilitySchema
+);
+
+export default venueAvailabilityModel;
\ No newline at end of file
diff --git a/backend/src/models/venueModel.js b/backend/src/models/venueModel.js
new file mode 100644
index 0000000000..ca740e9a8e
--- /dev/null
+++ b/backend/src/models/venueModel.js
@@ -0,0 +1,141 @@
+import mongoose from "mongoose";
+
+const venueSchema = new mongoose.Schema(
+ {
+ ownerId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+
+ description: {
+ type: String,
+ required: true,
+ },
+
+ category:{
+ type:String,
+ enum:[
+ "wedding",
+ "corporate",
+ "birthday",
+ "party",
+ "function",
+ "photoshoot",
+ "other"
+ ],
+ required:true
+ },
+
+ images: [
+ {
+ url: {
+ type: String,
+ required: true,
+ },
+
+ public_id: {
+ type: String,
+ required: true,
+ },
+ },
+ ],
+ coverImage: {
+ url: {
+ type: String,
+ default: "",
+ },
+
+ public_id: {
+ type: String,
+ default: "",
+ },
+ },
+ venueType: {
+ type: String,
+ enum: ["offline", "online", "hybrid"],
+ default: "offline",
+ },
+
+ indoorOutdoor: {
+ type: String,
+ enum: ["indoor", "outdoor", "both"],
+ default: "indoor",
+ },
+
+ price: {
+ type: Number,
+ required: true,
+ },
+
+ pricingUnit: {
+ type: String,
+ enum: ["perhour", "perday"],
+ default: "perhour",
+ },
+
+ capacity: {
+ type: Number,
+ required: true,
+ },
+
+ amenities: [
+ {
+ type: String,
+ },
+ ],
+ rules: [
+ {
+ type: String,
+ },
+ ],
+
+ address: {
+ type: String,
+ required: true,
+ },
+
+ city: String,
+ state: String,
+ pincode: String,
+
+ location: {
+ latitude: {
+ type: Number,
+ default: null,
+ },
+
+ longitude: {
+ type: Number,
+ default: null,
+ },
+ },
+
+ averageRating: {
+ type: Number,
+ default: 0,
+ },
+
+ totalReviews: {
+ type: Number,
+ default: 0,
+ },
+
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+const venueModel = mongoose.model("Venue", venueSchema);
+export default venueModel;
\ No newline at end of file
diff --git a/backend/src/routes/adminRoutes.js b/backend/src/routes/adminRoutes.js
new file mode 100644
index 0000000000..1de6e5a9af
--- /dev/null
+++ b/backend/src/routes/adminRoutes.js
@@ -0,0 +1,36 @@
+import express from "express";
+import userAuthMiddleware from "../middleware/userAuthMiddleware.js";
+import authorizeRoles from "../middleware/authorizeRoles.js";
+import { getDashboardStats, getRecentActivity} from "../controllers/adminDashboardController.js";
+import { getUsers,getUserById,activateUser,deactivateUser} from "../controllers/adminUserController.js";
+import { getVenues,getVenueById,activateVenue,deactivateVenue} from "../controllers/adminVenueController.js";
+import { getBookings,getBookingById} from "../controllers/adminBookingController.js";
+import { getPaymentOrders,getPaymentHistory,getAbandonedPayments} from "../controllers/adminPaymentController.js";
+
+
+const adminRouter = express.Router();
+
+adminRouter.use(userAuthMiddleware);
+adminRouter.use(authorizeRoles("admin"));
+
+adminRouter.get("/dashboard/stats", getDashboardStats);
+adminRouter.get("/dashboard/recent-activity", getRecentActivity);
+
+adminRouter.get("/users", getUsers);
+adminRouter.get("/users/:id", getUserById);
+adminRouter.patch("/users/:id/activate", activateUser);
+adminRouter.patch("/users/:id/deactivate", deactivateUser);
+
+adminRouter.get("/venues", getVenues);
+adminRouter.get("/venues/:id", getVenueById);
+adminRouter.patch("/venues/:id/activate", activateVenue);
+adminRouter.patch("/venues/:id/deactivate", deactivateVenue);
+
+adminRouter.get("/bookings", getBookings);
+adminRouter.get("/bookings/:id", getBookingById);
+
+adminRouter.get("/payments/orders", getPaymentOrders);
+adminRouter.get("/payments/history", getPaymentHistory);
+adminRouter.get("/payments/abandoned", getAbandonedPayments);
+
+export default adminRouter;
diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js
new file mode 100644
index 0000000000..ff6465b29c
--- /dev/null
+++ b/backend/src/routes/authRoutes.js
@@ -0,0 +1,24 @@
+import express from "express";
+import {becomeProvider,forgotPassword,getMe,login,logout,register,resendOtp,resetPassword,updateMe,updateProfileImage,verifyEmail} from "../controllers/authController.js";
+import authMiddleware from "../middleware/userAuthMiddleware.js";
+import upload from "../middleware/upload.js";
+
+
+
+const authRouter = express.Router();
+
+authRouter.post("/register", register);
+authRouter.post("/verify-email", verifyEmail);
+authRouter.post("/resend-otp", resendOtp);
+authRouter.post("/login", login);
+authRouter.post("/forgot-password", forgotPassword);
+authRouter.post("/reset-password", resetPassword);
+authRouter.get("/me", authMiddleware, getMe);
+authRouter.patch("/me", authMiddleware, updateMe);
+authRouter.patch("/me/avatar",authMiddleware,upload.single("profileImage"), updateProfileImage);
+authRouter.post("/logout", logout);
+
+authRouter.patch("/become-provider", authMiddleware, becomeProvider);
+
+
+export default authRouter;
\ No newline at end of file
diff --git a/backend/src/routes/bookingRoutes.js b/backend/src/routes/bookingRoutes.js
new file mode 100644
index 0000000000..b53b4fbf84
--- /dev/null
+++ b/backend/src/routes/bookingRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express';
+import { getMyBookings,getProviderBookings } from '../controllers/bookingController.js';
+import userAuthMiddleware from '../middleware/userAuthMiddleware.js';
+import authorizeRoles from '../middleware/authorizeRoles.js';
+
+const router = express.Router();
+
+router.get("/my-bookings", userAuthMiddleware, getMyBookings);
+router.get("/provider-bookings",userAuthMiddleware,authorizeRoles("provider"),getProviderBookings)
+
+
+export default router;
diff --git a/backend/src/routes/paymentRoutes.js b/backend/src/routes/paymentRoutes.js
new file mode 100644
index 0000000000..4e106c49fb
--- /dev/null
+++ b/backend/src/routes/paymentRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express';
+import userAuthMiddleware from '../middleware/userAuthMiddleware.js';
+import { createOrder, verifyPayment } from '../controllers/paymentController.js';
+
+
+
+const router = express.Router()
+
+router.post("/create-order", userAuthMiddleware, createOrder);
+router.post("/verify-payment", userAuthMiddleware, verifyPayment);
+
+export default router;
\ No newline at end of file
diff --git a/backend/src/routes/venueAvailabilityRoutes.js b/backend/src/routes/venueAvailabilityRoutes.js
new file mode 100644
index 0000000000..43b1a41906
--- /dev/null
+++ b/backend/src/routes/venueAvailabilityRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { activateAvailability, createAvailability, deactivateAvailability, getVenueAvailability } from '../controllers/venueAvailabilityController.js'
+import userAuthMiddleware from "../middleware/userAuthMiddleware.js";
+import authorizeRoles from '../middleware/authorizeRoles.js';
+
+const router = express.Router();
+
+router.post("/create", userAuthMiddleware, authorizeRoles("provider"), createAvailability)
+router.get("/:venueId",getVenueAvailability)
+router.patch("/deactivate/:slotId",userAuthMiddleware,authorizeRoles("provider"), deactivateAvailability);
+router.patch("/activate/:slotId",userAuthMiddleware,authorizeRoles("provider"),activateAvailability);
+export default router;
\ No newline at end of file
diff --git a/backend/src/routes/venueRoutes.js b/backend/src/routes/venueRoutes.js
new file mode 100644
index 0000000000..de749402ee
--- /dev/null
+++ b/backend/src/routes/venueRoutes.js
@@ -0,0 +1,28 @@
+import express from "express";
+import { activateVenue, createVenue, deactivateVenue, getAllVenues, getMyVenues, getPublicVenueById,
+ getVenueById, updateVenue} from "../controllers/venueController.js";
+import userAuthMiddleware from "../middleware/userAuthMiddleware.js";
+import authorizeRoles from "../middleware/authorizeRoles.js";
+import upload from "../middleware/upload.js";
+
+const router = express.Router();
+
+
+router.post("/create", userAuthMiddleware,authorizeRoles("provider"),upload.array("images", 5), createVenue);
+
+router.get("/my-venues",userAuthMiddleware,authorizeRoles("provider"),getMyVenues);
+
+router.get("/provider/:id",userAuthMiddleware,authorizeRoles("provider"),getVenueById);
+
+router.put("/update/:id",userAuthMiddleware,authorizeRoles("provider"),upload.array("images",5),updateVenue);
+
+router.patch("/deactivate/:id",userAuthMiddleware,authorizeRoles("provider"),deactivateVenue);
+
+router.patch("/activate/:id",userAuthMiddleware,authorizeRoles("provider"),activateVenue);
+
+//public apis for veune and details
+router.get("/",getAllVenues)
+router.get('/:venueId', getPublicVenueById)
+
+
+export default router;
\ No newline at end of file
diff --git a/backend/src/utils/cache.js b/backend/src/utils/cache.js
new file mode 100644
index 0000000000..0b66f6409f
--- /dev/null
+++ b/backend/src/utils/cache.js
@@ -0,0 +1,73 @@
+import { redisClient } from "../config/redis.js";
+
+const ACTIVE_VENUES_CACHE_KEY = "venues:active:all";
+const ACTIVE_VENUES_CACHE_TTL = 300; // 5 minutes
+const VENUE_AVAILABILITY_CACHE_TTL = 60; // 1 minute
+
+const getActiveVenueCacheKey = (venueId) => `venues:active:${venueId}`;
+const getVenueAvailabilityCacheKey = (venueId) =>
+ `availability:venue:${venueId}`;
+
+const isRedisReady = () => Boolean(redisClient?.isOpen);
+
+const getCache = async (key) => {
+ try {
+ if (!isRedisReady()) {
+ return null;
+ }
+
+ const cached = await redisClient.get(key);
+ if (!cached) {
+ return null;
+ }
+
+ return JSON.parse(cached);
+ } catch (error) {
+ console.error("Redis getCache error:", error);
+ return null;
+ }
+};
+
+const setCache = async (key, value, ttlSeconds) => {
+ try {
+ if (!isRedisReady()) {
+ return;
+ }
+
+ await redisClient.set(key, JSON.stringify(value), {
+ EX: ttlSeconds,
+ });
+ } catch (error) {
+ console.error("Redis setCache error:", error);
+ }
+};
+
+const delCache = async (key) => {
+ try {
+ if (!isRedisReady()) {
+ return;
+ }
+
+ await redisClient.del(key);
+ } catch (error) {
+ console.error("Redis delCache error:", error);
+ }
+};
+
+const invalidateActiveVenuesCache = async () => {
+ await delCache(ACTIVE_VENUES_CACHE_KEY);
+};
+
+const invalidateActiveVenueCache = async (venueId) => {
+ await delCache(getActiveVenueCacheKey(venueId));
+};
+
+const invalidateVenueAvailabilityCache = async (venueId) => {
+ await delCache(getVenueAvailabilityCacheKey(venueId));
+};
+
+export {
+ getCache, setCache, delCache, getActiveVenueCacheKey, getVenueAvailabilityCacheKey,
+ invalidateActiveVenuesCache, invalidateActiveVenueCache, invalidateVenueAvailabilityCache,
+ ACTIVE_VENUES_CACHE_KEY, ACTIVE_VENUES_CACHE_TTL, VENUE_AVAILABILITY_CACHE_TTL,
+};
diff --git a/backend/src/utils/deleteFromCloudinary.js b/backend/src/utils/deleteFromCloudinary.js
new file mode 100644
index 0000000000..41316a0a3a
--- /dev/null
+++ b/backend/src/utils/deleteFromCloudinary.js
@@ -0,0 +1,12 @@
+import cloudinary from "../config/cloudinary.js";
+
+const deleteFromCloudinary = async (publicId)=>{
+ try {
+ await cloudinary.uploader.destroy(publicId);
+ } catch (error) {
+ console.error("Error deleting from Cloudinary:", error);
+ throw error;
+ }
+}
+
+export default deleteFromCloudinary;
\ No newline at end of file
diff --git a/backend/src/utils/generateOtp.js b/backend/src/utils/generateOtp.js
new file mode 100644
index 0000000000..09e42f4c9f
--- /dev/null
+++ b/backend/src/utils/generateOtp.js
@@ -0,0 +1,7 @@
+const generateOtp = () => {
+ const otp = Math.floor(100000 + Math.random() * 900000).toString();
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1000)
+ return { otp, expiresAt }
+}
+
+export default generateOtp;
diff --git a/backend/src/utils/marketplaceUserFilter.js b/backend/src/utils/marketplaceUserFilter.js
new file mode 100644
index 0000000000..894e2fa900
--- /dev/null
+++ b/backend/src/utils/marketplaceUserFilter.js
@@ -0,0 +1,24 @@
+export const MARKETPLACE_USER_FILTER = {
+ roles: { $nin: ["admin"] },
+};
+
+export const withMarketplaceUserFilter = (filter = {}) => {
+ const hasConditions = Object.keys(filter).length > 0;
+
+ if (!hasConditions) {
+ return { ...MARKETPLACE_USER_FILTER };
+ }
+
+ return {
+ $and: [MARKETPLACE_USER_FILTER, filter],
+ };
+};
+
+export const isPlatformOperator = (user) =>
+ Array.isArray(user?.roles) && user.roles.includes("admin");
+
+export const escapeRegex = (value = "") =>
+ String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+
+export const buildSearchRegex = (value = "") =>
+ new RegExp(escapeRegex(value.trim()), "i");
diff --git a/backend/src/utils/parsePagination.js b/backend/src/utils/parsePagination.js
new file mode 100644
index 0000000000..6d01a4b2e9
--- /dev/null
+++ b/backend/src/utils/parsePagination.js
@@ -0,0 +1,24 @@
+const parsePagination = (query) => {
+ const maxLimit = 100;
+
+ let page = parseInt(query.page, 10);
+ let limit = parseInt(query.limit, 10);
+
+ if (Number.isNaN(page) || page < 1) {
+ page = 1;
+ }
+
+ if (Number.isNaN(limit) || limit < 1) {
+ limit = 10;
+ }
+
+ if (limit > maxLimit) {
+ limit = maxLimit;
+ }
+
+ const skip = (page - 1) * limit;
+
+ return { page, limit, skip };
+};
+
+export default parsePagination;
diff --git a/backend/src/utils/parseSlotTime.js b/backend/src/utils/parseSlotTime.js
new file mode 100644
index 0000000000..80bbade25e
--- /dev/null
+++ b/backend/src/utils/parseSlotTime.js
@@ -0,0 +1,47 @@
+const SLOT_TIME_REGEX =
+ /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i;
+
+/**
+ * Parses a 12-hour slot time string (e.g. "09:00 AM") to minutes from midnight.
+ * Returns null when the value is invalid.
+ */
+export const parseSlotTimeToMinutes = (value) => {
+ if (value == null || typeof value !== "string") {
+ return null;
+ }
+
+ const trimmed = value.trim();
+ const match = trimmed.match(SLOT_TIME_REGEX);
+
+ if (!match) {
+ return null;
+ }
+
+ let hours = Number(match[1]);
+ const minutes = Number(match[2]);
+ const meridiem = match[3].toUpperCase();
+
+ if (
+ !Number.isInteger(hours) ||
+ !Number.isInteger(minutes) ||
+ hours < 1 ||
+ hours > 12 ||
+ minutes < 0 ||
+ minutes > 59
+ ) {
+ return null;
+ }
+
+ if (meridiem === "PM" && hours !== 12) {
+ hours += 12;
+ }
+
+ if (meridiem === "AM" && hours === 12) {
+ hours = 0;
+ }
+
+ return hours * 60 + minutes;
+};
+
+export const isValidSlotTime = (value) =>
+ parseSlotTimeToMinutes(value) !== null;
diff --git a/backend/src/utils/sanitizeUser.js b/backend/src/utils/sanitizeUser.js
new file mode 100644
index 0000000000..459b9822c2
--- /dev/null
+++ b/backend/src/utils/sanitizeUser.js
@@ -0,0 +1,19 @@
+const sanitizeUser = (user) => {
+ if (!user) {
+ return null;
+ }
+
+ const doc = user.toObject ? user.toObject() : user;
+
+ const {
+ password,
+ otp,
+ otpExpiresAt,
+ __v,
+ ...safeUser
+ } = doc;
+
+ return safeUser;
+};
+
+export default sanitizeUser;
diff --git a/backend/src/utils/sendEmail.js b/backend/src/utils/sendEmail.js
new file mode 100644
index 0000000000..34d1494930
--- /dev/null
+++ b/backend/src/utils/sendEmail.js
@@ -0,0 +1,68 @@
+import SibApiV3Sdk from "sib-api-v3-sdk";
+import dotenv from "dotenv";
+
+dotenv.config();
+
+const client = SibApiV3Sdk.ApiClient.instance;
+
+const apiKey = client.authentications["api-key"];
+apiKey.apiKey = process.env.BREVO_API_KEY;
+
+const emailApi = new SibApiV3Sdk.TransactionalEmailsApi();
+
+const sendEmail = async (
+ to,
+ otp,
+ purpose = "verification"
+) => {
+ try {
+ const subject =
+ purpose === "reset"
+ ? "Password Reset OTP"
+ : "Email Verification OTP";
+
+ const htmlContent = `
+
+
${subject}
+
+
Your OTP code is:
+
+
+ ${otp}
+
+
+
+ This OTP will expire in
+ 10 minutes.
+
+
+
+ Ignore this email if you did not request it.
+
+
+ `;
+
+ await emailApi.sendTransacEmail({
+ sender: {
+ email: process.env.SENDER_EMAIL,
+ name: process.env.SENDER_NAME,
+ },
+
+ to: [{ email: to }],
+
+ subject,
+ htmlContent,
+ });
+
+ return true;
+ } catch (error) {
+ console.log(
+ "Email error:",
+ error.response?.body || error.message
+ );
+
+ throw new Error("Failed to send email");
+ }
+};
+
+export default sendEmail;
\ No newline at end of file
diff --git a/backend/src/utils/uploadToCloudinary.js b/backend/src/utils/uploadToCloudinary.js
new file mode 100644
index 0000000000..65749dbafc
--- /dev/null
+++ b/backend/src/utils/uploadToCloudinary.js
@@ -0,0 +1,15 @@
+import cloudinary from "../config/cloudinary.js";
+
+const uploadToCloudinary = async (fileBuffer)=>{
+ return new Promise((resolve, reject)=>{
+ cloudinary.uploader.upload_stream({folder: 'bookmyvenue'}, (error, result)=>{
+ if(error){
+ reject(error);
+ }else{
+ resolve(result);
+ }
+ }).end(fileBuffer);
+ });
+}
+
+export default uploadToCloudinary;
\ No newline at end of file
diff --git a/backend/src/utils/venueCategory.js b/backend/src/utils/venueCategory.js
new file mode 100644
index 0000000000..bb13bed66d
--- /dev/null
+++ b/backend/src/utils/venueCategory.js
@@ -0,0 +1,36 @@
+export const VENUE_CATEGORY_SLUGS = [
+ "wedding",
+ "corporate",
+ "birthday",
+ "party",
+ "function",
+ "photoshoot",
+ "other",
+];
+
+const LEGACY_CATEGORY_MAP = {
+ meetings: "corporate",
+ meeting: "corporate",
+};
+
+export const resolveVenueCategory = (value) => {
+ if (value == null || typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.trim().toLowerCase();
+
+ if (!normalized) {
+ return null;
+ }
+
+ if (VENUE_CATEGORY_SLUGS.includes(normalized)) {
+ return normalized;
+ }
+
+ if (LEGACY_CATEGORY_MAP[normalized]) {
+ return LEGACY_CATEGORY_MAP[normalized];
+ }
+
+ return null;
+};
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000000..cf8df74928
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,7 @@
+# Vite only exposes variables prefixed with VITE_
+# Restart the dev server after changing this file: npm run dev
+
+VITE_API_URL=http://localhost:5000/api
+
+# Public Razorpay key (same as backend RAZORPAY_KEY_ID — never use KEY_SECRET here)
+VITE_RAZORPAY_KEY_ID=rzp_test_your_key_here
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000000..a36934d874
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000000..ea36dd3dc4
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,21 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ parserOptions: { ecmaFeatures: { jsx: true } },
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000000..ea03b1ce13
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+ BookMyVenue
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000000..79290e5aa1
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3144 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.1",
+ "axios": "^1.17.0",
+ "framer-motion": "^12.40.0",
+ "lucide-react": "^1.18.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-hot-toast": "^2.6.0",
+ "react-icons": "^5.6.0",
+ "react-router-dom": "^7.17.0",
+ "tailwindcss": "^4.3.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "vite": "^8.0.12"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
+ "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "tailwindcss": "4.3.1"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "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/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "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/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "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/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
+ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.37",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.372",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
+ "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
+ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.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",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz",
+ "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
+ "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.40.0",
+ "motion-utils": "^12.39.0",
+ "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/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/goober": {
+ "version": "2.1.19",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz",
+ "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/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/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.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz",
+ "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
+ "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.39.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.39.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "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.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-hot-toast": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
+ "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
+ "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
+ "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.17.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "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/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/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000000..02fc1234bd
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.1",
+ "axios": "^1.17.0",
+ "framer-motion": "^12.40.0",
+ "lucide-react": "^1.18.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-hot-toast": "^2.6.0",
+ "react-icons": "^5.6.0",
+ "react-router-dom": "^7.17.0",
+ "tailwindcss": "^4.3.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "vite": "^8.0.12"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000000..6893eb1323
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000000..e9522193d9
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000000..149f4a9392
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,124 @@
+import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
+import { Toaster } from "react-hot-toast";
+
+import { AuthProvider } from "./context/AuthContext";
+
+import PublicLayout from "./layouts/PublicLayout";
+
+import Home from "./pages/guest/Home";
+import Venues from "./pages/guest/Venues";
+import VenueDetails from "./pages/guest/VenueDetails";
+import NotFound from "./pages/guest/NotFound";
+
+import Login from "./pages/auth/Login";
+import Register from "./pages/auth/Register";
+import VerifyEmail from "./pages/auth/VerifyEmail";
+import ForgotPassword from "./pages/auth/ForgotPassword";
+import ResetPassword from "./pages/auth/ResetPassword";
+
+import Profile from "./pages/user/Profile";
+import MyBookings from "./pages/user/MyBookings";
+import ProviderDashboard from "./pages/provider/Dashboard";
+import MyVenues from "./pages/provider/MyVenues";
+import CreateVenue from "./pages/provider/CreateVenue";
+import EditVenue from "./pages/provider/EditVenue";
+import ManageAvailability from "./pages/provider/ManageAvailability";
+import ProviderBookings from "./pages/provider/ProviderBookings";
+
+import AdminDashboard from "./pages/admin/Dashboard";
+import AdminUsers from "./pages/admin/Users";
+import AdminUserDetail from "./pages/admin/UserDetail";
+import AdminVenues from "./pages/admin/Venues";
+import AdminVenueDetail from "./pages/admin/VenueDetail";
+import AdminBookings from "./pages/admin/Bookings";
+import AdminBookingDetail from "./pages/admin/BookingDetail";
+import AdminPayments from "./pages/admin/Payments";
+
+import ProtectedRoute from "./components/protected/ProtectedRoutes";
+import ProviderRoute from "./components/protected/ProviderRoute";
+import MarketplaceGuard from "./components/protected/MarketplaceGuard";
+import AdminRoute from "./components/protected/AdminRoute";
+import ProviderLayout from "./layouts/ProviderLayout";
+import AdminLayout from "./layouts/AdminLayout";
+import ScrollToTop from "./components/common/ScrollToTop";
+
+function App() {
+ return (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ }>
+ }>
+ } />
+ } />
+ } />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+
+
+
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/assets/bmv-logo.jpeg b/frontend/src/assets/bmv-logo.jpeg
new file mode 100644
index 0000000000..341ae00dbd
Binary files /dev/null and b/frontend/src/assets/bmv-logo.jpeg differ
diff --git a/frontend/src/assets/event1.jpg b/frontend/src/assets/event1.jpg
new file mode 100644
index 0000000000..92a80d5ff3
Binary files /dev/null and b/frontend/src/assets/event1.jpg differ
diff --git a/frontend/src/assets/event2.webp b/frontend/src/assets/event2.webp
new file mode 100644
index 0000000000..883ad1a068
Binary files /dev/null and b/frontend/src/assets/event2.webp differ
diff --git a/frontend/src/assets/event3.webp b/frontend/src/assets/event3.webp
new file mode 100644
index 0000000000..440d9ea803
Binary files /dev/null and b/frontend/src/assets/event3.webp differ
diff --git a/frontend/src/assets/event4.webp b/frontend/src/assets/event4.webp
new file mode 100644
index 0000000000..b878579ddf
Binary files /dev/null and b/frontend/src/assets/event4.webp differ
diff --git a/frontend/src/assets/event5.webp b/frontend/src/assets/event5.webp
new file mode 100644
index 0000000000..7f44600c29
Binary files /dev/null and b/frontend/src/assets/event5.webp differ
diff --git a/frontend/src/assets/event6.webp b/frontend/src/assets/event6.webp
new file mode 100644
index 0000000000..0c09cd28c1
Binary files /dev/null and b/frontend/src/assets/event6.webp differ
diff --git a/frontend/src/assets/event7.jfif b/frontend/src/assets/event7.jfif
new file mode 100644
index 0000000000..b400bb0aa7
Binary files /dev/null and b/frontend/src/assets/event7.jfif differ
diff --git a/frontend/src/assets/hero/hero-main.jpg b/frontend/src/assets/hero/hero-main.jpg
new file mode 100644
index 0000000000..065230afca
Binary files /dev/null and b/frontend/src/assets/hero/hero-main.jpg differ
diff --git a/frontend/src/assets/hero/hero-venue-1.jpg b/frontend/src/assets/hero/hero-venue-1.jpg
new file mode 100644
index 0000000000..065230afca
Binary files /dev/null and b/frontend/src/assets/hero/hero-venue-1.jpg differ
diff --git a/frontend/src/assets/hero/hero-venue-2.jpg b/frontend/src/assets/hero/hero-venue-2.jpg
new file mode 100644
index 0000000000..065230afca
Binary files /dev/null and b/frontend/src/assets/hero/hero-venue-2.jpg differ
diff --git a/frontend/src/assets/log-theme.webp b/frontend/src/assets/log-theme.webp
new file mode 100644
index 0000000000..befb9d00ae
Binary files /dev/null and b/frontend/src/assets/log-theme.webp differ
diff --git a/frontend/src/assets/logo.jpeg b/frontend/src/assets/logo.jpeg
new file mode 100644
index 0000000000..065230afca
Binary files /dev/null and b/frontend/src/assets/logo.jpeg differ
diff --git a/frontend/src/assets/reg-theme.jpg b/frontend/src/assets/reg-theme.jpg
new file mode 100644
index 0000000000..6d1a6a851b
Binary files /dev/null and b/frontend/src/assets/reg-theme.jpg differ
diff --git a/frontend/src/components/admin/AdminBrand.jsx b/frontend/src/components/admin/AdminBrand.jsx
new file mode 100644
index 0000000000..5b96b81b64
--- /dev/null
+++ b/frontend/src/components/admin/AdminBrand.jsx
@@ -0,0 +1,38 @@
+import { Link } from "react-router-dom";
+import { Shield } from "lucide-react";
+import Logo from "../../assets/logo.jpeg";
+
+const AdminBrand = ({ compact = false, onClick }) => {
+ return (
+
+
+
+ Admin Console
+
+
+
+
+
+ Book My Venue
+
+
+
+
+ );
+};
+
+export default AdminBrand;
diff --git a/frontend/src/components/admin/AdminDetailRow.jsx b/frontend/src/components/admin/AdminDetailRow.jsx
new file mode 100644
index 0000000000..91472cefe8
--- /dev/null
+++ b/frontend/src/components/admin/AdminDetailRow.jsx
@@ -0,0 +1,12 @@
+const AdminDetailRow = ({ label, value, align = "center" }) => (
+
+
{label}
+ {value}
+
+);
+
+export default AdminDetailRow;
diff --git a/frontend/src/components/admin/AdminFilterSelect.jsx b/frontend/src/components/admin/AdminFilterSelect.jsx
new file mode 100644
index 0000000000..541bf8e386
--- /dev/null
+++ b/frontend/src/components/admin/AdminFilterSelect.jsx
@@ -0,0 +1,28 @@
+const AdminFilterSelect = ({
+ label,
+ value,
+ onChange,
+ options,
+ className = "",
+}) => (
+
+);
+
+export default AdminFilterSelect;
diff --git a/frontend/src/components/admin/AdminPageHeader.jsx b/frontend/src/components/admin/AdminPageHeader.jsx
new file mode 100644
index 0000000000..90fe956f90
--- /dev/null
+++ b/frontend/src/components/admin/AdminPageHeader.jsx
@@ -0,0 +1,15 @@
+const AdminPageHeader = ({ title, description, children }) => (
+
+
+
+ {title}
+
+ {description && (
+
{description}
+ )}
+
+ {children &&
{children}
}
+
+);
+
+export default AdminPageHeader;
diff --git a/frontend/src/components/admin/AdminPagination.jsx b/frontend/src/components/admin/AdminPagination.jsx
new file mode 100644
index 0000000000..6f8c36154b
--- /dev/null
+++ b/frontend/src/components/admin/AdminPagination.jsx
@@ -0,0 +1,54 @@
+import { ChevronLeft, ChevronRight } from "lucide-react";
+
+const AdminPagination = ({
+ page = 1,
+ limit = 20,
+ count = 0,
+ onPageChange,
+}) => {
+ const totalPages = Math.max(1, Math.ceil(count / limit));
+ const canGoPrev = page > 1;
+ const canGoNext = page < totalPages;
+ const start = count === 0 ? 0 : (page - 1) * limit + 1;
+ const end = Math.min(page * limit, count);
+
+ if (count === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Showing {start}–{end} of {count}
+
+
+
+
+
+
+ Page {page} of {totalPages}
+
+
+
+
+
+ );
+};
+
+export default AdminPagination;
diff --git a/frontend/src/components/admin/AdminSearchInput.jsx b/frontend/src/components/admin/AdminSearchInput.jsx
new file mode 100644
index 0000000000..8c32308477
--- /dev/null
+++ b/frontend/src/components/admin/AdminSearchInput.jsx
@@ -0,0 +1,24 @@
+import { Search } from "lucide-react";
+
+const AdminSearchInput = ({
+ value,
+ onChange,
+ placeholder = "Search...",
+ className = "",
+}) => (
+
+);
+
+export default AdminSearchInput;
diff --git a/frontend/src/components/admin/AdminStatCard.jsx b/frontend/src/components/admin/AdminStatCard.jsx
new file mode 100644
index 0000000000..d8f92323e3
--- /dev/null
+++ b/frontend/src/components/admin/AdminStatCard.jsx
@@ -0,0 +1,55 @@
+import { Eye, EyeOff } from "lucide-react";
+
+const AdminStatCard = ({
+ label,
+ value,
+ icon: Icon,
+ iconClass,
+ maskedValue,
+ isValueVisible,
+ onToggleVisibility,
+}) => {
+ const isHideable = Boolean(maskedValue && onToggleVisibility);
+ const displayValue =
+ isHideable && !isValueVisible ? maskedValue : value;
+
+ return (
+
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+ {displayValue}
+
+ {isHideable && (
+
+ )}
+
+
+ {label}
+
+
+
+
+ );
+};
+
+export default AdminStatCard;
diff --git a/frontend/src/components/admin/AdminTable.jsx b/frontend/src/components/admin/AdminTable.jsx
new file mode 100644
index 0000000000..73c0d66173
--- /dev/null
+++ b/frontend/src/components/admin/AdminTable.jsx
@@ -0,0 +1,9 @@
+const AdminTable = ({ children, className = "" }) => (
+
+ {children}
+
+);
+
+export default AdminTable;
diff --git a/frontend/src/components/admin/AdminTextFilter.jsx b/frontend/src/components/admin/AdminTextFilter.jsx
new file mode 100644
index 0000000000..5a30682d1b
--- /dev/null
+++ b/frontend/src/components/admin/AdminTextFilter.jsx
@@ -0,0 +1,23 @@
+const AdminTextFilter = ({
+ label,
+ value,
+ onChange,
+ placeholder = "",
+ type = "text",
+ min,
+ className = "",
+}) => (
+
+);
+
+export default AdminTextFilter;
diff --git a/frontend/src/components/auth/AuthLayout.jsx b/frontend/src/components/auth/AuthLayout.jsx
new file mode 100644
index 0000000000..a080da7cd7
--- /dev/null
+++ b/frontend/src/components/auth/AuthLayout.jsx
@@ -0,0 +1,85 @@
+import { Link } from "react-router-dom";
+import { ArrowLeft, Check } from "lucide-react";
+import bmvLogo from "../../assets/bmv-logo.jpeg";
+
+const AuthLayout = ({
+ brandingTitle,
+ brandingSubtitle,
+ brandingPoints = [],
+ brandingAlign = "end",
+ brandingImage = bmvLogo,
+ brandingImagePosition = "center",
+ children,
+}) => {
+ const brandingPositionClass =
+ brandingAlign === "center" ? "justify-center" : "justify-end";
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AuthLayout;
diff --git a/frontend/src/components/auth/OtpInput.jsx b/frontend/src/components/auth/OtpInput.jsx
new file mode 100644
index 0000000000..5bb7eb8949
--- /dev/null
+++ b/frontend/src/components/auth/OtpInput.jsx
@@ -0,0 +1,110 @@
+import { useRef, useEffect } from "react";
+
+const OTP_LENGTH = 6;
+
+const OtpInput = ({ value, onChange, disabled = false, error }) => {
+ const inputsRef = useRef([]);
+ const digits = value.padEnd(OTP_LENGTH, " ").slice(0, OTP_LENGTH).split("");
+ const errorId = error ? "otp-error" : undefined;
+
+ useEffect(() => {
+ inputsRef.current[0]?.focus();
+ }, []);
+
+ const updateValue = (nextDigits) => {
+ onChange(nextDigits.join("").replace(/\s/g, ""));
+ };
+
+ const handleChange = (index, char) => {
+ const sanitized = char.replace(/\D/g, "");
+ const next = [...digits];
+
+ if (!sanitized) {
+ next[index] = " ";
+ updateValue(next);
+ return;
+ }
+
+ next[index] = sanitized.slice(-1);
+ updateValue(next);
+
+ if (index < OTP_LENGTH - 1) {
+ inputsRef.current[index + 1]?.focus();
+ }
+ };
+
+ const handleKeyDown = (index, event) => {
+ if (event.key === "Backspace") {
+ event.preventDefault();
+ const next = [...digits];
+
+ if (digits[index] && digits[index] !== " ") {
+ next[index] = " ";
+ updateValue(next);
+ return;
+ }
+
+ if (index > 0) {
+ next[index - 1] = " ";
+ updateValue(next);
+ inputsRef.current[index - 1]?.focus();
+ }
+ }
+ };
+
+ const handlePaste = (event) => {
+ event.preventDefault();
+ const pasted = event.clipboardData
+ .getData("text")
+ .replace(/\D/g, "")
+ .slice(0, OTP_LENGTH);
+
+ if (!pasted) return;
+
+ updateValue(pasted.padEnd(OTP_LENGTH, " ").split(""));
+ inputsRef.current[Math.min(pasted.length, OTP_LENGTH - 1)]?.focus();
+ };
+
+ return (
+
+
+ {digits.map((digit, index) => (
+ {
+ inputsRef.current[index] = el;
+ }}
+ type="text"
+ inputMode="numeric"
+ autoComplete={index === 0 ? "one-time-code" : "off"}
+ maxLength={1}
+ value={digit.trim()}
+ disabled={disabled}
+ aria-invalid={Boolean(error)}
+ onChange={(event) => handleChange(index, event.target.value)}
+ onKeyDown={(event) => handleKeyDown(index, event)}
+ onPaste={handlePaste}
+ onFocus={(event) => event.target.select()}
+ className={`h-12 w-full min-w-0 flex-1 rounded-lg border bg-white text-center text-lg font-semibold text-gray-900 shadow-sm focus:outline-none focus:ring-2 disabled:opacity-70 ${
+ error
+ ? "border-red-400 focus:border-red-500 focus:ring-red-100"
+ : "border-gray-300 focus:border-red-500 focus:ring-red-100"
+ }`}
+ />
+ ))}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default OtpInput;
diff --git a/frontend/src/components/auth/PasswordInput.jsx b/frontend/src/components/auth/PasswordInput.jsx
new file mode 100644
index 0000000000..6b81ddfe02
--- /dev/null
+++ b/frontend/src/components/auth/PasswordInput.jsx
@@ -0,0 +1,61 @@
+import { useState } from "react";
+import { Eye, EyeOff } from "lucide-react";
+
+const inputClass =
+ "w-full rounded-lg border border-gray-300 bg-white px-3.5 py-3 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-100 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70";
+
+const PasswordInput = ({
+ id,
+ name,
+ value,
+ onChange,
+ placeholder = "Enter your password",
+ autoComplete = "current-password",
+ disabled = false,
+ error,
+}) => {
+ const [visible, setVisible] = useState(false);
+ const errorId = error ? `${id}-error` : undefined;
+
+ return (
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default PasswordInput;
diff --git a/frontend/src/components/bookings/BookingFiltersBar.jsx b/frontend/src/components/bookings/BookingFiltersBar.jsx
new file mode 100644
index 0000000000..2e063cad55
--- /dev/null
+++ b/frontend/src/components/bookings/BookingFiltersBar.jsx
@@ -0,0 +1,52 @@
+import { Search } from "lucide-react";
+import { BOOKING_FILTERS } from "../../utils/bookingFilters";
+
+const BookingFiltersBar = ({
+ activeFilter,
+ onFilterChange,
+ searchQuery,
+ onSearchChange,
+ searchPlaceholder = "Search by venue name",
+ filters = BOOKING_FILTERS,
+}) => (
+
+
+ {filters.map((filter) => {
+ const isActive = activeFilter === filter.id;
+
+ return (
+
+ );
+ })}
+
+
+
+
+);
+
+export default BookingFiltersBar;
diff --git a/frontend/src/components/bookings/BookingSummary.jsx b/frontend/src/components/bookings/BookingSummary.jsx
new file mode 100644
index 0000000000..5aa4b5273c
--- /dev/null
+++ b/frontend/src/components/bookings/BookingSummary.jsx
@@ -0,0 +1,63 @@
+import { CalendarCheck, CalendarClock, CalendarX, LayoutList } from "lucide-react";
+
+const statConfig = [
+ {
+ key: "total",
+ label: "Total Bookings",
+ icon: LayoutList,
+ iconClass: "bg-red-50 text-red-600",
+ },
+ {
+ key: "upcoming",
+ label: "Upcoming",
+ icon: CalendarClock,
+ iconClass: "bg-emerald-50 text-emerald-600",
+ },
+ {
+ key: "completed",
+ label: "Completed",
+ icon: CalendarCheck,
+ iconClass: "bg-sky-50 text-sky-600",
+ },
+ {
+ key: "cancelled",
+ label: "Cancelled",
+ icon: CalendarX,
+ iconClass: "bg-gray-100 text-gray-600",
+ },
+];
+
+const BookingSummary = ({ stats, hideCancelled = false }) => {
+ const visibleStats = hideCancelled
+ ? statConfig.filter((item) => item.key !== "cancelled")
+ : statConfig;
+
+ return (
+
+ {visibleStats.map(({ key, label, icon: Icon, iconClass }) => (
+
+
+
+
+
+
+
+ {stats[key] ?? 0}
+
+
+ {label}
+
+
+
+
+ ))}
+
+ );
+};
+
+export default BookingSummary;
diff --git a/frontend/src/components/bookings/BookingsEmptyState.jsx b/frontend/src/components/bookings/BookingsEmptyState.jsx
new file mode 100644
index 0000000000..42b47b710c
--- /dev/null
+++ b/frontend/src/components/bookings/BookingsEmptyState.jsx
@@ -0,0 +1,25 @@
+import { Link } from "react-router-dom";
+import { CalendarDays } from "lucide-react";
+
+const BookingsEmptyState = () => (
+
+
+
+
+
+
No bookings yet
+
+ Your reservations will appear here once you book a venue. Browse venues to
+ find your next event space.
+
+
+
+ Browse Venues
+
+
+);
+
+export default BookingsEmptyState;
diff --git a/frontend/src/components/common/BookingCard.jsx b/frontend/src/components/common/BookingCard.jsx
new file mode 100644
index 0000000000..53e4e0fdfa
--- /dev/null
+++ b/frontend/src/components/common/BookingCard.jsx
@@ -0,0 +1,115 @@
+import { MapPin } from "lucide-react";
+import { formatSlotLabel,formatTimeRange,} from "../../utils/formatDate";
+import { formatPrice } from "../../utils/formatPrice";
+import { resolvePopulatedRef } from "../../utils/booking";
+import { getBookingDisplayStatus } from "../../utils/bookingFilters";
+import { getVenueCoverUrl } from "../../utils/venue";
+
+const statusToneStyles = {
+ upcoming: "bg-emerald-50 text-emerald-700 ring-emerald-100",
+ completed: "bg-sky-50 text-sky-700 ring-sky-100",
+ cancelled: "bg-gray-100 text-gray-600 ring-gray-200",
+};
+
+const formatCompactDate = (date) => {
+ if (!date) return "—";
+
+ return new Date(date).toLocaleDateString("en-IN", {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ });
+};
+
+const BookingCard = ({ booking }) => {
+ const venue = resolvePopulatedRef(booking?.venueId);
+ const slot = resolvePopulatedRef(booking?.availabilityId);
+ const { amount } = formatPrice(booking?.amount);
+ const displayStatus = getBookingDisplayStatus(booking);
+
+ const slotDate = slot?.date ? formatCompactDate(slot.date) : "—";
+ const slotTime = slot
+ ? formatTimeRange(slot.startTime, slot.endTime)
+ : "—";
+ const slotLabel = slot?.slotLabel ? formatSlotLabel(slot.slotLabel) : null;
+ const coverUrl = getVenueCoverUrl(venue);
+ const location =
+ venue?.city || venue?.state
+ ? [venue.city, venue.state].filter(Boolean).join(", ")
+ : venue?.address || "Location unavailable";
+
+ return (
+
+
+
+
+ {coverUrl ? (
+

+ ) : (
+
+ No image
+
+ )}
+
+
+
+
+ {venue?.title || "Venue unavailable"}
+
+
+
+
+ {location}
+
+
+
+ {slotDate}
+ {slotLabel ? ` · ${slotLabel}` : ""}
+ {slotTime !== "—" ? ` · ${slotTime}` : ""}
+
+
+
+ Ref: {booking.bookingReference || "—"}
+
+
+
+
+
+
{slotDate}
+ {slotLabel && (
+
{slotLabel}
+ )}
+
{slotTime}
+
+
+
+
+ Reference
+
+
+ {booking.bookingReference || "—"}
+
+
+
+
+
+ {displayStatus.label}
+
+
+
{amount}
+
+
+
+ );
+};
+
+export default BookingCard;
diff --git a/frontend/src/components/common/BrandName.jsx b/frontend/src/components/common/BrandName.jsx
new file mode 100644
index 0000000000..79a1e30547
--- /dev/null
+++ b/frontend/src/components/common/BrandName.jsx
@@ -0,0 +1,46 @@
+import Logo from "../../assets/logo.jpeg";
+
+const variantStyles = {
+ navbar: {
+ wrapper: "gap-2.5 sm:gap-3",
+ logo: "h-11 w-auto object-contain sm:h-12",
+ text: "font-brand text-lg text-red-600 sm:text-xl lg:text-2xl",
+ },
+ inline: {
+ wrapper: "gap-1.5",
+ logo: "h-5 w-auto object-contain sm:h-6",
+ text: "font-brand text-base text-red-400 sm:text-lg",
+ },
+ inlineLight: {
+ wrapper: "",
+ text: "font-brand text-base text-red-600 sm:text-lg",
+ },
+ heading: {
+ wrapper: "",
+ logo: "h-6 w-auto object-contain sm:h-7",
+ text: "font-brand text-2xl text-red-600 sm:text-3xl",
+ },
+};
+
+const BrandName = ({ variant = "navbar", showLogo, className = "" }) => {
+ const styles = variantStyles[variant] ?? variantStyles.navbar;
+ const displayLogo = showLogo ?? true;
+
+ return (
+
+ {displayLogo && (
+
+ )}
+ Book My Venue
+
+ );
+};
+
+export default BrandName;
diff --git a/frontend/src/components/common/ConfirmModal.jsx b/frontend/src/components/common/ConfirmModal.jsx
new file mode 100644
index 0000000000..39ce20e2d0
--- /dev/null
+++ b/frontend/src/components/common/ConfirmModal.jsx
@@ -0,0 +1,57 @@
+const ConfirmModal = ({
+ open,
+ title,
+ message,
+ confirmLabel = "Confirm",
+ cancelLabel = "Cancel",
+ isLoading = false,
+ onConfirm,
+ onCancel,
+}) => {
+ if (!open) return null;
+
+ return (
+
+
+
+
+
+ {title}
+
+
{message}
+
+
+
+
+
+
+
+ );
+};
+
+export default ConfirmModal;
diff --git a/frontend/src/components/common/EmptyState.jsx b/frontend/src/components/common/EmptyState.jsx
new file mode 100644
index 0000000000..e59dc29831
--- /dev/null
+++ b/frontend/src/components/common/EmptyState.jsx
@@ -0,0 +1,16 @@
+const EmptyState = ({ title, description }) => {
+ return (
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ );
+};
+
+export default EmptyState;
\ No newline at end of file
diff --git a/frontend/src/components/common/ErrorState.jsx b/frontend/src/components/common/ErrorState.jsx
new file mode 100644
index 0000000000..80694851c5
--- /dev/null
+++ b/frontend/src/components/common/ErrorState.jsx
@@ -0,0 +1,23 @@
+const ErrorState = ({ message, onRetry }) => {
+ return (
+
+
+ Something went wrong
+
+
+
{message}
+
+ {onRetry && (
+
+ )}
+
+ );
+};
+
+export default ErrorState;
diff --git a/frontend/src/components/common/Footer.jsx b/frontend/src/components/common/Footer.jsx
new file mode 100644
index 0000000000..671801ceab
--- /dev/null
+++ b/frontend/src/components/common/Footer.jsx
@@ -0,0 +1,148 @@
+import { Link } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import BrandName from "./BrandName";
+
+const footerLinkClass =
+ "text-sm text-gray-600 transition-colors duration-200 hover:text-red-600";
+
+const footerHeadingClass =
+ "text-xs font-semibold uppercase tracking-[0.14em] text-gray-900";
+
+const scrollToProviderCta = (event) => {
+ if (window.location.pathname !== "/") return;
+
+ event.preventDefault();
+
+ document.getElementById("provider-cta")?.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+
+ window.history.replaceState(null, "", "/#provider-cta");
+};
+
+const Footer = () => {
+ const { loading, authReady, isAuthenticated, isProvider } = useAuth();
+ const authSettled = !loading && authReady;
+
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/frontend/src/components/common/Loader.jsx b/frontend/src/components/common/Loader.jsx
new file mode 100644
index 0000000000..550e3cbed1
--- /dev/null
+++ b/frontend/src/components/common/Loader.jsx
@@ -0,0 +1,14 @@
+const Loader = ({ label = "Loading..." }) => {
+ return (
+
+ );
+};
+
+export default Loader;
diff --git a/frontend/src/components/common/Navbar.jsx b/frontend/src/components/common/Navbar.jsx
new file mode 100644
index 0000000000..0cc2ef47e3
--- /dev/null
+++ b/frontend/src/components/common/Navbar.jsx
@@ -0,0 +1,253 @@
+import { useState } from "react";
+import { Link, NavLink } from "react-router-dom";
+import { Building2, CalendarCheck, Home, LayoutDashboard, Menu, X, User, } from "lucide-react";
+import { useAuth } from "../../context/AuthContext";
+import BrandName from "./BrandName";
+
+const desktopNavLinkClass = ({ isActive }) =>
+ [
+ "px-4 py-2 text-sm font-medium transition-colors duration-300",
+ isActive
+ ? "text-red-600"
+ : "text-gray-600 hover:text-red-600",
+ ].join(" ");
+
+const mobileDrawerNavClass = ({ isActive }) =>
+ [
+ "flex items-center gap-3 px-4 py-3 text-sm font-medium transition-colors duration-300",
+ isActive
+ ? "text-red-600"
+ : "text-gray-600 hover:text-red-600",
+ ].join(" ");
+
+const dashboardLinkClass =
+ "hidden items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 lg:inline-flex";
+
+const Navbar = () => {
+ const { user, loading, authReady, userRoles, isAuthenticated } = useAuth();
+ const [menuOpen, setMenuOpen] = useState(false);
+ const isProvider = userRoles.includes("provider");
+ const userInitial = user?.name?.charAt(0)?.toUpperCase();
+
+ const closeMenu = () => setMenuOpen(false);
+ const toggleMenu = () => setMenuOpen((open) => !open);
+
+ const profileAvatarClass =
+ "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full border border-gray-200 bg-gradient-to-br from-red-50 to-white text-sm font-semibold text-red-600 shadow-sm transition-all duration-200 hover:border-red-200 hover:shadow-md";
+
+ const menuButtonClass = [
+ "flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 lg:hidden",
+ ].join(" ");
+
+ const renderMobileNavLinks = () => (
+ <>
+
+
+ Home
+
+
+
+
+ Venues
+
+
+ {isAuthenticated && (
+
+
+ My Bookings
+
+ )}
+
+ {isAuthenticated && isProvider && (
+
+
+ Dashboard
+
+ )}
+
+ {!isAuthenticated && (
+
+ Get started
+
+ )}
+ >
+ );
+
+ const renderDesktopPillNav = () => (
+
+ );
+
+ return (
+
+ );
+};
+
+export default Navbar;
diff --git a/frontend/src/components/common/ScrollToTop.jsx b/frontend/src/components/common/ScrollToTop.jsx
new file mode 100644
index 0000000000..26d6789f2e
--- /dev/null
+++ b/frontend/src/components/common/ScrollToTop.jsx
@@ -0,0 +1,34 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
+
+const ScrollToTop = () => {
+ const { pathname, hash } = useLocation();
+
+ useEffect(() => {
+ if (hash) {
+ const id = hash.replace("#", "");
+
+ const scrollToTarget = () => {
+ const element = document.getElementById(id);
+ if (element) {
+ element.scrollIntoView({ behavior: "smooth", block: "start" });
+ return true;
+ }
+ return false;
+ };
+
+ if (!scrollToTarget()) {
+ const timer = window.setTimeout(scrollToTarget, 100);
+ return () => window.clearTimeout(timer);
+ }
+
+ return;
+ }
+
+ window.scrollTo({ top: 0, left: 0, behavior: "auto" });
+ }, [pathname, hash]);
+
+ return null;
+};
+
+export default ScrollToTop;
diff --git a/frontend/src/components/home/FeaturedVenueCard.jsx b/frontend/src/components/home/FeaturedVenueCard.jsx
new file mode 100644
index 0000000000..e0a5933c47
--- /dev/null
+++ b/frontend/src/components/home/FeaturedVenueCard.jsx
@@ -0,0 +1,79 @@
+import { Link } from "react-router-dom";
+import { MapPin, Users } from "lucide-react";
+import { getVenueCoverUrl } from "../../utils/venue";
+import { getCategoryLabel } from "../../utils/venueFilters";
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+
+const formatVenueLocation = (venue) => {
+ const parts = [venue.city, venue.state].filter(Boolean);
+
+ if (parts.length > 0) {
+ return `${parts.join(", ")}, India`;
+ }
+
+ return venue.address || "Location not specified";
+};
+
+const FeaturedVenueCard = ({ venue }) => {
+ const coverUrl = getVenueCoverUrl(venue);
+ const priceLabel = formatBookingPriceDisplay(venue?.price);
+
+ return (
+
+ {coverUrl ? (
+
+ ) : (
+
+ No image
+
+ )}
+
+
+
+ {venue.category && (
+
+ {getCategoryLabel(venue.category)}
+
+ )}
+
+
+
+ {venue.title}
+
+
+
+
+ {formatVenueLocation(venue)}
+
+
+
+ {priceLabel}
+
+
+
+
+ Up to {venue.capacity ?? "—"} guests
+
+
+
+ );
+};
+
+export default FeaturedVenueCard;
diff --git a/frontend/src/components/home/FeaturedVenues.jsx b/frontend/src/components/home/FeaturedVenues.jsx
new file mode 100644
index 0000000000..3e8c647554
--- /dev/null
+++ b/frontend/src/components/home/FeaturedVenues.jsx
@@ -0,0 +1,120 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { ArrowRight } from "lucide-react";
+import { getAllVenues } from "../../services/venueService";
+import FeaturedVenueCard from "./FeaturedVenueCard";
+import VenueCardGrid from "../venues/VenueCardGrid";
+import Loader from "../common/Loader";
+import EmptyState from "../common/EmptyState";
+import ErrorState from "../common/ErrorState";
+
+const FEATURED_LIMIT = 6;
+
+const FeaturedVenues = () => {
+ const [venues, setVenues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const fetchFeaturedVenues = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAllVenues();
+
+ if (data.success) {
+ setVenues((data.data ?? []).slice(0, FEATURED_LIMIT));
+ } else {
+ setVenues([]);
+ setError(data.message || "Failed to load featured venues.");
+ }
+ } catch (err) {
+ setVenues([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load featured venues. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchFeaturedVenues();
+ }, []);
+
+ return (
+
+
+
+ Recent picks
+
+
+
+
+ Featured Venues
+
+
+ Hand-picked spaces for weddings, corporate events, and celebrations
+
+
+
+
+
+
+
+
+
+
+ {loading &&
}
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && venues.length === 0 && (
+
+
+
+
+ Go to venue listings
+
+
+ )}
+
+ {!loading && !error && venues.length > 0 && (
+ <>
+
+ {venues.map((venue) => (
+
+ ))}
+
+
+
+
+ View all venues
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default FeaturedVenues;
diff --git a/frontend/src/components/home/Hero.jsx b/frontend/src/components/home/Hero.jsx
new file mode 100644
index 0000000000..38745e783c
--- /dev/null
+++ b/frontend/src/components/home/Hero.jsx
@@ -0,0 +1,127 @@
+import { Link } from "react-router-dom";
+import { ArrowRight } from "lucide-react";
+import { useAuth } from "../../context/AuthContext";
+import event1 from "../../assets/event1.jpg";
+import event2 from "../../assets/event2.webp";
+import event4 from "../../assets/event4.webp";
+import event6 from "../../assets/event6.webp";
+import event7 from "../../assets/event7.jfif";
+
+const EVENT_IMAGES = [
+ { src: event1, alt: "Outdoor wedding venue setup" },
+ { src: event2, alt: "Elegant banquet hall" },
+ { src: event4, alt: "Garden party venue" },
+ { src: event6, alt: "Decorated event mandap" },
+ { src: event7, alt: "Premium event space" },
+];
+
+const U_CURVE_OFFSETS = [
+ "translate-y-0",
+ "translate-y-3 sm:translate-y-5",
+ "translate-y-8 sm:translate-y-10 md:translate-y-12",
+ "translate-y-3 sm:translate-y-5",
+ "translate-y-0",
+];
+
+const HeroPremiumBackdrop = () => (
+
+);
+
+const Hero = () => {
+ const { user, loading, authReady } = useAuth();
+
+ const scrollToProviderCta = (event) => {
+ if (window.location.pathname !== "/") return;
+
+ event.preventDefault();
+
+ document.getElementById("provider-cta")?.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+
+ window.history.replaceState(null, "", "/#provider-cta");
+ };
+
+ return (
+
+
+
+
+
+
+ Find your space
+
+
+
+
+ Find the Perfect Venue
+
+
+ for Every Celebration, Event, and Milestone That Matters Most
+
+
+
+
+
+
+
+
+
+
+
+ Browse venues
+
+
+
+ {!loading && authReady && !user && (
+
+ Become a provider
+
+ )}
+
+
+
+
+
+
+ {EVENT_IMAGES.map((image, index) => (
+
+
+

+
+
+ ))}
+
+
+
+ );
+};
+
+export default Hero;
diff --git a/frontend/src/components/home/HowItWorks.jsx b/frontend/src/components/home/HowItWorks.jsx
new file mode 100644
index 0000000000..6bb37b02cd
--- /dev/null
+++ b/frontend/src/components/home/HowItWorks.jsx
@@ -0,0 +1,82 @@
+import {Building2,FileSearch,CalendarCheck,CheckCircle2} from "lucide-react";
+
+const steps = [
+ {
+ title: "Browse Venues",
+ description:
+ "Discover venues by city, category, and capacity for your event.",
+ icon: Building2,
+ },
+ {
+ title: "View Venue Details",
+ description:
+ "See photos, pricing, capacity, and available slots before you book.",
+ icon: FileSearch,
+ },
+ {
+ title: "Book Your Slot",
+ description:
+ "Choose a date and time slot that works for your celebration.",
+ icon: CalendarCheck,
+ },
+ {
+ title: "Pay & Confirm",
+ description:
+ "Complete secure online payment and receive instant booking confirmation.",
+ icon: CheckCircle2,
+ },
+];
+
+const HowItWorksBackdrop = () => (
+
+);
+
+const HowItWorks = () => {
+ return (
+
+
+
+
+
+ How it Works
+
+
+
+ {steps.map(({ title, description, icon: Icon }) => (
+
+
+
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default HowItWorks;
diff --git a/frontend/src/components/home/ProviderCTA.jsx b/frontend/src/components/home/ProviderCTA.jsx
new file mode 100644
index 0000000000..4aecc2a040
--- /dev/null
+++ b/frontend/src/components/home/ProviderCTA.jsx
@@ -0,0 +1,164 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { ArrowRight } from "lucide-react";
+import toast from "react-hot-toast";
+import { useAuth } from "../../context/AuthContext";
+import { becomeProvider } from "../../services/authService";
+import BecomeHostModal from "../provider/onboarding/BecomeHostModal";
+import providerCtaImage from "../../assets/hero/hero-venue-1.jpg";
+
+const ProviderCTA = () => {
+ const navigate = useNavigate();
+ const {
+ user,
+ loading,
+ authReady,
+ isProvider,
+ syncUserAfterBecomeProvider,
+ } = useAuth();
+ const [modalOpen, setModalOpen] = useState(false);
+ const [upgrading, setUpgrading] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleClick = () => {
+ if (!user) {
+ navigate("/login", { state: { from: "/" } });
+ return;
+ }
+
+ if (isProvider) {
+ navigate("/provider/dashboard");
+ return;
+ }
+
+ setError("");
+ setModalOpen(true);
+ };
+
+ const handleCloseModal = () => {
+ if (upgrading) return;
+ setModalOpen(false);
+ setError("");
+ };
+
+ const handleConfirmBecomeHost = async () => {
+ if (upgrading || isProvider) return;
+
+ try {
+ setUpgrading(true);
+ setError("");
+
+ const data = await becomeProvider();
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to upgrade to provider.");
+ }
+
+ const roles = await syncUserAfterBecomeProvider(data.roles);
+
+ if (!roles?.includes("provider")) {
+ throw new Error(
+ "Provider upgrade could not be confirmed. Please refresh and try again."
+ );
+ }
+
+ setModalOpen(false);
+ toast.success(data.message || "You are now a provider!");
+ navigate("/provider/dashboard");
+ } catch (err) {
+ const message =
+ err.response?.data?.message ||
+ err.message ||
+ "Unable to become a provider. Please try again.";
+ setError(message);
+ toast.error(message);
+ } finally {
+ setUpgrading(false);
+ }
+ };
+
+ const buttonLabel = isProvider
+ ? "Go to Provider Dashboard"
+ : "Host a Venue";
+
+ const heading = isProvider ? "Manage your venues" : "Are you a venue owner?";
+ const subheading = isProvider
+ ? "Access your Provider Dashboard"
+ : "List your venues";
+ const description = isProvider
+ ? "Update listings, manage availability, and track reservations across your venues from one place."
+ : "List your space on Book My Venue, manage availability, accept bookings, and track reservations from your provider dashboard, all in one place.";
+
+ return (
+ <>
+
+
+
+
+
+ {heading}
+
+
+
+ {subheading}
+
+
+
+ {description}
+
+
+ {!loading && authReady && (
+
+ )}
+
+ {error && !modalOpen && (
+
+ {error}
+
+ )}
+
+ {!loading && authReady && user && !isProvider && !error && (
+
+ Start hosting your venue and manage bookings from your provider
+ dashboard.
+
+ )}
+
+
+
+
+

+
+
+
+
+
+
+ {modalOpen ? (
+
+ ) : null}
+ >
+ );
+};
+
+export default ProviderCTA;
diff --git a/frontend/src/components/profile/ProfileField.jsx b/frontend/src/components/profile/ProfileField.jsx
new file mode 100644
index 0000000000..a0f37beb8f
--- /dev/null
+++ b/frontend/src/components/profile/ProfileField.jsx
@@ -0,0 +1,85 @@
+const inputClass =
+ "mt-1 w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-100 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70";
+
+const hasDisplayValue = (value) =>
+ value !== undefined && value !== null && String(value).trim() !== "";
+
+const ProfileField = ({
+ label,
+ value,
+ placeholder,
+ children,
+ className = "",
+ isEditing = false,
+ name,
+ onChange,
+ inputType = "text",
+ options = [],
+ rows = 3,
+}) => {
+ if (isEditing) {
+ const fieldId = name || label.toLowerCase().replace(/\s+/g, "-");
+
+ return (
+
+
+
+ {inputType === "select" ? (
+
+ ) : inputType === "textarea" ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+ {label}
+
+ {hasDisplayValue(value) ? (
+
{children ?? value}
+ ) : (
+
{placeholder}
+ )}
+
+ );
+};
+
+export default ProfileField;
diff --git a/frontend/src/components/profile/ProfileHeader.jsx b/frontend/src/components/profile/ProfileHeader.jsx
new file mode 100644
index 0000000000..511f9069b8
--- /dev/null
+++ b/frontend/src/components/profile/ProfileHeader.jsx
@@ -0,0 +1,133 @@
+import { useRef } from "react";
+import { Camera, Loader2, Pencil, User } from "lucide-react";
+
+const formatRole = (role) => role.charAt(0).toUpperCase() + role.slice(1);
+
+const roleStyles = {
+ customer: "bg-gray-100 text-gray-700",
+ provider: "bg-red-50 text-red-700",
+ admin: "bg-violet-50 text-violet-700",
+};
+
+const ProfileHeader = ({
+ user,
+ roles,
+ onEditProfile,
+ isEditing = false,
+ avatarSrc,
+ isUploadingImage = false,
+ onProfileImageSelect,
+}) => {
+ const fileInputRef = useRef(null);
+ const initial = user?.name?.charAt(0)?.toUpperCase() ?? "?";
+ const displaySrc = avatarSrc || user?.profileImage;
+
+ const handleCameraClick = () => {
+ if (isUploadingImage) return;
+ fileInputRef.current?.click();
+ };
+
+ const handleFileChange = (event) => {
+ const file = event.target.files?.[0];
+ if (file) {
+ onProfileImageSelect(file);
+ }
+ event.target.value = "";
+ };
+
+ return (
+
+
+
+
+ {displaySrc ? (
+

+ ) : (
+
+
+ {initial}
+
+
+ )}
+
+ {isUploadingImage && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {user?.name}
+
+
+
+
+ {user?.email}
+
+
+
+ {roles.map((role) => (
+
+ {formatRole(role)}
+
+ ))}
+
+
+
+
+ {!isEditing && (
+
+ )}
+
+
+ );
+};
+
+export default ProfileHeader;
diff --git a/frontend/src/components/profile/ProfileSection.jsx b/frontend/src/components/profile/ProfileSection.jsx
new file mode 100644
index 0000000000..3b681b9dde
--- /dev/null
+++ b/frontend/src/components/profile/ProfileSection.jsx
@@ -0,0 +1,10 @@
+const ProfileSection = ({ title, children, className = "" }) => (
+
+);
+
+export default ProfileSection;
diff --git a/frontend/src/components/profile/StatusBadge.jsx b/frontend/src/components/profile/StatusBadge.jsx
new file mode 100644
index 0000000000..2f72df8430
--- /dev/null
+++ b/frontend/src/components/profile/StatusBadge.jsx
@@ -0,0 +1,21 @@
+const StatusBadge = ({ icon: Icon, label, tone = "neutral" }) => {
+ const toneStyles = {
+ success: "bg-emerald-50 text-emerald-700 ring-emerald-100",
+ warning: "bg-amber-50 text-amber-700 ring-amber-100",
+ danger: "bg-red-50 text-red-700 ring-red-100",
+ neutral: "bg-gray-100 text-gray-600 ring-gray-200",
+ };
+
+ return (
+
+ {Icon && }
+ {label}
+
+ );
+};
+
+export default StatusBadge;
diff --git a/frontend/src/components/protected/AdminRoute.jsx b/frontend/src/components/protected/AdminRoute.jsx
new file mode 100644
index 0000000000..4978bc105c
--- /dev/null
+++ b/frontend/src/components/protected/AdminRoute.jsx
@@ -0,0 +1,34 @@
+import { Navigate, Outlet, useLocation } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import Loader from "../common/Loader";
+
+const AdminRoute = () => {
+ const { user, loading, authReady, isAdmin } = useAuth();
+ const location = useLocation();
+
+ if (loading || !authReady) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ if (!isAdmin) {
+ return ;
+ }
+
+ return ;
+};
+
+export default AdminRoute;
diff --git a/frontend/src/components/protected/MarketplaceGuard.jsx b/frontend/src/components/protected/MarketplaceGuard.jsx
new file mode 100644
index 0000000000..55cc4540aa
--- /dev/null
+++ b/frontend/src/components/protected/MarketplaceGuard.jsx
@@ -0,0 +1,14 @@
+import { Navigate, Outlet } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+
+const MarketplaceGuard = () => {
+ const { user, authReady, isAdmin } = useAuth();
+
+ if (authReady && user && isAdmin) {
+ return ;
+ }
+
+ return ;
+};
+
+export default MarketplaceGuard;
diff --git a/frontend/src/components/protected/ProtectedRoutes.jsx b/frontend/src/components/protected/ProtectedRoutes.jsx
new file mode 100644
index 0000000000..e71cfeb63f
--- /dev/null
+++ b/frontend/src/components/protected/ProtectedRoutes.jsx
@@ -0,0 +1,30 @@
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import Loader from "../common/Loader";
+
+const ProtectedRoute = ({ children }) => {
+ const { user, loading, authReady } = useAuth();
+ const location = useLocation();
+
+ if (loading || !authReady) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ return children;
+};
+
+export default ProtectedRoute;
diff --git a/frontend/src/components/protected/ProviderRoute.jsx b/frontend/src/components/protected/ProviderRoute.jsx
new file mode 100644
index 0000000000..87dd3badeb
--- /dev/null
+++ b/frontend/src/components/protected/ProviderRoute.jsx
@@ -0,0 +1,38 @@
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "../../context/AuthContext";
+import Loader from "../common/Loader";
+
+const ProviderRoute = ({ children }) => {
+ const { user, loading, authReady, isProvider, isAdmin } = useAuth();
+ const location = useLocation();
+
+ if (loading || !authReady) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ if (isAdmin) {
+ return ;
+ }
+
+ if (!isProvider) {
+ return ;
+ }
+
+ return children;
+};
+
+export default ProviderRoute;
diff --git a/frontend/src/components/provider/ProviderBrand.jsx b/frontend/src/components/provider/ProviderBrand.jsx
new file mode 100644
index 0000000000..812f61d88b
--- /dev/null
+++ b/frontend/src/components/provider/ProviderBrand.jsx
@@ -0,0 +1,43 @@
+import { Link } from "react-router-dom";
+import { Crown } from "lucide-react";
+import Logo from "../../assets/logo.jpeg";
+
+const ProviderBrand = ({ compact = false, onClick }) => {
+ return (
+
+
+
+ Provider
+
+
+
+
+
+ Book My Venue
+
+
+
+ {!compact && (
+
+ Provider Portal
+
+ )}
+
+ );
+};
+
+export default ProviderBrand;
diff --git a/frontend/src/components/provider/availability/AvailabilityListGrouped.jsx b/frontend/src/components/provider/availability/AvailabilityListGrouped.jsx
new file mode 100644
index 0000000000..4fd44a32b0
--- /dev/null
+++ b/frontend/src/components/provider/availability/AvailabilityListGrouped.jsx
@@ -0,0 +1,52 @@
+import { formatSlotDate, formatSlotDateCompact, toDateKey } from "../../../utils/formatDate";
+import AvailabilitySlotRow, {
+ AvailabilityTableHeader,
+} from "./AvailabilitySlotRow";
+
+const AvailabilityListGrouped = ({
+ groups,
+ processingSlotId,
+ onActivate,
+ onDeactivate,
+}) => (
+
+
+
+
+ {groups.map((group) => {
+ const dateKey = toDateKey(group.date);
+ const dateLabel = formatSlotDateCompact(group.date);
+
+ return (
+
+
+
+ {dateLabel}
+
+
+ {group.slots.length} slot{group.slots.length === 1 ? "" : "s"}
+
+
+
+
+ {group.slots.map((slot) => (
+
+ ))}
+
+
+ );
+ })}
+
+
+);
+
+export default AvailabilityListGrouped;
diff --git a/frontend/src/components/provider/availability/AvailabilitySkeleton.jsx b/frontend/src/components/provider/availability/AvailabilitySkeleton.jsx
new file mode 100644
index 0000000000..d4a170f731
--- /dev/null
+++ b/frontend/src/components/provider/availability/AvailabilitySkeleton.jsx
@@ -0,0 +1,24 @@
+const AvailabilitySkeleton = ({ count = 4 }) => (
+
+ {Array.from({ length: count }).map((_, index) => (
+
+ ))}
+
+);
+
+export default AvailabilitySkeleton;
diff --git a/frontend/src/components/provider/availability/AvailabilitySlotRow.jsx b/frontend/src/components/provider/availability/AvailabilitySlotRow.jsx
new file mode 100644
index 0000000000..083e672b56
--- /dev/null
+++ b/frontend/src/components/provider/availability/AvailabilitySlotRow.jsx
@@ -0,0 +1,92 @@
+import {
+ getDisplayLabelForSlot,
+ getSlotStatusLabel,
+} from "../../../utils/predefinedSlots";
+
+const statusStyles = {
+ Available: "bg-emerald-50 text-emerald-700 ring-emerald-100",
+ Booked: "bg-amber-50 text-amber-700 ring-amber-100",
+ Inactive: "bg-gray-100 text-gray-600 ring-gray-200",
+};
+
+const ROW_GRID =
+ "md:grid md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_auto_auto] md:items-center md:gap-3";
+
+const AvailabilitySlotRow = ({
+ slot,
+ onActivate,
+ onDeactivate,
+ isProcessing,
+}) => {
+ const status = getSlotStatusLabel(slot);
+ const isBooked = Boolean(slot.isBooked);
+ const isActive = Boolean(slot.isActive);
+
+ return (
+
+
+
+ {getDisplayLabelForSlot(slot)}
+
+
+
+
+ {slot.startTime} – {slot.endTime}
+
+
+
+
+ {status}
+
+ {isBooked && (
+ Cannot deactivate
+ )}
+
+
+
+ {!isBooked && (
+ <>
+ {isActive ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+ );
+};
+
+export const AvailabilityTableHeader = () => (
+
+ Slot
+ Time
+ Status
+ Action
+
+);
+
+export default AvailabilitySlotRow;
diff --git a/frontend/src/components/provider/availability/AvailabilitySummary.jsx b/frontend/src/components/provider/availability/AvailabilitySummary.jsx
new file mode 100644
index 0000000000..4ed20f53f9
--- /dev/null
+++ b/frontend/src/components/provider/availability/AvailabilitySummary.jsx
@@ -0,0 +1,57 @@
+import { CalendarCheck, CalendarClock, CalendarX, LayoutList } from "lucide-react";
+
+const statConfig = [
+ {
+ key: "total",
+ label: "Total slots",
+ icon: LayoutList,
+ iconClass: "bg-red-50 text-red-600",
+ },
+ {
+ key: "available",
+ label: "Available",
+ icon: CalendarClock,
+ iconClass: "bg-emerald-50 text-emerald-600",
+ },
+ {
+ key: "booked",
+ label: "Booked",
+ icon: CalendarCheck,
+ iconClass: "bg-amber-50 text-amber-600",
+ },
+ {
+ key: "inactive",
+ label: "Inactive",
+ icon: CalendarX,
+ iconClass: "bg-gray-100 text-gray-600",
+ },
+];
+
+const AvailabilitySummary = ({ stats }) => (
+
+ {statConfig.map(({ key, label, icon: Icon, iconClass }) => (
+
+
+
+
+
+
+
+ {stats[key] ?? 0}
+
+
+ {label}
+
+
+
+
+ ))}
+
+);
+
+export default AvailabilitySummary;
diff --git a/frontend/src/components/provider/availability/CreateAvailabilityCard.jsx b/frontend/src/components/provider/availability/CreateAvailabilityCard.jsx
new file mode 100644
index 0000000000..d35846bcab
--- /dev/null
+++ b/frontend/src/components/provider/availability/CreateAvailabilityCard.jsx
@@ -0,0 +1,226 @@
+import { useEffect, useMemo, useState } from "react";
+import {
+ areAllSlotsExpiredForToday,
+ getCreatablePresetsForDate,
+ getSlotOptionStateWithSelection,
+ getTodayDateInputValue,
+ getVisibleSlotOptions,
+ SLOT_RULES_HELPER,
+} from "../../../utils/predefinedSlots";
+import { getTodayDateKey, isTodayCalendarDate } from "../../../utils/formatDate";
+
+const fieldClass =
+ "w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm text-gray-900 outline-none transition-colors focus:border-red-300 focus:ring-2 focus:ring-red-100";
+
+const CreateAvailabilityCard = ({
+ date,
+ selectedSlotIds,
+ slots,
+ dateError,
+ slotsError,
+ submitError,
+ isSubmitting,
+ onDateChange,
+ onToggleSlot,
+ onSubmit,
+}) => {
+ const [, setTimeTick] = useState(0);
+
+ useEffect(() => {
+ const intervalId = window.setInterval(() => {
+ setTimeTick((tick) => tick + 1);
+ }, 60_000);
+
+ return () => window.clearInterval(intervalId);
+ }, []);
+
+ const minDate = getTodayDateInputValue();
+ const isToday = date === getTodayDateKey();
+
+ const visibleOptions = useMemo(
+ () => getVisibleSlotOptions(slots, date, selectedSlotIds),
+ [slots, date, selectedSlotIds]
+ );
+
+ const creatablePresets = useMemo(
+ () => getCreatablePresetsForDate(slots, date, selectedSlotIds),
+ [slots, date, selectedSlotIds]
+ );
+
+ const allTodayExpired = useMemo(
+ () => areAllSlotsExpiredForToday(date),
+ [date]
+ );
+
+ const canSubmit =
+ Boolean(date) &&
+ selectedSlotIds.length > 0 &&
+ creatablePresets.length > 0 &&
+ selectedSlotIds.every((id) =>
+ creatablePresets.some((preset) => preset.id === id)
+ );
+
+ const emptySlotMessage = useMemo(() => {
+ if (!date) return null;
+ if (allTodayExpired) {
+ return "No remaining slots available for today.";
+ }
+ if (creatablePresets.length === 0) {
+ return "All slot options are already added or blocked for this date.";
+ }
+ return null;
+ }, [date, allTodayExpired, creatablePresets.length]);
+
+ return (
+
+ Add slots
+
+ Choose a date, pick predefined slots, then save.
+
+
+
+
+ Slot rules (Full Day vs partial slots)
+
+
+ {SLOT_RULES_HELPER}
+
+
+
+ {submitError && (
+
+ {submitError}
+
+ )}
+
+
+
+ );
+};
+
+export default CreateAvailabilityCard;
diff --git a/frontend/src/components/provider/availability/VenueAvailabilityHeader.jsx b/frontend/src/components/provider/availability/VenueAvailabilityHeader.jsx
new file mode 100644
index 0000000000..c931ed0dff
--- /dev/null
+++ b/frontend/src/components/provider/availability/VenueAvailabilityHeader.jsx
@@ -0,0 +1,59 @@
+import { formatBookingPriceDisplay } from "../../../utils/formatPrice";
+import { getVenueCoverUrl } from "../../../utils/venue";
+
+const formatLocation = (venue) => {
+ const parts = [venue?.city, venue?.state].filter(Boolean);
+ return parts.length > 0 ? parts.join(", ") : venue?.address || "Location not specified";
+};
+
+const VenueAvailabilityHeader = ({ venue, slotCount }) => {
+ const coverUrl = getVenueCoverUrl(venue);
+
+ return (
+
+
+
+ {coverUrl ? (
+

+ ) : (
+
+ No image
+
+ )}
+
+
+
+
+ {venue?.title}
+
+
+ {formatLocation(venue)}
+
+
+
+ {formatBookingPriceDisplay(venue?.price)}
+
+
+ {venue?.isActive ? "Venue active" : "Venue inactive"}
+
+
+ {slotCount} slot{slotCount === 1 ? "" : "s"}
+
+
+
+
+
+ );
+};
+
+export default VenueAvailabilityHeader;
diff --git a/frontend/src/components/provider/bookings/ProviderBookingRow.jsx b/frontend/src/components/provider/bookings/ProviderBookingRow.jsx
new file mode 100644
index 0000000000..68e5bcdcde
--- /dev/null
+++ b/frontend/src/components/provider/bookings/ProviderBookingRow.jsx
@@ -0,0 +1,151 @@
+import { Mail, Phone } from "lucide-react";
+import {
+ formatSlotDateCompact,
+ formatSlotLabel,
+ formatTimeRange,
+} from "../../../utils/formatDate";
+import { formatPrice } from "../../../utils/formatPrice";
+import { resolvePopulatedRef } from "../../../utils/booking";
+
+const formatStatus = (status) => {
+ if (!status) return "";
+ return status.charAt(0).toUpperCase() + status.slice(1);
+};
+
+const statusStyles = {
+ confirmed: "bg-emerald-50 text-emerald-700 ring-emerald-100",
+ cancelled: "bg-gray-100 text-gray-600 ring-gray-200",
+ pending: "bg-amber-50 text-amber-700 ring-amber-100",
+ paid: "bg-emerald-50 text-emerald-700 ring-emerald-100",
+ failed: "bg-red-50 text-red-700 ring-red-100",
+ refunded: "bg-sky-50 text-sky-700 ring-sky-100",
+};
+
+const formatPhoneDisplay = (phone) => {
+ const digits = String(phone).replace(/\D/g, "");
+ if (digits.length === 10) {
+ return `${digits.slice(0, 5)} ${digits.slice(5)}`;
+ }
+ return phone;
+};
+
+const toTelHref = (phone) => {
+ const digits = String(phone).replace(/\D/g, "");
+ if (digits.length === 10) return `tel:+91${digits}`;
+ if (digits.length > 10) return `tel:+${digits}`;
+ return `tel:${digits}`;
+};
+
+const ROW_GRID =
+ "md:grid md:grid-cols-[minmax(0,1.15fr)_minmax(0,0.95fr)_minmax(0,1fr)_auto_minmax(0,5.5rem)] md:items-center md:gap-3";
+
+const ProviderBookingRow = ({ booking }) => {
+ const customer = resolvePopulatedRef(booking?.userId);
+ const venue = resolvePopulatedRef(booking?.venueId);
+ const slot = resolvePopulatedRef(booking?.availabilityId);
+
+ const { amount } = formatPrice(booking?.amount);
+ const slotDate = slot?.date ? formatSlotDateCompact(slot.date) : "—";
+ const slotTime = slot
+ ? formatTimeRange(slot.startTime, slot.endTime)
+ : "—";
+ const slotLabel = slot?.slotLabel ? formatSlotLabel(slot.slotLabel) : null;
+ const reference = booking.bookingReference || booking._id || "—";
+ const customerPhone = customer?.phone?.trim() || "";
+
+ return (
+
+
+
+ {customer?.name || "Customer"}
+
+ {customer?.email ? (
+
+
+ {customer.email}
+
+ ) : (
+
No email
+ )}
+
+
+
+
+
+
+ {venue?.title || "Venue unavailable"}
+
+
+ {reference}
+
+
+
+
+
{slotDate}
+ {slotLabel &&
{slotLabel}
}
+
{slotTime}
+
+
+
+ {booking.bookingStatus && (
+
+ {formatStatus(booking.bookingStatus)}
+
+ )}
+ {booking.paymentStatus && (
+
+ {formatStatus(booking.paymentStatus)}
+
+ )}
+
+
+
+
{amount}
+
+ {reference}
+
+
+
+ );
+};
+
+export const ProviderBookingTableHeader = () => (
+
+ Customer
+ Venue
+ Slot
+ Status
+ Amount
+
+);
+
+export default ProviderBookingRow;
diff --git a/frontend/src/components/provider/onboarding/BecomeHostModal.jsx b/frontend/src/components/provider/onboarding/BecomeHostModal.jsx
new file mode 100644
index 0000000000..fd59dc0d2e
--- /dev/null
+++ b/frontend/src/components/provider/onboarding/BecomeHostModal.jsx
@@ -0,0 +1,269 @@
+import { useCallback, useEffect, useId, useRef, useState } from "react";
+import {
+ ArrowRight,
+ Building2,
+ CalendarCheck,
+ Camera,
+ ChevronRight,
+ Crown,
+ MapPin,
+ ShieldCheck,
+ X,
+} from "lucide-react";
+
+const responsibilities = [
+ "List only genuine venues",
+ "Keep venue information accurate",
+ "Upload real venue photos",
+ "Maintain up-to-date availability",
+ "Honour confirmed bookings",
+ "Follow community guidelines",
+];
+
+const nextSteps = [
+ { label: "Create venue", icon: Building2 },
+ { label: "Add photos", icon: Camera },
+ { label: "Set location", icon: MapPin },
+ { label: "Add slots", icon: CalendarCheck },
+ { label: "Get bookings", icon: ShieldCheck },
+];
+
+const BecomeHostModal = ({
+ open,
+ onClose,
+ onConfirm,
+ isLoading = false,
+ error = "",
+}) => {
+ const titleId = useId();
+ const descriptionId = useId();
+ const dialogRef = useRef(null);
+ const [accepted, setAccepted] = useState(false);
+
+ const handleClose = useCallback(() => {
+ if (isLoading) return;
+ setAccepted(false);
+ onClose();
+ }, [isLoading, onClose]);
+
+ useEffect(() => {
+ if (!open) return undefined;
+
+ const dialog = dialogRef.current;
+ if (!dialog) return undefined;
+
+ const focusable = dialog.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+
+ first?.focus();
+
+ const handleKeyDown = (event) => {
+ if (event.key === "Escape" && !isLoading) {
+ event.preventDefault();
+ handleClose();
+ return;
+ }
+
+ if (event.key !== "Tab" || focusable.length === 0) return;
+
+ if (event.shiftKey && document.activeElement === first) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && document.activeElement === last) {
+ event.preventDefault();
+ first.focus();
+ }
+ };
+
+ document.body.style.overflow = "hidden";
+ window.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.body.style.overflow = "";
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [open, isLoading, handleClose]);
+
+ if (!open) return null;
+
+ const handleConfirm = () => {
+ if (!accepted || isLoading) return;
+ onConfirm();
+ };
+
+ const stopDialogClick = (event) => {
+ event.stopPropagation();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ What happens next
+
+
+
+ {nextSteps.map(({ label, icon: Icon }, index) => (
+ -
+
+
+ {label}
+
+ {index < nextSteps.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+ Your responsibilities
+
+
+
+ {responsibilities.map((item) => (
+ -
+
+ {item}
+
+ ))}
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default BecomeHostModal;
diff --git a/frontend/src/components/provider/venue-form/ChipInput.jsx b/frontend/src/components/provider/venue-form/ChipInput.jsx
new file mode 100644
index 0000000000..4ec5120418
--- /dev/null
+++ b/frontend/src/components/provider/venue-form/ChipInput.jsx
@@ -0,0 +1,140 @@
+import { useId, useState } from "react";
+import { X } from "lucide-react";
+import { addUniqueChipValue } from "../../../utils/venueForm";
+
+const ChipInput = ({
+ id: idProp,
+ label,
+ labelledBy,
+ hint,
+ placeholder,
+ value = [],
+ onChange,
+ disabled = false,
+ error,
+}) => {
+ const generatedId = useId();
+ const inputId = idProp || generatedId;
+ const hintId = hint ? `${inputId}-hint` : undefined;
+ const errorId = error ? `${inputId}-error` : undefined;
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
+
+ const [draft, setDraft] = useState("");
+
+ const commitValue = (raw) => {
+ const parts = String(raw)
+ .split(",")
+ .map((part) => part.trim())
+ .filter(Boolean);
+
+ if (parts.length === 0) return;
+
+ let next = value;
+ parts.forEach((part) => {
+ next = addUniqueChipValue(next, part);
+ });
+
+ onChange(next);
+ setDraft("");
+ };
+
+ const handleKeyDown = (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ commitValue(draft);
+ return;
+ }
+
+ if (event.key === "Backspace" && !draft && value.length > 0) {
+ onChange(value.slice(0, -1));
+ }
+ };
+
+ const handleChange = (event) => {
+ const nextValue = event.target.value;
+
+ if (nextValue.includes(",")) {
+ commitValue(nextValue);
+ return;
+ }
+
+ setDraft(nextValue);
+ };
+
+ const handleBlur = () => {
+ if (draft.trim()) {
+ commitValue(draft);
+ }
+ };
+
+ const removeChip = (index) => {
+ onChange(value.filter((_, itemIndex) => itemIndex !== index));
+ };
+
+ return (
+
+ {label ? (
+
+ ) : null}
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+
+
+ {value.map((chip, index) => (
+
+ {chip}
+
+
+ ))}
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default ChipInput;
diff --git a/frontend/src/components/provider/venue-form/VenueForm.jsx b/frontend/src/components/provider/venue-form/VenueForm.jsx
new file mode 100644
index 0000000000..c5d08e4417
--- /dev/null
+++ b/frontend/src/components/provider/venue-form/VenueForm.jsx
@@ -0,0 +1,746 @@
+import { useEffect, useRef, useState } from "react";
+import { AlertTriangle, CheckCircle2, Loader2, MapPin } from "lucide-react";
+import ChipInput from "./ChipInput";
+import VenueFormImagePreview, {VenueFormImageUpload,} from "./VenueFormImagePreview";
+import {geocodeVenueAddress,GeocodeError,GEOCODE_ERROR,getGeocodeUserMessage,hasRequiredGeocodeFields,} from "../../../utils/geocode";
+import {MAX_VENUE_IMAGES,scrollToFirstFormError,stringifyListField,validateImageSelection,validateVenueCoreFields,} from "../../../utils/venueForm";
+import { VENUE_CATEGORY_OPTIONS } from "../../../utils/venueFilters";
+
+const LOCATION_FIELDS = new Set(["address", "city", "state", "pincode"]);
+const LOCATION_VERIFIED_MESSAGE = "Location verified successfully";
+
+const inputClass = (hasError) =>
+ [
+ "w-full rounded-lg border bg-white px-3 py-2.5 text-sm text-gray-900 outline-none transition-colors focus:ring-2 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70",
+ hasError
+ ? "border-red-400 focus:border-red-500 focus:ring-red-100"
+ : "border-gray-200 focus:border-red-500 focus:ring-red-100",
+ ].join(" ");
+
+const sectionClass =
+ "rounded-xl border border-gray-200 bg-white p-4 shadow-sm sm:p-6";
+
+const FormField = ({
+ fieldKey,
+ label,
+ required = false,
+ hint,
+ error,
+ children,
+}) => {
+ const hintId = hint ? `venue-${fieldKey}-hint` : undefined;
+ const errorId = error ? `venue-${fieldKey}-error` : undefined;
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
+
+ return (
+
+ {label && (
+
+ )}
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+ {typeof children === "function"
+ ? children({
+ id: `venue-input-${fieldKey}`,
+ errorId,
+ describedBy,
+ hasError: Boolean(error),
+ })
+ : children}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+const VenueForm = ({
+ mode = "create",
+ initialValues,
+ existingImages = [],
+ submitError = "",
+ submitting = false,
+ onSubmit,
+ onCancel,
+ submitLabel,
+ submittingLabel,
+}) => {
+ const isEdit = mode === "edit";
+
+ const [form, setForm] = useState(initialValues);
+ const [newImages, setNewImages] = useState([]);
+ const [errors, setErrors] = useState({});
+ const [imageInputError, setImageInputError] = useState("");
+ const [isDetectingLocation, setIsDetectingLocation] = useState(false);
+ const [locationDetectFeedback, setLocationDetectFeedback] = useState(null);
+ const detectRequestIdRef = useRef(0);
+
+ useEffect(() => {
+ setForm(initialValues);
+ setLocationDetectFeedback(null);
+ }, [initialValues]);
+
+ const displayImageCount = isEdit
+ ? newImages.length > 0
+ ? newImages.length
+ : existingImages.length
+ : newImages.length;
+
+ const canAddMoreImages = newImages.length < MAX_VENUE_IMAGES;
+
+ const handleChange = (event) => {
+ const { name, value } = event.target;
+ setForm((prev) => {
+ const next = { ...prev, [name]: value };
+
+ if (LOCATION_FIELDS.has(name)) {
+ return {
+ ...next,
+ latitude: "",
+ longitude: "",
+ };
+ }
+
+ return next;
+ });
+ setErrors((prev) => ({ ...prev, [name]: "" }));
+
+ if (LOCATION_FIELDS.has(name)) {
+ setLocationDetectFeedback(null);
+ }
+ };
+
+ const handleDetectLocation = async () => {
+ if (isDetectingLocation || submitting) return;
+
+ if (!hasRequiredGeocodeFields(form)) {
+ setLocationDetectFeedback({
+ type: "error",
+ message: getGeocodeUserMessage(GEOCODE_ERROR.MISSING_REQUIRED),
+ });
+ return;
+ }
+
+ const requestId = detectRequestIdRef.current + 1;
+ detectRequestIdRef.current = requestId;
+
+ setIsDetectingLocation(true);
+ setLocationDetectFeedback(null);
+
+ try {
+ const result = await geocodeVenueAddress(form);
+
+ if (detectRequestIdRef.current !== requestId) return;
+
+ if (!result) {
+ setLocationDetectFeedback({
+ type: "error",
+ message: getGeocodeUserMessage(GEOCODE_ERROR.NOT_FOUND),
+ });
+ return;
+ }
+
+ setForm((prev) => ({
+ ...prev,
+ latitude: String(result.latitude),
+ longitude: String(result.longitude),
+ }));
+ setLocationDetectFeedback({
+ type: "success",
+ message: LOCATION_VERIFIED_MESSAGE,
+ });
+ } catch (error) {
+ if (detectRequestIdRef.current !== requestId) return;
+
+ const code = error instanceof GeocodeError ? error.code : "NOT_FOUND";
+ setLocationDetectFeedback({
+ type: "error",
+ message: getGeocodeUserMessage(code),
+ });
+ } finally {
+ if (detectRequestIdRef.current === requestId) {
+ setIsDetectingLocation(false);
+ }
+ }
+ };
+
+ const hasDetectedCoordinates =
+ form.latitude !== "" &&
+ form.longitude !== "" &&
+ Number.isFinite(Number(form.latitude)) &&
+ Number.isFinite(Number(form.longitude));
+
+ const canDetectLocation = hasRequiredGeocodeFields(form);
+ const showLocationVerified =
+ hasDetectedCoordinates && locationDetectFeedback?.type !== "error";
+
+ const handleListChange = (name, value) => {
+ setForm((prev) => ({ ...prev, [name]: value }));
+ setErrors((prev) => ({ ...prev, [name]: "" }));
+ };
+
+ const handleImageChange = (event) => {
+ const currentCount = isEdit ? newImages.length : newImages.length;
+
+ const { valid, files, message } = validateImageSelection(event.target.files, {
+ currentCount: isEdit ? currentCount : currentCount,
+ maxCount: MAX_VENUE_IMAGES,
+ });
+
+ event.target.value = "";
+
+ if (!valid) {
+ setImageInputError(message);
+ setErrors((prev) => ({ ...prev, images: message }));
+ return;
+ }
+
+ setImageInputError("");
+ setErrors((prev) => ({ ...prev, images: "" }));
+
+ const mapped = files.map((file) => ({
+ file,
+ preview: URL.createObjectURL(file),
+ }));
+
+ if (isEdit) {
+ setNewImages((prev) => {
+ const combined = [...prev, ...mapped];
+ if (combined.length > MAX_VENUE_IMAGES) {
+ const message = `You can upload a maximum of ${MAX_VENUE_IMAGES} images.`;
+ setImageInputError(message);
+ setErrors((prevErrors) => ({ ...prevErrors, images: message }));
+ mapped.forEach((img) => URL.revokeObjectURL(img.preview));
+ return prev;
+ }
+
+ return combined;
+ });
+ return;
+ }
+
+ setNewImages((prev) => [...prev, ...mapped]);
+ };
+
+ const removeNewImage = (index) => {
+ setNewImages((prev) => {
+ const next = [...prev];
+ URL.revokeObjectURL(next[index].preview);
+ next.splice(index, 1);
+ return next;
+ });
+ setImageInputError("");
+ setErrors((prev) => ({ ...prev, images: "" }));
+ };
+
+ const validate = () => {
+ const nextErrors = validateVenueCoreFields(form);
+
+ if (isEdit) {
+ const willHaveImages = newImages.length > 0 || existingImages.length > 0;
+ if (!willHaveImages) {
+ nextErrors.images = "At least one image is required";
+ }
+ } else if (newImages.length === 0) {
+ nextErrors.images = "At least one image is required";
+ }
+
+ setErrors(nextErrors);
+ return nextErrors;
+ };
+
+
+
+ const buildFormData = () => {
+ const formData = new FormData();
+ formData.append("title", form.title.trim());
+ formData.append("description", form.description.trim());
+ formData.append("category", form.category.trim());
+ formData.append("capacity", String(Number(form.capacity)));
+ formData.append("price", String(Number(form.price)));
+ formData.append("address", form.address.trim());
+
+ if (form.city.trim()) formData.append("city", form.city.trim());
+ if (form.state.trim()) formData.append("state", form.state.trim());
+ if (form.pincode.trim()) formData.append("pincode", form.pincode.trim());
+
+ if (hasDetectedCoordinates) {
+ formData.append("latitude", String(form.latitude));
+ formData.append("longitude", String(form.longitude));
+ }
+
+ formData.append("amenities", stringifyListField(form.amenities));
+ formData.append("rules", stringifyListField(form.rules));
+
+ if (newImages.length > 0) {
+ newImages.forEach(({ file }) => {
+ formData.append("images", file);
+ });
+ }
+
+ return formData;
+ };
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+ if (submitting) return;
+
+ const nextErrors = validate();
+ if (Object.keys(nextErrors).length > 0) {
+ scrollToFirstFormError(nextErrors);
+ return;
+ }
+
+ await onSubmit(buildFormData());
+ };
+
+ const previewImages = isEdit && newImages.length === 0 ? existingImages : newImages;
+ const previewIsRemote = isEdit && newImages.length === 0 && existingImages.length > 0;
+
+ return (
+
+ );
+};
+
+export default VenueForm;
diff --git a/frontend/src/components/provider/venue-form/VenueFormImagePreview.jsx b/frontend/src/components/provider/venue-form/VenueFormImagePreview.jsx
new file mode 100644
index 0000000000..d15fe81987
--- /dev/null
+++ b/frontend/src/components/provider/venue-form/VenueFormImagePreview.jsx
@@ -0,0 +1,331 @@
+import { ImagePlus, X } from "lucide-react";
+
+const tileBase =
+ "group relative overflow-hidden rounded-2xl bg-gray-100 ring-1 ring-gray-200/80";
+
+const RemoveImageButton = ({ onClick, disabled, label }) => (
+
+);
+
+const CoverMarker = () => (
+
+ Cover photo
+
+);
+
+const PreviewTile = ({
+ src,
+ alt,
+ isCover,
+ canRemove,
+ onRemove,
+ disabled,
+ className = "",
+}) => (
+
+

+ {isCover &&
}
+ {canRemove && (
+
+ )}
+
+);
+
+const MobilePreviewStrip = ({ items, canRemove, onRemove, submitting }) => (
+
+ {items.map((item, index) => (
+
onRemove(index)}
+ disabled={submitting}
+ className="aspect-[4/3] w-[min(82vw,22rem)] shrink-0 snap-center"
+ />
+ ))}
+
+);
+
+const DesktopPreviewGallery = ({
+ items,
+ canRemove,
+ onRemove,
+ submitting,
+}) => {
+ const count = items.length;
+
+ if (count === 1) {
+ return (
+ onRemove(0)}
+ disabled={submitting}
+ className="aspect-[16/10] w-full sm:aspect-[2/1]"
+ />
+ );
+ }
+
+ if (count === 2) {
+ return (
+
+ {items.map((item, index) => (
+
onRemove(index)}
+ disabled={submitting}
+ className="aspect-[4/3]"
+ />
+ ))}
+
+ );
+ }
+
+ if (count === 3) {
+ return (
+
+
onRemove(0)}
+ disabled={submitting}
+ className="row-span-2 h-full"
+ />
+ onRemove(1)}
+ disabled={submitting}
+ className="h-full"
+ />
+ onRemove(2)}
+ disabled={submitting}
+ className="h-full"
+ />
+
+ );
+ }
+
+ if (count === 4) {
+ return (
+
+
onRemove(0)}
+ disabled={submitting}
+ className="col-span-2 row-span-2 h-full"
+ />
+ onRemove(1)}
+ disabled={submitting}
+ className="h-full"
+ />
+ onRemove(2)}
+ disabled={submitting}
+ className="h-full"
+ />
+ onRemove(3)}
+ disabled={submitting}
+ className="col-span-2 h-full"
+ />
+
+ );
+ }
+
+ return (
+
+
onRemove(0)}
+ disabled={submitting}
+ className="col-span-2 row-span-2 h-full"
+ />
+ {items.slice(1).map((item, index) => (
+ onRemove(index + 1)}
+ disabled={submitting}
+ className="h-full"
+ />
+ ))}
+
+ );
+};
+
+const VenueFormImagePreview = ({
+ images,
+ isRemoteGallery = false,
+ onRemove,
+ submitting = false,
+}) => {
+ if (!images?.length) return null;
+
+ const items = images.map((img, index) => ({
+ key: img.key || img.preview || img.url,
+ src: img.url || img.preview,
+ alt: isRemoteGallery
+ ? `Current venue image ${index + 1}`
+ : `Selected image ${index + 1}`,
+ canRemove: !isRemoteGallery,
+ }));
+
+ return (
+
+ {isRemoteGallery && (
+
Current gallery
+ )}
+
+
+
+
+
+
+
+ {!isRemoteGallery && (
+
+ The first image is used as the cover photo on your venue listing.
+
+ )}
+
+ );
+};
+
+export const VenueFormImageUpload = ({
+ inputId,
+ label,
+ required,
+ disabled,
+ canAddMore,
+ maxImages,
+ displayCount,
+ error,
+ maxHintId,
+ errorId,
+ describedBy,
+ onImageChange,
+}) => (
+
+
+
+
+
+
+
+
+
+
+
+ {canAddMore ? "Click to upload photos" : "Maximum images reached"}
+
+
+ {displayCount} of {maxImages} uploaded · JPG, PNG, WEBP up to 5MB each
+
+
+
+
+ {!canAddMore && (
+
+ Remove an image to upload another. Maximum {maxImages} images per venue.
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+);
+
+export default VenueFormImagePreview;
diff --git a/frontend/src/components/venue-details/MobileBookingBar.jsx b/frontend/src/components/venue-details/MobileBookingBar.jsx
new file mode 100644
index 0000000000..9655287219
--- /dev/null
+++ b/frontend/src/components/venue-details/MobileBookingBar.jsx
@@ -0,0 +1,63 @@
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+import { formatSlotDate, formatTimeRange } from "../../utils/formatDate";
+import { getDisplayLabelForSlot } from "../../utils/predefinedSlots";
+
+const MobileBookingBar = ({
+ venue,
+ selectedSlot,
+ canBook,
+ isPaying,
+ onBookNow,
+ onSelectSlot,
+}) => {
+ const priceLabel = formatBookingPriceDisplay(venue?.price);
+ const hasSelection = Boolean(selectedSlot);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default MobileBookingBar;
diff --git a/frontend/src/components/venue-details/VenueAmenities.jsx b/frontend/src/components/venue-details/VenueAmenities.jsx
new file mode 100644
index 0000000000..6c17b8f64f
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueAmenities.jsx
@@ -0,0 +1,55 @@
+import {
+ Car,
+ CheckCircle2,
+ Coffee,
+ Mic2,
+ Projector,
+ ShieldCheck,
+ Snowflake,
+ UtensilsCrossed,
+ Wifi,
+} from "lucide-react";
+
+const AMENITY_ICONS = [
+ { match: /wifi|internet/i, icon: Wifi },
+ { match: /parking|car/i, icon: Car },
+ { match: /ac|air/i, icon: Snowflake },
+ { match: /cater|food|kitchen/i, icon: UtensilsCrossed },
+ { match: /sound|audio|mic/i, icon: Mic2 },
+ { match: /projector|screen/i, icon: Projector },
+ { match: /security/i, icon: ShieldCheck },
+ { match: /coffee|tea/i, icon: Coffee },
+];
+
+const getAmenityIcon = (amenity) => {
+ const match = AMENITY_ICONS.find((entry) => entry.match.test(amenity));
+ return match?.icon ?? CheckCircle2;
+};
+
+const VenueAmenities = ({ amenities }) => {
+ if (!Array.isArray(amenities) || amenities.length === 0) return null;
+
+ return (
+
+ Amenities
+
+
+ {amenities.map((amenity) => {
+ const Icon = getAmenityIcon(amenity);
+
+ return (
+
+
+ {amenity}
+
+ );
+ })}
+
+
+ );
+};
+
+export default VenueAmenities;
diff --git a/frontend/src/components/venue-details/VenueBookingCard.jsx b/frontend/src/components/venue-details/VenueBookingCard.jsx
new file mode 100644
index 0000000000..6b502f2a1b
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueBookingCard.jsx
@@ -0,0 +1,189 @@
+import { Link } from "react-router-dom";
+import { CalendarDays, CheckCircle2, ExternalLink, MapPin } from "lucide-react";
+import { formatBookingPriceDisplay, formatPrice } from "../../utils/formatPrice";
+import { formatSlotDate, formatTimeRange } from "../../utils/formatDate";
+import { getDisplayLabelForSlot } from "../../utils/predefinedSlots";
+import {
+ getVenueGoogleMapsUrl,
+ hasVenueLocationData,
+} from "../../utils/venueLocation";
+
+const VenueBookingCard = ({
+ venue,
+ selectedSlot,
+ bookableSlotCount,
+ isAuthenticated,
+ loginPath,
+ canBook,
+ isPaying,
+ onBookNow,
+ onViewAvailability,
+ className = "",
+}) => {
+ const priceLabel = formatBookingPriceDisplay(venue?.price);
+
+ return (
+
+
+
{priceLabel}
+
+ {bookableSlotCount} slot{bookableSlotCount === 1 ? "" : "s"} open
+
+
+
+ {selectedSlot ? (
+
+
Selected slot
+
{formatSlotDate(selectedSlot.date)}
+
+ {getDisplayLabelForSlot(selectedSlot)}
+
+
+ {formatTimeRange(selectedSlot.startTime, selectedSlot.endTime)}
+
+
+ ) : (
+
+ Select a slot below to continue booking.
+
+ )}
+
+
+
+ {onViewAvailability && (
+
+ )}
+
+ {!isAuthenticated && (
+
+
+ Sign in
+ {" "}
+ to book
+
+ )}
+
+ );
+};
+
+export const BookingSuccessCard = ({
+ booking,
+ venue,
+ venueTitle,
+ selectedSlot,
+ onViewBookings,
+ onBookAnother,
+}) => {
+ const { amount } = formatPrice(booking?.amount ?? selectedSlot?.price);
+ const slotLabel = selectedSlot
+ ? getDisplayLabelForSlot(selectedSlot)
+ : "—";
+ const slotDate = selectedSlot?.date
+ ? formatSlotDate(selectedSlot.date)
+ : "—";
+ const timeRange = selectedSlot
+ ? formatTimeRange(selectedSlot.startTime, selectedSlot.endTime)
+ : "—";
+ const venueMapsUrl = venue && hasVenueLocationData(venue)
+ ? getVenueGoogleMapsUrl(venue)
+ : null;
+
+ return (
+
+
+
+
+
+ Booking confirmed
+
+
+
+
- Reference
+ - {booking?.bookingReference || "—"}
+
+
+
- Venue
+ - {venueTitle || "—"}
+
+
+
- Date
+ - {slotDate}
+
+
+
- Slot
+ -
+ {slotLabel}
+ {timeRange !== "—" && (
+ {timeRange}
+ )}
+
+
+
+
- Amount
+ - {amount}
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueBookingCard;
diff --git a/frontend/src/components/venue-details/VenueHostedBy.jsx b/frontend/src/components/venue-details/VenueHostedBy.jsx
new file mode 100644
index 0000000000..5977b0a48d
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueHostedBy.jsx
@@ -0,0 +1,61 @@
+import { BadgeCheck, CircleDot } from "lucide-react";
+import { getVenueProvider } from "../../utils/venue";
+
+const TrustBadge = ({ label }) => (
+
+
+ {label}
+
+);
+
+const ActiveBadge = ({ label }) => (
+
+
+ {label}
+
+);
+
+const VenueHostedBy = ({ venue }) => {
+ const provider = getVenueProvider(venue);
+ if (!provider) return null;
+
+ return (
+
+ Hosted by
+
+
+ {provider.profileImage ? (
+

+ ) : (
+
+ {provider.initial}
+
+ )}
+
+
+
+ {provider.name}
+
+
+ {provider.trustIndicators.length > 0 && (
+
+ {provider.trustIndicators.map((indicator) =>
+ indicator.key === "active-host" ? (
+
+ ) : (
+
+ )
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default VenueHostedBy;
diff --git a/frontend/src/components/venue-details/VenueImageGallery.jsx b/frontend/src/components/venue-details/VenueImageGallery.jsx
new file mode 100644
index 0000000000..2656dec00f
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueImageGallery.jsx
@@ -0,0 +1,303 @@
+import { useCallback, useEffect, useState } from "react";
+import { ChevronLeft, ChevronRight, Images, X } from "lucide-react";
+
+const GALLERY_PREVIEW_LIMIT = 5;
+
+const VenueImageLightbox = ({ images, startIndex, onClose }) => {
+ const [index, setIndex] = useState(startIndex);
+
+ const goPrev = useCallback(() => {
+ setIndex((current) => (current === 0 ? images.length - 1 : current - 1));
+ }, [images.length]);
+
+ const goNext = useCallback(() => {
+ setIndex((current) => (current === images.length - 1 ? 0 : current + 1));
+ }, [images.length]);
+
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (event.key === "Escape") onClose();
+ if (event.key === "ArrowLeft") goPrev();
+ if (event.key === "ArrowRight") goNext();
+ };
+
+ document.body.style.overflow = "hidden";
+ window.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.body.style.overflow = "";
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [goNext, goPrev, onClose]);
+
+ return (
+
+
+
+ {images.length > 1 && (
+ <>
+
+
+
+ >
+ )}
+
+

+
+
+ {index + 1} / {images.length}
+
+
+ );
+};
+
+const ImageCountBadge = ({ count, className = "" }) => (
+
+
+ {count} photos
+
+);
+
+const GalleryTile = ({ url, alt, onClick, className = "", overlay }) => (
+
+);
+
+const VenueImageGallery = ({ images, title }) => {
+ const [lightboxIndex, setLightboxIndex] = useState(null);
+ const previewImages = images.slice(0, GALLERY_PREVIEW_LIMIT);
+ const remainingCount = Math.max(images.length - GALLERY_PREVIEW_LIMIT, 0);
+
+ const openLightbox = (index) => setLightboxIndex(index);
+ const closeLightbox = () => setLightboxIndex(null);
+
+ if (images.length === 0) {
+ return (
+
+ No photos available
+
+ );
+ }
+
+const GALLERY_HEIGHT = "h-[340px] lg:h-[460px]";
+
+ const renderDesktopGallery = () => {
+ if (previewImages.length === 1) {
+ return (
+
+ );
+ }
+
+ if (previewImages.length === 2) {
+ return (
+
+ openLightbox(0)}
+ className="col-span-2 h-full"
+ />
+ openLightbox(1)}
+ className="h-full"
+ />
+
+
+ );
+ }
+
+ if (previewImages.length === 3) {
+ return (
+
+ openLightbox(0)}
+ className="row-span-2 h-full"
+ />
+ openLightbox(1)}
+ className="h-full"
+ />
+ openLightbox(2)}
+ className="h-full"
+ />
+
+
+ );
+ }
+
+ if (previewImages.length === 4) {
+ return (
+
+ openLightbox(0)}
+ className="col-span-2 row-span-2 h-full"
+ />
+ openLightbox(1)}
+ className="h-full"
+ />
+ openLightbox(2)}
+ className="h-full"
+ />
+ openLightbox(3)}
+ className="col-span-2 h-full"
+ />
+
+
+ );
+ }
+
+ return (
+
+ openLightbox(0)}
+ className="col-span-2 row-span-2"
+ />
+
+ {previewImages.slice(1, GALLERY_PREVIEW_LIMIT).map((url, index) => {
+ const absoluteIndex = index + 1;
+ const isLastPreview =
+ absoluteIndex === GALLERY_PREVIEW_LIMIT - 1 && remainingCount > 0;
+
+ return (
+ openLightbox(absoluteIndex)}
+ overlay={
+ isLastPreview ? (
+
+ +{remainingCount} more
+
+ ) : null
+ }
+ />
+ );
+ })}
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+
+ {images.map((url, index) => (
+
+ ))}
+
+
+
+
+ {renderDesktopGallery()}
+
+ {lightboxIndex !== null && (
+
+ )}
+ >
+ );
+};
+
+export default VenueImageGallery;
diff --git a/frontend/src/components/venue-details/VenueLocation.jsx b/frontend/src/components/venue-details/VenueLocation.jsx
new file mode 100644
index 0000000000..8034d740b4
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueLocation.jsx
@@ -0,0 +1,80 @@
+import { ExternalLink, MapPin } from "lucide-react";
+import {
+ getVenueAddressDisplay,
+ getVenueGoogleMapsUrl,
+ hasVenueLocationData,
+} from "../../utils/venueLocation";
+
+const VenueLocation = ({ venue, className = "" }) => {
+ if (!venue || !hasVenueLocationData(venue)) {
+ return null;
+ }
+
+ const { title, lines } = getVenueAddressDisplay(venue);
+ const mapsUrl = getVenueGoogleMapsUrl(venue);
+
+ if (!lines.length && !title) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ Venue location
+
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {lines.map(({ key, label, value }) => (
+
+ {label ? (
+
+ {label}:
+ {value}
+
+ ) : (
+
+ {value}
+
+ )}
+
+ ))}
+
+
+ {mapsUrl && (
+
+
+ Location
+
+
+ )}
+
+ );
+};
+
+export default VenueLocation;
diff --git a/frontend/src/components/venue-details/VenueQuickFacts.jsx b/frontend/src/components/venue-details/VenueQuickFacts.jsx
new file mode 100644
index 0000000000..7b29dd2724
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueQuickFacts.jsx
@@ -0,0 +1,107 @@
+import toast from "react-hot-toast";
+import {
+ Building2,
+ IndianRupee,
+ MapPin,
+ Share2,
+ Tag,
+ Users,
+} from "lucide-react";
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+import { getVenueCityStateLabel } from "../../utils/venueLocation";
+import { shareVenue } from "../../utils/shareVenue";
+import { getCategoryLabel } from "../../utils/venueFilters";
+
+const QuickFact = ({ icon: Icon, label, value }) => (
+
+
+
+
+ {label}
+
+
{value}
+
+
+);
+
+const VenueQuickFacts = ({ venue }) => {
+ if (!venue) return null;
+
+ const priceLabel = formatBookingPriceDisplay(venue.price);
+ const locationLabel = getVenueCityStateLabel(venue);
+
+ const handleShare = async () => {
+ try {
+ const result = await shareVenue({
+ title: venue.title,
+ url: window.location.href,
+ });
+
+ if (result.method === "clipboard") {
+ toast.success("Link copied to clipboard");
+ }
+ } catch {
+ toast.error("Unable to share this venue");
+ }
+ };
+
+ return (
+
+
+
+
+ {venue.title}
+
+
+
+
+ {locationLabel}
+
+
+
+ {priceLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueQuickFacts;
diff --git a/frontend/src/components/venue-details/VenueRules.jsx b/frontend/src/components/venue-details/VenueRules.jsx
new file mode 100644
index 0000000000..b28e4e533b
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueRules.jsx
@@ -0,0 +1,25 @@
+import { Check } from "lucide-react";
+
+const VenueRules = ({ rules }) => {
+ if (!Array.isArray(rules) || rules.length === 0) return null;
+
+ return (
+
+ Venue rules
+
+
+ {rules.map((rule) => (
+ -
+
+ {rule}
+
+ ))}
+
+
+ );
+};
+
+export default VenueRules;
diff --git a/frontend/src/components/venue-details/VenueSlotCard.jsx b/frontend/src/components/venue-details/VenueSlotCard.jsx
new file mode 100644
index 0000000000..72779a1789
--- /dev/null
+++ b/frontend/src/components/venue-details/VenueSlotCard.jsx
@@ -0,0 +1,36 @@
+import { Check } from "lucide-react";
+import { getDisplayLabelForSlot } from "../../utils/predefinedSlots";
+import { formatTimeRange } from "../../utils/formatDate";
+
+const VenueSlotCard = ({ slot, selected = false, onSelect, disabled = false }) => {
+ return (
+
+ );
+};
+
+export default VenueSlotCard;
diff --git a/frontend/src/components/venues/SearchBar.jsx b/frontend/src/components/venues/SearchBar.jsx
new file mode 100644
index 0000000000..f291161123
--- /dev/null
+++ b/frontend/src/components/venues/SearchBar.jsx
@@ -0,0 +1,76 @@
+import { ChevronDown, MapPin, Search } from "lucide-react";
+
+const SearchBar = ({
+ search,
+ city,
+ cities,
+ onSearchChange,
+ onCityChange,
+ disabled = false,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onSearchChange(e.target.value)}
+ disabled={disabled}
+ placeholder="Search for venue, events"
+ aria-label="Search venues by title"
+ className="min-w-0 flex-1 border-0 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-60"
+ />
+
+
+
+
+ );
+};
+
+export default SearchBar;
diff --git a/frontend/src/components/venues/VenueCardGrid.jsx b/frontend/src/components/venues/VenueCardGrid.jsx
new file mode 100644
index 0000000000..da5e9e020c
--- /dev/null
+++ b/frontend/src/components/venues/VenueCardGrid.jsx
@@ -0,0 +1,12 @@
+export const VENUE_CARD_GRID_CLASS =
+ "mx-auto grid w-full max-w-6xl grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4 lg:grid-cols-4 lg:gap-4";
+
+const VenueCardGrid = ({ children, className = "" }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default VenueCardGrid;
diff --git a/frontend/src/components/venues/VenueFilters.jsx b/frontend/src/components/venues/VenueFilters.jsx
new file mode 100644
index 0000000000..1139ad456e
--- /dev/null
+++ b/frontend/src/components/venues/VenueFilters.jsx
@@ -0,0 +1,173 @@
+import {
+ Briefcase,
+ Cake,
+ Camera,
+ ChevronDown,
+ Heart,
+ LayoutGrid,
+ MoreHorizontal,
+ PartyPopper,
+ Users,
+} from "lucide-react";
+import {
+ VENUE_CATEGORIES,
+ SORT_OPTIONS,
+} from "../../utils/venueFilters";
+
+const CATEGORY_ICONS = {
+ all: LayoutGrid,
+ wedding: Heart,
+ corporate: Briefcase,
+ birthday: Cake,
+ party: PartyPopper,
+ function: Users,
+ photoshoot: Camera,
+ other: MoreHorizontal,
+};
+
+const fieldClass =
+ "w-full rounded-xl border border-gray-200 bg-white px-3 py-3 text-sm text-gray-900 transition-colors focus:border-red-400 focus:outline-none focus:ring-2 focus:ring-red-100 sm:py-2.5";
+
+const VenueFilters = ({
+ filters,
+ activeFilterCount,
+ onFilterChange,
+ onClearFilters,
+ disabled = false,
+}) => {
+ const handleChange = (name) => (event) => {
+ onFilterChange(name, event.target.value);
+ };
+
+ return (
+
+
+
+ Browse by Venue Categories
+
+
+
+ {VENUE_CATEGORIES.map(({ value, label }) => {
+ const Icon = CATEGORY_ICONS[value] ?? LayoutGrid;
+ const isActive = filters.category === value;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ {activeFilterCount > 0 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueFilters;
diff --git a/frontend/src/components/venues/VenueGrid.jsx b/frontend/src/components/venues/VenueGrid.jsx
new file mode 100644
index 0000000000..0d947cd425
--- /dev/null
+++ b/frontend/src/components/venues/VenueGrid.jsx
@@ -0,0 +1,38 @@
+import FeaturedVenueCard from "../home/FeaturedVenueCard";
+import EmptyState from "../common/EmptyState";
+import VenueCardGrid from "./VenueCardGrid";
+
+const VenueGrid = ({ venues, totalCount }) => {
+ if (venues.length === 0) {
+ return (
+ 0
+ ? "Try a different city, category, or search term."
+ : "Check back soon - new venues are added regularly."
+ }
+ />
+ );
+ }
+
+ return (
+
+
+
+ {venues.length}
+ {totalCount !== venues.length ? ` of ${totalCount}` : ""} venue
+ {venues.length !== 1 ? "s" : ""}
+
+
+
+
+ {venues.map((venue) => (
+
+ ))}
+
+
+ );
+};
+
+export default VenueGrid;
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
new file mode 100644
index 0000000000..5fa0652198
--- /dev/null
+++ b/frontend/src/context/AuthContext.jsx
@@ -0,0 +1,160 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import * as authService from "../services/authService";
+import { resolveEffectiveRoles } from "../utils/auth";
+
+const AuthContext = createContext();
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [roleOverride, setRoleOverride] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const authRequestId = useRef(0);
+ const isLoggingOutRef = useRef(false);
+
+ const applyAuthResult = useCallback((requestId, nextUser) => {
+ if (requestId !== authRequestId.current) return false;
+ if (isLoggingOutRef.current && nextUser !== null) return false;
+
+ setUser(nextUser);
+ return true;
+ }, []);
+
+ const syncUser = useCallback(
+ async ({ manageLoading = false } = {}) => {
+ const requestId = ++authRequestId.current;
+ isLoggingOutRef.current = false;
+
+ if (manageLoading) {
+ setLoading(true);
+ }
+
+ try {
+ const data = await authService.getMe();
+
+ if (requestId !== authRequestId.current) return null;
+
+ const nextUser = data.success ? data.user : null;
+ applyAuthResult(requestId, nextUser);
+ return nextUser;
+ } catch {
+ if (requestId !== authRequestId.current) return null;
+ applyAuthResult(requestId, null);
+ return null;
+ } finally {
+ if (manageLoading && requestId === authRequestId.current) {
+ setLoading(false);
+ }
+ }
+ },
+ [applyAuthResult]
+ );
+
+ const fetchUser = useCallback(
+ () => syncUser({ manageLoading: true }),
+ [syncUser]
+ );
+
+ const refreshUser = useCallback(
+ () => syncUser({ manageLoading: false }),
+ [syncUser]
+ );
+
+ const syncUserAfterBecomeProvider = useCallback(
+ async (rolesFromApi) => {
+ const nextRoles =
+ Array.isArray(rolesFromApi) && rolesFromApi.length > 0
+ ? rolesFromApi
+ : null;
+
+ if (nextRoles) {
+ setRoleOverride(nextRoles);
+ }
+
+ await syncUser({ manageLoading: false });
+
+ return nextRoles;
+ },
+ [syncUser]
+ );
+
+ const logout = useCallback(async () => {
+ authRequestId.current += 1;
+ isLoggingOutRef.current = true;
+ setUser(null);
+ setRoleOverride(null);
+ setLoading(false);
+
+ try {
+ await authService.logout();
+ } catch {
+ // Clear local session even if the API call fails.
+ } finally {
+ isLoggingOutRef.current = false;
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchUser();
+ }, [fetchUser]);
+
+ const userRoles = useMemo(
+ () => resolveEffectiveRoles(roleOverride, user),
+ [roleOverride, user]
+ );
+
+ const isAuthenticated = Boolean(user);
+ const isProvider = useMemo(
+ () => userRoles.includes("provider"),
+ [userRoles]
+ );
+ const isAdmin = useMemo(
+ () => userRoles.includes("admin"),
+ [userRoles]
+ );
+ const authReady = !loading;
+
+ const value = useMemo(
+ () => ({
+ user,
+ userRoles,
+ loading,
+ authReady,
+ fetchUser,
+ refreshUser,
+ syncUser,
+ syncUserAfterBecomeProvider,
+ logout,
+ isAuthenticated,
+ isProvider,
+ isAdmin,
+ }),
+ [
+ user,
+ userRoles,
+ loading,
+ authReady,
+ fetchUser,
+ refreshUser,
+ syncUser,
+ syncUserAfterBecomeProvider,
+ logout,
+ isAuthenticated,
+ isProvider,
+ isAdmin,
+ ]
+ );
+
+ return (
+ {children}
+ );
+};
+
+export const useAuth = () => useContext(AuthContext);
diff --git a/frontend/src/hooks/useRazorpay.js b/frontend/src/hooks/useRazorpay.js
new file mode 100644
index 0000000000..e33b5a4e77
--- /dev/null
+++ b/frontend/src/hooks/useRazorpay.js
@@ -0,0 +1,112 @@
+// Vite loads env vars at build/dev startup — restart `npm run dev` after editing frontend/.env
+
+const RAZORPAY_SCRIPT_URL = "https://checkout.razorpay.com/v1/checkout.js";
+
+export const PAYMENT_UNAVAILABLE_MESSAGE =
+ "Online payments are temporarily unavailable. Please try again later.";
+
+const razorpayKeyFromEnv = import.meta.env.VITE_RAZORPAY_KEY_ID;
+
+export const isRazorpayConfigured = () =>
+ Boolean(razorpayKeyFromEnv?.trim());
+
+const getRazorpayKeyId = () => {
+ const key = razorpayKeyFromEnv?.trim();
+
+ if (!key) {
+ console.warn(
+ "Razorpay is not configured. Set VITE_RAZORPAY_KEY_ID in frontend/.env"
+ );
+ throw new Error(PAYMENT_UNAVAILABLE_MESSAGE);
+ }
+
+ return key;
+};
+
+const isValidOrder = (order) => {
+ if (!order || typeof order !== "object") return false;
+
+ const amount = Number(order.amount);
+ const orderId = order.id;
+ const currency = order.currency;
+
+ return (
+ hasValue(orderId) &&
+ Number.isFinite(amount) &&
+ amount > 0 &&
+ hasValue(currency)
+ );
+};
+
+const hasValue = (value) =>
+ value !== undefined && value !== null && String(value).trim() !== "";
+
+const loadRazorpayScript = () => {
+ return new Promise((resolve) => {
+ if (window.Razorpay) {
+ resolve(true);
+ return;
+ }
+
+ const script = document.createElement("script");
+ script.src = RAZORPAY_SCRIPT_URL;
+ script.async = true;
+ script.onload = () => resolve(true);
+ script.onerror = () => resolve(false);
+ document.body.appendChild(script);
+ });
+};
+
+const useRazorpay = () => {
+ const openCheckout = async (order, options = {}) => {
+ if (!isRazorpayConfigured()) {
+ console.warn(
+ "Razorpay is not configured. Set VITE_RAZORPAY_KEY_ID in frontend/.env"
+ );
+ throw new Error(PAYMENT_UNAVAILABLE_MESSAGE);
+ }
+
+ if (!isValidOrder(order)) {
+ throw new Error("Invalid payment order. Please try booking again.");
+ }
+
+ const isLoaded = await loadRazorpayScript();
+
+ if (!isLoaded) {
+ throw new Error("Failed to load Razorpay. Please try again.");
+ }
+
+ const key = getRazorpayKeyId();
+
+ return new Promise((resolve, reject) => {
+ const razorpay = new window.Razorpay({
+ key,
+ amount: order.amount,
+ currency: order.currency,
+ order_id: order.id,
+ name: options.name || "BookMyVenue",
+ description: options.description || "Venue booking",
+ handler: (response) => resolve(response),
+ modal: {
+ ondismiss: () => {
+ reject(new Error("Payment cancelled."));
+ },
+ },
+ });
+
+ razorpay.on("payment.failed", (response) => {
+ reject(
+ new Error(
+ response.error?.description || "Payment failed. Please try again."
+ )
+ );
+ });
+
+ razorpay.open();
+ });
+ };
+
+ return { openCheckout, isRazorpayConfigured };
+};
+
+export default useRazorpay;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000000..2def5705c7
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,15 @@
+@import "tailwindcss";
+
+@theme {
+ --font-brand: "Pacifico", cursive;
+}
+
+html,
+body {
+ overflow-x: clip;
+}
+
+#root {
+ overflow-x: clip;
+ min-height: 100%;
+}
\ No newline at end of file
diff --git a/frontend/src/layouts/AdminLayout.jsx b/frontend/src/layouts/AdminLayout.jsx
new file mode 100644
index 0000000000..c09bfd41d6
--- /dev/null
+++ b/frontend/src/layouts/AdminLayout.jsx
@@ -0,0 +1,178 @@
+import { useState } from "react";
+import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
+import {
+ LayoutDashboard,
+ Users,
+ Building2,
+ CalendarCheck,
+ CreditCard,
+ Menu,
+ X,
+ LogOut,
+} from "lucide-react";
+import { useAuth } from "../context/AuthContext";
+import AdminBrand from "../components/admin/AdminBrand";
+
+const navItems = [
+ {
+ id: "dashboard",
+ to: "/admin/dashboard",
+ label: "Dashboard",
+ icon: LayoutDashboard,
+ },
+ { id: "users", to: "/admin/users", label: "Users", icon: Users },
+ { id: "venues", to: "/admin/venues", label: "Venues", icon: Building2 },
+ {
+ id: "bookings",
+ to: "/admin/bookings",
+ label: "Bookings",
+ icon: CalendarCheck,
+ },
+ {
+ id: "payments",
+ to: "/admin/payments",
+ label: "Payments",
+ icon: CreditCard,
+ },
+];
+
+const isNavItemActive = (id, pathname) => {
+ switch (id) {
+ case "dashboard":
+ return pathname === "/admin/dashboard";
+ case "users":
+ return pathname.startsWith("/admin/users");
+ case "venues":
+ return pathname.startsWith("/admin/venues");
+ case "bookings":
+ return pathname.startsWith("/admin/bookings");
+ case "payments":
+ return pathname.startsWith("/admin/payments");
+ default:
+ return false;
+ }
+};
+
+const navLinkClass = ({ isActive }) =>
+ `flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors duration-200 ${
+ isActive
+ ? "border-l-[3px] border-violet-800 bg-violet-50/90 pl-[9px] text-violet-900"
+ : "border-l-[3px] border-transparent pl-[9px] text-gray-600 hover:bg-gray-50 hover:text-gray-900"
+ }`;
+
+const AdminLayout = () => {
+ const { user, isAuthenticated, logout } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ const closeSidebar = () => setSidebarOpen(false);
+
+ const handleLogout = async () => {
+ closeSidebar();
+ await logout();
+ navigate("/login");
+ };
+
+ const sidebarContent = (
+ <>
+
+
+
+
+ {isAuthenticated && user && (
+
+
+
+ {user.name || "Admin"}
+
+
{user.email || ""}
+
+
+
+
+ )}
+ >
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {sidebarOpen && (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default AdminLayout;
diff --git a/frontend/src/layouts/ProviderLayout.jsx b/frontend/src/layouts/ProviderLayout.jsx
new file mode 100644
index 0000000000..815a3854b9
--- /dev/null
+++ b/frontend/src/layouts/ProviderLayout.jsx
@@ -0,0 +1,205 @@
+import { useState } from "react";
+import { Link, NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
+import {
+ LayoutDashboard,
+ Building2,
+ PlusCircle,
+ CalendarCheck,
+ Menu,
+ X,
+ LogOut,
+ Store,
+} from "lucide-react";
+import { useAuth } from "../context/AuthContext";
+import ProviderBrand from "../components/provider/ProviderBrand";
+
+const navItems = [
+ {
+ id: "dashboard",
+ to: "/provider/dashboard",
+ label: "Dashboard",
+ icon: LayoutDashboard,
+ },
+ {
+ id: "venues",
+ to: "/provider/venues",
+ label: "My Venues",
+ icon: Building2,
+ },
+ {
+ id: "create-venue",
+ to: "/provider/venues/new",
+ label: "Create Venue",
+ icon: PlusCircle,
+ },
+ {
+ id: "bookings",
+ to: "/provider/bookings",
+ label: "Bookings",
+ icon: CalendarCheck,
+ },
+];
+
+const isNavItemActive = (id, pathname) => {
+ switch (id) {
+ case "dashboard":
+ return pathname === "/provider/dashboard";
+ case "venues":
+ return (
+ pathname === "/provider/venues" ||
+ (pathname.startsWith("/provider/venues/") &&
+ pathname !== "/provider/venues/new")
+ );
+ case "create-venue":
+ return pathname === "/provider/venues/new";
+ case "bookings":
+ return (
+ pathname === "/provider/bookings" ||
+ pathname.startsWith("/provider/bookings/")
+ );
+ default:
+ return false;
+ }
+};
+
+const navLinkClass = ({ isActive }) =>
+ `flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors duration-200 ${
+ isActive
+ ? "border-l-[3px] border-red-800 bg-red-50/90 pl-[9px] text-red-900"
+ : "border-l-[3px] border-transparent pl-[9px] text-gray-600 hover:bg-gray-50 hover:text-gray-900"
+ }`;
+
+const ProviderLayout = () => {
+ const { user, isAuthenticated, logout } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ const closeSidebar = () => setSidebarOpen(false);
+
+ const handleLogout = async () => {
+ closeSidebar();
+ await logout();
+ navigate("/");
+ };
+
+ const sidebarContent = (
+ <>
+
+
+
+
+ {isAuthenticated && user && (
+
+
+
+ {user.name || "User"}
+
+
{user.email || ""}
+
+
+
+
+
+ Back to marketplace
+
+
+
+
+
+ )}
+ >
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {sidebarOpen && (
+
+ )}
+
+
+
+
+
+
+
Provider workspace
+
+ Manage your venues and bookings
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProviderLayout;
diff --git a/frontend/src/layouts/PublicLayout.jsx b/frontend/src/layouts/PublicLayout.jsx
new file mode 100644
index 0000000000..2b3ca8d6cc
--- /dev/null
+++ b/frontend/src/layouts/PublicLayout.jsx
@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import Navbar from "../components/common/Navbar";
+import Footer from "../components/common/Footer";
+
+const PublicLayout = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default PublicLayout;
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000000..b9a1a6deac
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/frontend/src/pages/admin/BookingDetail.jsx b/frontend/src/pages/admin/BookingDetail.jsx
new file mode 100644
index 0000000000..2e9c11db4c
--- /dev/null
+++ b/frontend/src/pages/admin/BookingDetail.jsx
@@ -0,0 +1,204 @@
+import { useEffect, useState } from "react";
+import { Link, useParams } from "react-router-dom";
+import { getAdminBookingById } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminDetailRow from "../../components/admin/AdminDetailRow";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { resolvePopulatedRef } from "../../utils/booking";
+import { formatPrice } from "../../utils/formatPrice";
+import {
+ formatSlotDate,
+ formatSlotLabel,
+ formatTimeRange,
+} from "../../utils/formatDate";
+import { formatStatusLabel } from "../../utils/adminFormat";
+
+const AdminBookingDetail = () => {
+ const { id } = useParams();
+ const [booking, setBooking] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const fetchBooking = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminBookingById(id);
+
+ if (data.success) {
+ setBooking(data.data);
+ } else {
+ setBooking(null);
+ setError(data.message || "Booking not found.");
+ }
+ } catch (err) {
+ setBooking(null);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load booking. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchBooking();
+ }, [id]);
+
+ const customer = resolvePopulatedRef(booking?.userId);
+ const venue = resolvePopulatedRef(booking?.venueId);
+ const slot = resolvePopulatedRef(booking?.availabilityId);
+ const { amount } = formatPrice(booking?.amount);
+
+ return (
+ <>
+
+
+ ← Back to bookings
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && booking && (
+ <>
+
+
+
+
+ Booking
+
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+
+
+ Customer
+ {customer ? (
+
+
+
+
+
+ ) : (
+ —
+ )}
+ {customer?._id && (
+
+ View customer →
+
+ )}
+
+
+
+ Venue
+ {venue ? (
+ <>
+
+
+
+
+
+ View venue →
+
+ >
+ ) : (
+ —
+ )}
+
+
+
+ Slot
+ {slot ? (
+
+
+
+
+
+ ) : (
+ —
+ )}
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default AdminBookingDetail;
diff --git a/frontend/src/pages/admin/Bookings.jsx b/frontend/src/pages/admin/Bookings.jsx
new file mode 100644
index 0000000000..422a7fac6e
--- /dev/null
+++ b/frontend/src/pages/admin/Bookings.jsx
@@ -0,0 +1,188 @@
+import { useCallback, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { getAdminBookings } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminFilterSelect from "../../components/admin/AdminFilterSelect";
+import AdminTable from "../../components/admin/AdminTable";
+import AdminPagination from "../../components/admin/AdminPagination";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { resolvePopulatedRef } from "../../utils/booking";
+import { formatPrice } from "../../utils/formatPrice";
+import {
+ formatSlotDateCompact,
+ formatSlotLabel,
+} from "../../utils/formatDate";
+import { formatStatusLabel } from "../../utils/adminFormat";
+
+const BOOKING_STATUS_OPTIONS = [
+ { value: "", label: "All booking statuses" },
+ { value: "confirmed", label: "Confirmed" },
+ { value: "cancelled", label: "Cancelled" },
+];
+
+const PAYMENT_STATUS_OPTIONS = [
+ { value: "", label: "All payment statuses" },
+ { value: "pending", label: "Pending" },
+ { value: "paid", label: "Paid" },
+ { value: "failed", label: "Failed" },
+ { value: "refunded", label: "Refunded" },
+];
+
+const AdminBookings = () => {
+ const [bookings, setBookings] = useState([]);
+ const [count, setCount] = useState(0);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(6);
+ const [bookingStatus, setBookingStatus] = useState("");
+ const [paymentStatus, setPaymentStatus] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const fetchBookings = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminBookings({
+ page,
+ limit,
+ bookingStatus,
+ paymentStatus,
+ });
+
+ if (data.success) {
+ setBookings(data.data ?? []);
+ setCount(data.count ?? 0);
+ } else {
+ setBookings([]);
+ setCount(0);
+ setError(data.message || "Failed to load bookings.");
+ }
+ } catch (err) {
+ setBookings([]);
+ setCount(0);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load bookings. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [page, limit, bookingStatus, paymentStatus]);
+
+ useEffect(() => {
+ fetchBookings();
+ }, [fetchBookings]);
+
+ return (
+ <>
+
+
+
+
{
+ setBookingStatus(value);
+ setPage(1);
+ }}
+ options={BOOKING_STATUS_OPTIONS}
+ />
+ {
+ setPaymentStatus(value);
+ setPage(1);
+ }}
+ options={PAYMENT_STATUS_OPTIONS}
+ />
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && bookings.length === 0 && (
+
+ )}
+
+ {!loading && !error && bookings.length > 0 && (
+
+
+ {bookings.map((booking) => {
+ const customer = resolvePopulatedRef(booking.userId);
+ const venue = resolvePopulatedRef(booking.venueId);
+ const slot = resolvePopulatedRef(booking.availabilityId);
+ const { amount } = formatPrice(booking.amount);
+
+ return (
+
+
+
+ {booking.bookingReference || booking._id}
+
+
+ {customer?.name || "Customer"}
+
+
+
+
+ {venue?.title || "Venue"}
+
+
+ {slot
+ ? `${formatSlotDateCompact(slot.date)} · ${formatSlotLabel(slot.slotLabel)}`
+ : "—"}
+
+
+
{amount}
+
+
+
+ );
+ })}
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default AdminBookings;
diff --git a/frontend/src/pages/admin/Dashboard.jsx b/frontend/src/pages/admin/Dashboard.jsx
new file mode 100644
index 0000000000..2247cce8a7
--- /dev/null
+++ b/frontend/src/pages/admin/Dashboard.jsx
@@ -0,0 +1,232 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { Users, Building2, CalendarCheck, IndianRupee, UserCog, CheckCircle2, ArrowRight, } from "lucide-react";
+import { getDashboardStats, getRecentActivity, } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminStatCard from "../../components/admin/AdminStatCard";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import { formatPrice } from "../../utils/formatPrice";
+import { formatSlotDateCompact } from "../../utils/formatDate";
+
+const AdminDashboard = () => {
+ const [stats, setStats] = useState(null);
+ const [activity, setActivity] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [activityError, setActivityError] = useState("");
+ const [revenueVisible, setRevenueVisible] = useState(false);
+
+ const fetchDashboard = async () => {
+ try {
+ setLoading(true);
+ setError("");
+ setActivityError("");
+
+ const [statsData, activityData] = await Promise.all([
+ getDashboardStats(),
+ getRecentActivity(),
+ ]);
+
+ if (statsData.success) {
+ setStats(statsData.data);
+ } else {
+ setError(statsData.message || "Failed to load dashboard stats.");
+ return;
+ }
+
+ if (activityData.success) {
+ setActivity(activityData.data);
+ } else {
+ setActivity(null);
+ setActivityError(
+ activityData.message || "Could not load recent activity."
+ );
+ }
+ } catch (err) {
+ setError(
+ err.response?.data?.message ||
+ "Unable to load dashboard. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDashboard();
+ }, []);
+
+ const revenueDisplay = stats
+ ? formatPrice(stats.totalRevenue).amount
+ : "—";
+
+ return (
+ <>
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && stats && (
+
+
+
+
+
+
+
+
setRevenueVisible((visible) => !visible)}
+ icon={IndianRupee}
+ iconClass="bg-emerald-50 text-emerald-600"
+ />
+
+
+ {activityError && (
+
+ {activityError}
+
+ )}
+
+ {activity && (
+
+
(
+
+
+ {user.name}
+
+
+ {user.email}
+
+
+ )}
+ emptyMessage="No marketplace users yet."
+ />
+
+ (
+
+
+ {venue.title}
+
+
+ {venue.city || "—"}
+
+
+ )}
+ emptyMessage="No venues yet."
+ />
+
+ (
+
+
+ {booking.bookingReference || booking._id}
+
+
+ {booking.venueId?.title || "Venue"} ·{" "}
+ {formatSlotDateCompact(booking.createdAt)}
+
+
+ )}
+ emptyMessage="No bookings yet."
+ />
+
+ )}
+
+ )}
+ >
+ );
+};
+
+const RecentSection = ({
+ title,
+ linkTo,
+ items = [],
+ renderItem,
+ emptyMessage,
+}) => (
+
+
+
{title}
+
+ View all
+
+
+
+
+ {items.length === 0 ? (
+ -
+ {emptyMessage}
+
+ ) : (
+ items.map((item) => (
+ - {renderItem(item)}
+ ))
+ )}
+
+
+);
+
+export default AdminDashboard;
diff --git a/frontend/src/pages/admin/Payments.jsx b/frontend/src/pages/admin/Payments.jsx
new file mode 100644
index 0000000000..9073b7ebbd
--- /dev/null
+++ b/frontend/src/pages/admin/Payments.jsx
@@ -0,0 +1,240 @@
+import { useCallback, useEffect, useState } from "react";
+import { Link, useSearchParams } from "react-router-dom";
+import { getAdminPaymentOrders, getAdminPaymentHistory, getAdminAbandonedPayments, } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminFilterSelect from "../../components/admin/AdminFilterSelect";
+import AdminTextFilter from "../../components/admin/AdminTextFilter";
+import AdminTable from "../../components/admin/AdminTable";
+import AdminPagination from "../../components/admin/AdminPagination";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { resolvePopulatedRef } from "../../utils/booking";
+import { formatPrice } from "../../utils/formatPrice";
+import { formatSlotDateCompact } from "../../utils/formatDate";
+import { formatStatusLabel } from "../../utils/adminFormat";
+
+const TABS = [
+ { id: "orders", label: "Payment orders" },
+ { id: "history", label: "Payment history" },
+ { id: "abandoned", label: "Abandoned" },
+];
+
+const ORDER_STATUS_OPTIONS = [
+ { value: "", label: "All statuses" },
+ { value: "created", label: "Created" },
+ { value: "completed", label: "Completed" },
+];
+
+const AdminPayments = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const activeTab = searchParams.get("tab") || "orders";
+
+ const [items, setItems] = useState([]);
+ const [count, setCount] = useState(0);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(6);
+ const [status, setStatus] = useState("");
+ const [hours, setHours] = useState("24");
+ const [debouncedHours, setDebouncedHours] = useState("24");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedHours(hours);
+ setPage(1);
+ }, 300);
+ return () => clearTimeout(timer);
+ }, [hours]);
+
+ const setTab = (tabId) => {
+ setSearchParams({ tab: tabId });
+ setPage(1);
+ setStatus("");
+ setError("");
+ };
+
+ const fetchPayments = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ let data;
+
+ if (activeTab === "history") {
+ data = await getAdminPaymentHistory({ page, limit });
+ } else if (activeTab === "abandoned") {
+ data = await getAdminAbandonedPayments({
+ page,
+ limit,
+ hours: debouncedHours || 24,
+ });
+ } else {
+ data = await getAdminPaymentOrders({ page, limit, status });
+ }
+
+ if (data.success) {
+ setItems(data.data ?? []);
+ setCount(data.count ?? 0);
+ } else {
+ setItems([]);
+ setCount(0);
+ setError(data.message || "Failed to load payments.");
+ }
+ } catch (err) {
+ setItems([]);
+ setCount(0);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load payments. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [activeTab, page, limit, status, debouncedHours]);
+
+ useEffect(() => {
+ fetchPayments();
+ }, [fetchPayments]);
+
+ return (
+ <>
+
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
+
+ {activeTab === "orders" && (
+
{
+ setStatus(value);
+ setPage(1);
+ }}
+ options={ORDER_STATUS_OPTIONS}
+ />
+ )}
+
+ {activeTab === "abandoned" && (
+
+ )}
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && items.length === 0 && (
+
+ )}
+
+ {!loading && !error && items.length > 0 && (
+
+
+ {activeTab === "history" &&
+ items.map((booking) => (
+
+ ))}
+
+ {activeTab !== "history" &&
+ items.map((order) => (
+
+ ))}
+
+
+
+
+ )}
+ >
+ );
+};
+
+const PaymentOrderRow = ({ order }) => {
+ const user = resolvePopulatedRef(order.userId);
+ const venue = resolvePopulatedRef(order.venueId);
+ const amount = formatPrice(order.amountInPaise / 100).amount;
+
+ return (
+
+
+
+ {order.razorpayOrderId}
+
+
{user?.name || "User"}
+
+
{venue?.title || "Venue"}
+
{amount}
+
+
+ );
+};
+
+const PaymentHistoryRow = ({ booking }) => {
+ const user = resolvePopulatedRef(booking.userId);
+ const venue = resolvePopulatedRef(booking.venueId);
+ const { amount } = formatPrice(booking.amount);
+
+ return (
+
+
+
+ {booking.bookingReference || booking._id}
+
+
{user?.name || "Customer"}
+
+
{venue?.title || "Venue"}
+
{amount}
+
+ {formatSlotDateCompact(booking.createdAt)}
+
+
+ );
+};
+
+export default AdminPayments;
diff --git a/frontend/src/pages/admin/UserDetail.jsx b/frontend/src/pages/admin/UserDetail.jsx
new file mode 100644
index 0000000000..cb9295cecb
--- /dev/null
+++ b/frontend/src/pages/admin/UserDetail.jsx
@@ -0,0 +1,205 @@
+import { useEffect, useState } from "react";
+import { Link, useParams } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getAdminUserById, activateAdminUser, deactivateAdminUser, } from "../../services/adminService";
+import { useAuth } from "../../context/AuthContext";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminDetailRow from "../../components/admin/AdminDetailRow";
+import ConfirmModal from "../../components/common/ConfirmModal";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { formatRoleLabel } from "../../utils/adminFormat";
+
+const AdminUserDetail = () => {
+ const { id } = useParams();
+ const { user: currentUser } = useAuth();
+ const [userData, setUserData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [confirmAction, setConfirmAction] = useState(null);
+ const [actionLoading, setActionLoading] = useState(false);
+
+ const fetchUser = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminUserById(id);
+
+ if (data.success) {
+ setUserData(data.data);
+ } else {
+ setUserData(null);
+ setError(data.message || "User not found.");
+ }
+ } catch (err) {
+ setUserData(null);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load user. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchUser();
+ }, [id]);
+
+ const handleToggleStatus = async () => {
+ if (!confirmAction || !userData?.user) return;
+
+ try {
+ setActionLoading(true);
+ const data =
+ confirmAction === "activate"
+ ? await activateAdminUser(id)
+ : await deactivateAdminUser(id);
+
+ if (data.success) {
+ toast.success(data.message || "User status updated.");
+ setConfirmAction(null);
+ fetchUser();
+ } else {
+ toast.error(data.message || "Failed to update user.");
+ }
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ "Unable to update user. Please try again."
+ );
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const user = userData?.user;
+ const isSelf =
+ currentUser?._id === user?._id || currentUser?.id === user?._id;
+
+ return (
+ <>
+
+
+ ← Back to users
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && user && (
+ <>
+
+ {user.isActive ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Profile
+
+
+
+
+
+
+
- Status
+ -
+
+
+
+
+
- Roles
+ -
+ {(user.roles ?? []).map((role) => (
+
+ {formatRoleLabel(role)}
+
+ ))}
+
+
+
+
+
+
+
+ Activity summary
+
+
+
+
+
+
+
+ >
+ )}
+
+ setConfirmAction(null)}
+ />
+ >
+ );
+};
+
+export default AdminUserDetail;
diff --git a/frontend/src/pages/admin/Users.jsx b/frontend/src/pages/admin/Users.jsx
new file mode 100644
index 0000000000..2a4ceb8dfd
--- /dev/null
+++ b/frontend/src/pages/admin/Users.jsx
@@ -0,0 +1,277 @@
+import { useCallback, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getAdminUsers, activateAdminUser, deactivateAdminUser, } from "../../services/adminService";
+import { useAuth } from "../../context/AuthContext";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminSearchInput from "../../components/admin/AdminSearchInput";
+import AdminFilterSelect from "../../components/admin/AdminFilterSelect";
+import AdminTable from "../../components/admin/AdminTable";
+import AdminPagination from "../../components/admin/AdminPagination";
+import ConfirmModal from "../../components/common/ConfirmModal";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { formatRoles } from "../../utils/adminFormat";
+
+const ROLE_OPTIONS = [
+ { value: "", label: "All roles" },
+ { value: "customer", label: "Customer" },
+ { value: "provider", label: "Provider" },
+];
+
+const STATUS_OPTIONS = [
+ { value: "", label: "All statuses" },
+ { value: "true", label: "Active" },
+ { value: "false", label: "Inactive" },
+];
+
+const AdminUsers = () => {
+ const { user: currentUser } = useAuth();
+ const [users, setUsers] = useState([]);
+ const [count, setCount] = useState(0);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(6);
+ const [search, setSearch] = useState("");
+ const [debouncedSearch, setDebouncedSearch] = useState("");
+ const [role, setRole] = useState("");
+ const [isActive, setIsActive] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [confirmAction, setConfirmAction] = useState(null);
+ const [actionLoading, setActionLoading] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedSearch(search);
+ setPage(1);
+ }, 300);
+ return () => clearTimeout(timer);
+ }, [search]);
+
+ const fetchUsers = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminUsers({
+ page,
+ limit,
+ search: debouncedSearch,
+ role,
+ isActive,
+ });
+
+ if (data.success) {
+ setUsers(data.data ?? []);
+ setCount(data.count ?? 0);
+ } else {
+ setUsers([]);
+ setCount(0);
+ setError(data.message || "Failed to load users.");
+ }
+ } catch (err) {
+ setUsers([]);
+ setCount(0);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load users. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [page, limit, debouncedSearch, role, isActive]);
+
+ useEffect(() => {
+ fetchUsers();
+ }, [fetchUsers]);
+
+ const handleToggleStatus = async () => {
+ if (!confirmAction) return;
+
+ try {
+ setActionLoading(true);
+ const { user, action } = confirmAction;
+ const data =
+ action === "activate"
+ ? await activateAdminUser(user._id)
+ : await deactivateAdminUser(user._id);
+
+ if (data.success) {
+ toast.success(data.message || "User status updated.");
+ setConfirmAction(null);
+ fetchUsers();
+ } else {
+ toast.error(data.message || "Failed to update user.");
+ }
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ "Unable to update user. Please try again."
+ );
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
{
+ setRole(value);
+ setPage(1);
+ }}
+ options={ROLE_OPTIONS}
+ />
+ {
+ setIsActive(value);
+ setPage(1);
+ }}
+ options={STATUS_OPTIONS}
+ />
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && users.length === 0 && (
+
+ )}
+
+ {!loading && !error && users.length > 0 && (
+
+
+ Name
+ Email
+ Roles
+ Status
+ Actions
+
+
+
+ {users.map((user) => {
+ const isSelf =
+ currentUser?._id === user._id ||
+ currentUser?.id === user._id;
+
+ return (
+
+
+
+ {user.name}
+
+
+ {user.email}
+
+
+
+ {user.email}
+
+
+ {formatRoles(user.roles)}
+
+
+
+
+ View
+
+ {user.isActive ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ )}
+
+ setConfirmAction(null)}
+ />
+ >
+ );
+};
+
+export default AdminUsers;
diff --git a/frontend/src/pages/admin/VenueDetail.jsx b/frontend/src/pages/admin/VenueDetail.jsx
new file mode 100644
index 0000000000..f645cf0689
--- /dev/null
+++ b/frontend/src/pages/admin/VenueDetail.jsx
@@ -0,0 +1,228 @@
+import { useEffect, useState } from "react";
+import { Link, useParams } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getAdminVenueById, activateAdminVenue, deactivateAdminVenue, } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminDetailRow from "../../components/admin/AdminDetailRow";
+import ConfirmModal from "../../components/common/ConfirmModal";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+import { getCategoryLabel } from "../../utils/venueFilters";
+import { getVenueCoverUrl } from "../../utils/venue";
+
+const AdminVenueDetail = () => {
+ const { id } = useParams();
+ const [venue, setVenue] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [confirmAction, setConfirmAction] = useState(null);
+ const [actionLoading, setActionLoading] = useState(false);
+
+ const fetchVenue = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminVenueById(id);
+
+ if (data.success) {
+ setVenue(data.data);
+ } else {
+ setVenue(null);
+ setError(data.message || "Venue not found.");
+ }
+ } catch (err) {
+ setVenue(null);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venue. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchVenue();
+ }, [id]);
+
+ const handleToggleStatus = async () => {
+ if (!confirmAction) return;
+
+ try {
+ setActionLoading(true);
+ const data =
+ confirmAction === "activate"
+ ? await activateAdminVenue(id)
+ : await deactivateAdminVenue(id);
+
+ if (data.success) {
+ toast.success(data.message || "Venue status updated.");
+ setConfirmAction(null);
+ fetchVenue();
+ } else {
+ toast.error(data.message || "Failed to update venue.");
+ }
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ "Unable to update venue. Please try again."
+ );
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ const owner = venue?.ownerId;
+ const coverUrl = venue ? getVenueCoverUrl(venue) : null;
+
+ return (
+ <>
+
+
+ ← Back to venues
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && venue && (
+ <>
+
+ {venue.isActive ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {coverUrl && (
+
+

+
+ )}
+
+
+
+ Description
+
+
+ {venue.description}
+
+
+
+ {venue.amenities?.length > 0 && (
+
+
+ Amenities
+
+
+ {venue.amenities.map((item) => (
+ -
+ {item}
+
+ ))}
+
+
+ )}
+
+
+
+
+ >
+ )}
+
+ setConfirmAction(null)}
+ />
+ >
+ );
+};
+
+export default AdminVenueDetail;
diff --git a/frontend/src/pages/admin/Venues.jsx b/frontend/src/pages/admin/Venues.jsx
new file mode 100644
index 0000000000..5ddb33dcec
--- /dev/null
+++ b/frontend/src/pages/admin/Venues.jsx
@@ -0,0 +1,282 @@
+import { useCallback, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getAdminVenues, activateAdminVenue, deactivateAdminVenue, } from "../../services/adminService";
+import AdminPageHeader from "../../components/admin/AdminPageHeader";
+import AdminSearchInput from "../../components/admin/AdminSearchInput";
+import AdminTextFilter from "../../components/admin/AdminTextFilter";
+import AdminFilterSelect from "../../components/admin/AdminFilterSelect";
+import AdminTable from "../../components/admin/AdminTable";
+import AdminPagination from "../../components/admin/AdminPagination";
+import ConfirmModal from "../../components/common/ConfirmModal";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import StatusBadge from "../../components/profile/StatusBadge";
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+import { getVenueCoverUrl } from "../../utils/venue";
+
+const STATUS_OPTIONS = [
+ { value: "", label: "All statuses" },
+ { value: "true", label: "Active" },
+ { value: "false", label: "Inactive" },
+];
+
+const AdminVenues = () => {
+ const [venues, setVenues] = useState([]);
+ const [count, setCount] = useState(0);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(6);
+ const [search, setSearch] = useState("");
+ const [debouncedSearch, setDebouncedSearch] = useState("");
+ const [city, setCity] = useState("");
+ const [debouncedCity, setDebouncedCity] = useState("");
+ const [isActive, setIsActive] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [confirmAction, setConfirmAction] = useState(null);
+ const [actionLoading, setActionLoading] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedSearch(search);
+ setPage(1);
+ }, 300);
+ return () => clearTimeout(timer);
+ }, [search]);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedCity(city);
+ setPage(1);
+ }, 300);
+ return () => clearTimeout(timer);
+ }, [city]);
+
+ const fetchVenues = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAdminVenues({
+ page,
+ limit,
+ search: debouncedSearch,
+ city: debouncedCity,
+ isActive,
+ });
+
+ if (data.success) {
+ setVenues(data.data ?? []);
+ setCount(data.count ?? 0);
+ } else {
+ setVenues([]);
+ setCount(0);
+ setError(data.message || "Failed to load venues.");
+ }
+ } catch (err) {
+ setVenues([]);
+ setCount(0);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venues. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [page, limit, debouncedSearch, debouncedCity, isActive]);
+
+ useEffect(() => {
+ fetchVenues();
+ }, [fetchVenues]);
+
+ const handleToggleStatus = async () => {
+ if (!confirmAction) return;
+
+ try {
+ setActionLoading(true);
+ const { venue, action } = confirmAction;
+ const data =
+ action === "activate"
+ ? await activateAdminVenue(venue._id)
+ : await deactivateAdminVenue(venue._id);
+
+ if (data.success) {
+ toast.success(data.message || "Venue status updated.");
+ setConfirmAction(null);
+ fetchVenues();
+ } else {
+ toast.error(data.message || "Failed to update venue.");
+ }
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ "Unable to update venue. Please try again."
+ );
+ } finally {
+ setActionLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
{
+ setIsActive(value);
+ setPage(1);
+ }}
+ options={STATUS_OPTIONS}
+ />
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && venues.length === 0 && (
+
+ )}
+
+ {!loading && !error && venues.length > 0 && (
+
+
+ {venues.map((venue) => {
+ const coverUrl = getVenueCoverUrl(venue);
+ const owner = venue.ownerId;
+
+ return (
+
+
+ {coverUrl ? (
+

+ ) : (
+
+ No image
+
+ )}
+
+
+
+
+ {venue.title}
+
+
+ {venue.city || "—"} · Owner:{" "}
+ {owner?.name || "Unknown"}
+
+
+ {formatBookingPriceDisplay(venue.price)}
+
+
+
+
+
+
+
+ View
+
+ {venue.isActive ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ )}
+
+ setConfirmAction(null)}
+ />
+ >
+ );
+};
+
+export default AdminVenues;
diff --git a/frontend/src/pages/auth/ForgotPassword.jsx b/frontend/src/pages/auth/ForgotPassword.jsx
new file mode 100644
index 0000000000..906bf75477
--- /dev/null
+++ b/frontend/src/pages/auth/ForgotPassword.jsx
@@ -0,0 +1,113 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+import api from "../../services/axios";
+import AuthLayout from "../../components/auth/AuthLayout";
+
+const authLinkClass =
+ "font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const inputClass = (hasError) =>
+ `w-full rounded-lg border bg-white px-3.5 py-3 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70 ${
+ hasError
+ ? "border-red-400 focus:border-red-500 focus:ring-red-100"
+ : "border-gray-300 focus:border-red-500 focus:ring-red-100"
+ }`;
+
+const ForgotPassword = () => {
+ const navigate = useNavigate();
+
+ const [email, setEmail] = useState("");
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ setError("");
+
+ if (!email.trim()) {
+ return setError("Email is required");
+ }
+
+ try {
+ setLoading(true);
+
+ const { data } = await api.post("/auth/forgot-password", {
+ email,
+ });
+
+ toast.success(data.message);
+
+ navigate("/reset-password", {
+ state: { email },
+ });
+ } catch (error) {
+ setError(error.response?.data?.message || "Failed to send OTP");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Forgot password
+
+
+ Enter your email to receive an OTP.
+
+
+
+
+
+
+ Back to Login
+
+
+
+
+ );
+};
+
+export default ForgotPassword;
diff --git a/frontend/src/pages/auth/Login.jsx b/frontend/src/pages/auth/Login.jsx
new file mode 100644
index 0000000000..e184fc04b3
--- /dev/null
+++ b/frontend/src/pages/auth/Login.jsx
@@ -0,0 +1,211 @@
+import { useState, useEffect } from "react";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import api from "../../services/axios";
+import toast from "react-hot-toast";
+import { useAuth } from "../../context/AuthContext";
+import AuthLayout from "../../components/auth/AuthLayout";
+import PasswordInput from "../../components/auth/PasswordInput";
+import BrandName from "../../components/common/BrandName";
+import logTheme from "../../assets/log-theme.webp";
+
+const authLinkClass =
+ "font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const authLinkClassSm =
+ "text-sm font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const inputClass = (hasError) =>
+ `w-full rounded-lg border bg-white px-3.5 py-3 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70 ${hasError
+ ? "border-red-400 focus:border-red-500 focus:ring-red-100"
+ : "border-gray-300 focus:border-red-500 focus:ring-red-100"
+ }`;
+
+const Login = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { refreshUser, isAdmin, authReady, user } = useAuth();
+
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+
+ const handleChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ });
+
+ setErrors({
+ ...errors,
+ [e.target.name]: "",
+ });
+ };
+
+ const validate = () => {
+ const newErrors = {};
+
+ if (!formData.email.trim()) {
+ newErrors.email = "Email is required";
+ }
+
+ if (!formData.password.trim()) {
+ newErrors.password = "Password is required";
+ }
+
+ setErrors(newErrors);
+
+ return Object.keys(newErrors).length === 0;
+ };
+
+ useEffect(() => {
+ if (authReady && user && isAdmin) {
+ navigate("/admin/dashboard", { replace: true });
+ }
+ }, [authReady, user, isAdmin, navigate]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validate()) return;
+
+ try {
+ setLoading(true);
+
+ const { data } = await api.post("/auth/login", formData);
+
+ if (data.success) {
+ await refreshUser();
+
+ toast.success("Login successful");
+
+ const roles = data.user?.roles ?? [];
+ const fromPath =
+ location.state?.from && typeof location.state.from === "string"
+ ? location.state.from
+ : null;
+
+ if (roles.includes("admin")) {
+ const adminRedirect =
+ fromPath && fromPath.startsWith("/admin")
+ ? fromPath
+ : "/admin/dashboard";
+ navigate(adminRedirect, { replace: true });
+ return;
+ }
+
+ const redirectTo = fromPath || "/";
+ navigate(redirectTo, { replace: true });
+ }
+ } catch (error) {
+ toast.error(error.response?.data?.message || "Login failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ Customers can find and reserve venues; providers can manage listings,
+ availability, and bookings, all on{" "}
+
+ >
+ }
+ brandingPoints={[
+ "Planning a celebration or running a venue? Pick up where you left off on Book My Venue.",
+ "From wedding halls to meeting spaces, book your next event or run your venue on Book My Venue.",
+ ]}
+ >
+
+
+
+ Welcome back to{" "}
+
+
+ Welcome back
+
+
+
+
+
+ Don't have an account?{" "}
+
+ Register
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/frontend/src/pages/auth/Register.jsx b/frontend/src/pages/auth/Register.jsx
new file mode 100644
index 0000000000..f4968d7ac9
--- /dev/null
+++ b/frontend/src/pages/auth/Register.jsx
@@ -0,0 +1,255 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import api from "../../services/axios";
+import toast from "react-hot-toast";
+import AuthLayout from "../../components/auth/AuthLayout";
+import PasswordInput from "../../components/auth/PasswordInput";
+import BrandName from "../../components/common/BrandName";
+import regTheme from "../../assets/reg-theme.jpg";
+
+const authLinkClass =
+ "font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const inputClass = (hasError) =>
+ `w-full rounded-lg border bg-white px-3.5 py-3 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition-colors focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-70 ${hasError
+ ? "border-red-400 focus:border-red-500 focus:ring-red-100"
+ : "border-gray-300 focus:border-red-500 focus:ring-red-100"
+ }`;
+
+const Register = () => {
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ phone: "",
+ password: "",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+
+ const handleChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ });
+
+ setErrors({
+ ...errors,
+ [e.target.name]: "",
+ });
+ };
+
+ const validate = () => {
+ const newErrors = {};
+
+ if (!formData.name.trim()) {
+ newErrors.name = "Name is required";
+ }
+
+ if (!formData.email.trim()) {
+ newErrors.email = "Email is required";
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = "Invalid email";
+ }
+
+ if (!formData.phone.trim()) {
+ newErrors.phone = "Phone number is required";
+ } else if (!/^[6-9]\d{9}$/.test(formData.phone)) {
+ newErrors.phone = "Invalid phone number";
+ }
+
+ if (!formData.password) {
+ newErrors.password = "Password is required";
+ } else if (
+ !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(formData.password)
+ ) {
+ newErrors.password = "Must contain uppercase, lowercase and number";
+ }
+
+ setErrors(newErrors);
+
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validate()) return;
+
+ try {
+ setLoading(true);
+
+ const { data } = await api.post("/auth/register", formData);
+
+ if (data.success) {
+ toast.success(data.message);
+
+ navigate("/verify-email", {
+ state: {
+ email: formData.email,
+ },
+ });
+ }
+ } catch (error) {
+ toast.error(error.response?.data?.message || "Registration failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ Reserve the right space for every occasion, or list your venue, take
+ bookings, and build your hosting business, all on{" "}
+
+ >
+ }
+ brandingPoints={[
+ "Browse venues by city, category, and capacity",
+ "Book available slots with secure online payment",
+ "List your venue and manage availability as a host",
+ "Track bookings from your dashboard in one place",
+ ]}
+ >
+
+
+
+ Get started with{" "}
+
+
+ Create your account
+
+
+
+
+
+ Already have an account?{" "}
+
+ Login
+
+
+
+
+ );
+};
+
+export default Register;
diff --git a/frontend/src/pages/auth/ResetPassword.jsx b/frontend/src/pages/auth/ResetPassword.jsx
new file mode 100644
index 0000000000..7dbcd49580
--- /dev/null
+++ b/frontend/src/pages/auth/ResetPassword.jsx
@@ -0,0 +1,172 @@
+import { useState } from "react";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+import api from "../../services/axios";
+import AuthLayout from "../../components/auth/AuthLayout";
+import OtpInput from "../../components/auth/OtpInput";
+import PasswordInput from "../../components/auth/PasswordInput";
+
+const authLinkClass =
+ "font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const ResetPassword = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const email = location.state?.email || "";
+
+ const [formData, setFormData] = useState({
+ otp: "",
+ newPassword: "",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+
+ const handleChange = (e) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+
+ setErrors((prev) => ({
+ ...prev,
+ [e.target.name]: "",
+ }));
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.otp.trim()) {
+ newErrors.otp = "OTP is required";
+ }
+
+ if (!formData.newPassword.trim()) {
+ newErrors.newPassword = "Password is required";
+ } else if (formData.newPassword.length < 6) {
+ newErrors.newPassword = "Password must be at least 6 characters";
+ }
+
+ setErrors(newErrors);
+
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) return;
+
+ try {
+ setLoading(true);
+
+ const { data } = await api.post("/auth/reset-password", {
+ email,
+ otp: formData.otp,
+ newPassword: formData.newPassword,
+ });
+
+ toast.success(data.message);
+
+ navigate("/login");
+ } catch (error) {
+ toast.error(error.response?.data?.message || "Password reset failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!email) {
+ return (
+
+
+
+ Invalid Request
+
+
+ Please start from Forgot Password.
+
+
+ Go to Forgot Password
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Reset password
+
+
+ Code sent to{" "}
+ {email}
+
+
+
+
+
+
+ Back to Login
+
+
+
+
+ );
+};
+
+export default ResetPassword;
diff --git a/frontend/src/pages/auth/VerifyEmail.jsx b/frontend/src/pages/auth/VerifyEmail.jsx
new file mode 100644
index 0000000000..72f529f5d9
--- /dev/null
+++ b/frontend/src/pages/auth/VerifyEmail.jsx
@@ -0,0 +1,150 @@
+import { useState } from "react";
+import { useLocation, useNavigate, Link } from "react-router-dom";
+import toast from "react-hot-toast";
+import api from "../../services/axios";
+import AuthLayout from "../../components/auth/AuthLayout";
+import OtpInput from "../../components/auth/OtpInput";
+
+const authLinkClass =
+ "font-medium text-red-600 underline-offset-2 transition-colors hover:text-red-700 hover:underline";
+
+const VerifyEmail = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const email = location.state?.email || "";
+
+ const [otp, setOtp] = useState("");
+ const [error, setError] = useState("");
+
+ const [loading, setLoading] = useState(false);
+ const [resending, setResending] = useState(false);
+
+ const handleVerify = async (e) => {
+ e.preventDefault();
+
+ setError("");
+
+ if (!email) {
+ setError("Invalid verification request. Please register again.");
+ return;
+ }
+
+ if (!otp.trim()) {
+ setError("OTP is required");
+ return;
+ }
+
+ if (otp.length !== 6) {
+ setError("OTP must be 6 digits");
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ const { data } = await api.post("/auth/verify-email", {
+ email,
+ otp,
+ });
+
+ toast.success(data.message);
+
+ navigate("/login");
+ } catch (error) {
+ setError(error.response?.data?.message || "Verification failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleResendOtp = async () => {
+ if (!email) {
+ setError("Email not found");
+ return;
+ }
+
+ try {
+ setResending(true);
+
+ const { data } = await api.post("/auth/resend-otp", {
+ email,
+ });
+
+ toast.success(data.message);
+ } catch (error) {
+ toast.error(error.response?.data?.message || "Failed to resend OTP");
+ } finally {
+ setResending(false);
+ }
+ };
+
+ return (
+
+
+
+ Verify email
+
+
+ Enter the 6-digit code sent to
+
+
+ {email ? (
+
+ {email}
+
+ ) : (
+
+ No email found. Please register again.
+
+ )}
+
+
+
+
+
+
+ Wrong email?{" "}
+
+ Register Again
+
+
+
+
+ );
+};
+
+export default VerifyEmail;
diff --git a/frontend/src/pages/guest/Home.jsx b/frontend/src/pages/guest/Home.jsx
new file mode 100644
index 0000000000..6373468e19
--- /dev/null
+++ b/frontend/src/pages/guest/Home.jsx
@@ -0,0 +1,26 @@
+import Hero from "../../components/home/Hero";
+import HowItWorks from "../../components/home/HowItWorks";
+import FeaturedVenues from "../../components/home/FeaturedVenues";
+import ProviderCTA from "../../components/home/ProviderCTA";
+
+const Home = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/frontend/src/pages/guest/NotFound.jsx b/frontend/src/pages/guest/NotFound.jsx
new file mode 100644
index 0000000000..d4ef39fc62
--- /dev/null
+++ b/frontend/src/pages/guest/NotFound.jsx
@@ -0,0 +1,21 @@
+import { Link } from "react-router-dom";
+
+const NotFound = () => {
+ return (
+
+ 404
+ Page not found
+
+ The page you are looking for does not exist or may have been moved.
+
+
+ Back to Home
+
+
+ );
+};
+
+export default NotFound;
diff --git a/frontend/src/pages/guest/VenueDetails.jsx b/frontend/src/pages/guest/VenueDetails.jsx
new file mode 100644
index 0000000000..3200e55dd1
--- /dev/null
+++ b/frontend/src/pages/guest/VenueDetails.jsx
@@ -0,0 +1,502 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
+import { ArrowLeft, CalendarDays } from "lucide-react";
+import { getPublicVenueById } from "../../services/venueService";
+import { getVenueAvailability } from "../../services/availabilityService";
+import { createOrder, verifyPayment } from "../../services/paymentService";
+import useRazorpay, { PAYMENT_UNAVAILABLE_MESSAGE } from "../../hooks/useRazorpay";
+import { useAuth } from "../../context/AuthContext";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import VenueImageGallery from "../../components/venue-details/VenueImageGallery";
+import VenueQuickFacts from "../../components/venue-details/VenueQuickFacts";
+import VenueBookingCard, {
+ BookingSuccessCard,
+} from "../../components/venue-details/VenueBookingCard";
+import VenueAmenities from "../../components/venue-details/VenueAmenities";
+import VenueLocation from "../../components/venue-details/VenueLocation";
+import VenueRules from "../../components/venue-details/VenueRules";
+import VenueHostedBy from "../../components/venue-details/VenueHostedBy";
+import VenueSlotCard from "../../components/venue-details/VenueSlotCard";
+import MobileBookingBar from "../../components/venue-details/MobileBookingBar";
+import { getVenueImages } from "../../utils/venue";
+import { buildBookingPayload, validateBookingPayload, } from "../../utils/booking";
+import { formatSlotDate, formatSlotDateCompact, toDateKey, } from "../../utils/formatDate";
+import { clearBookingContext, filterCustomerBookableSlots, findSlotById, getCustomerAvailabilityEmptyState, isSlotStillBookable, loadBookingContext, saveBookingContext, } from "../../utils/customerSlots";
+import { groupSlotsByDate } from "../../utils/predefinedSlots";
+
+const AlertBox = ({ variant, title, children }) => {
+ const styles =
+ variant === "success"
+ ? "border-green-200 bg-green-50 text-green-800"
+ : "border-red-200 bg-red-50 text-red-800";
+
+ return (
+
+
{title}
+ {children &&
{children}
}
+
+ );
+};
+
+const VenueDetails = () => {
+ const { id: venueId } = useParams();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { isAuthenticated } = useAuth();
+ const { openCheckout, isRazorpayConfigured } = useRazorpay();
+ const restoredContextRef = useRef(false);
+
+ const [venue, setVenue] = useState(null);
+ const [slots, setSlots] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [slotsError, setSlotsError] = useState("");
+ const [selectedSlot, setSelectedSlot] = useState(null);
+ const [isPaying, setIsPaying] = useState(false);
+ const [paymentError, setPaymentError] = useState("");
+ const [bookingConfirmation, setBookingConfirmation] = useState(null);
+ const [confirmedSlotSnapshot, setConfirmedSlotSnapshot] = useState(null);
+ const [slotUnavailableMessage, setSlotUnavailableMessage] = useState("");
+
+ const fetchAvailability = async () => {
+ if (!venueId) return;
+
+ try {
+ setSlotsError("");
+ const availabilityData = await getVenueAvailability(venueId);
+
+ if (availabilityData.success) {
+ setSlots(availabilityData.data ?? []);
+ } else {
+ setSlots([]);
+ setSlotsError(
+ availabilityData.message || "Unable to load availability."
+ );
+ }
+ } catch (err) {
+ setSlots([]);
+ setSlotsError(
+ err.response?.data?.message ||
+ "Unable to load availability. Please try again later."
+ );
+ }
+ };
+
+ const fetchVenueDetails = async ({ preserveSelection = false } = {}) => {
+ if (!venueId) {
+ setVenue(null);
+ setSlots([]);
+ setLoading(false);
+ setError("Invalid venue link.");
+ return;
+ }
+
+ setLoading(true);
+ setError("");
+ setSlotsError("");
+ if (!preserveSelection) {
+ setSelectedSlot(null);
+ setSlotUnavailableMessage("");
+ }
+ setPaymentError("");
+
+ try {
+ const venueData = await getPublicVenueById(venueId);
+
+ if (!venueData.success) {
+ setVenue(null);
+ setSlots([]);
+ setError(venueData.message || "Venue not found.");
+ return;
+ }
+
+ setVenue(venueData.data);
+ await fetchAvailability();
+ } catch (err) {
+ setVenue(null);
+ setSlots([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venue details. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ restoredContextRef.current = false;
+ fetchVenueDetails();
+ }, [venueId]);
+
+ useEffect(() => {
+ if (loading || restoredContextRef.current || slots.length === 0) return;
+
+ const context = loadBookingContext(venueId);
+ if (!context?.slotId) return;
+
+ restoredContextRef.current = true;
+ const slot = findSlotById(slots, context.slotId);
+
+ if (slot && isSlotStillBookable(slots, slot)) {
+ setSelectedSlot(slot);
+ clearBookingContext();
+ } else {
+ clearBookingContext();
+ setSlotUnavailableMessage(
+ "Your previously selected slot is no longer available. Please choose another."
+ );
+ }
+ }, [loading, slots, venueId]);
+
+ useEffect(() => {
+ if (!selectedSlot || bookingConfirmation) return;
+
+ if (!isSlotStillBookable(slots, selectedSlot)) {
+ setSelectedSlot(null);
+ clearBookingContext();
+ setSlotUnavailableMessage(
+ "Your selected slot is no longer available. Please choose another slot."
+ );
+ }
+ }, [slots, selectedSlot, bookingConfirmation]);
+
+ const venueImages = useMemo(() => getVenueImages(venue), [venue]);
+
+ const bookableSlots = useMemo(
+ () => filterCustomerBookableSlots(slots),
+ [slots]
+ );
+
+ const groupedSlots = useMemo(
+ () => groupSlotsByDate(bookableSlots),
+ [bookableSlots]
+ );
+
+ const availabilityEmptyState = useMemo(
+ () => getCustomerAvailabilityEmptyState(slots),
+ [slots]
+ );
+
+ const bookingPayload = useMemo(
+ () => buildBookingPayload(venue, selectedSlot),
+ [venue, selectedSlot]
+ );
+
+ const payloadValidation = useMemo(
+ () => validateBookingPayload(bookingPayload),
+ [bookingPayload]
+ );
+
+ const canBook =
+ payloadValidation.valid && isRazorpayConfigured() && !isPaying && !bookingConfirmation;
+
+ const handleSelectSlot = (slot) => {
+ setSelectedSlot((prev) => {
+ const next = prev?._id === slot._id ? null : slot;
+ if (next && venueId) {
+ saveBookingContext(venueId, next._id);
+ } else {
+ clearBookingContext();
+ }
+ return next;
+ });
+ setPaymentError("");
+ setSlotUnavailableMessage("");
+ };
+
+ const handleBookNow = async () => {
+ if (isPaying || bookingConfirmation) return;
+
+ const validation = validateBookingPayload(bookingPayload);
+
+ if (!validation.valid) {
+ setPaymentError(validation.error);
+ return;
+ }
+
+ if (!isRazorpayConfigured()) {
+ setPaymentError(PAYMENT_UNAVAILABLE_MESSAGE);
+ return;
+ }
+
+ if (!isAuthenticated) {
+ if (selectedSlot?._id) {
+ saveBookingContext(venueId, selectedSlot._id);
+ }
+ navigate("/login", { state: { from: location.pathname } });
+ return;
+ }
+
+ setIsPaying(true);
+ setPaymentError("");
+
+ const slotSnapshot = selectedSlot;
+
+ try {
+ const orderResponse = await createOrder(bookingPayload);
+
+ if (!orderResponse?.success || !orderResponse?.order) {
+ throw new Error(orderResponse?.message || "Failed to create order.");
+ }
+
+ const paymentResult = await openCheckout(orderResponse.order, {
+ name: "BookMyVenue",
+ description: venue?.title || "Venue booking",
+ });
+
+ const verifyResponse = await verifyPayment(bookingPayload, paymentResult);
+
+ if (!verifyResponse.success) {
+ throw new Error(verifyResponse.message || "Payment verification failed.");
+ }
+
+ setBookingConfirmation(verifyResponse.data ?? null);
+ setConfirmedSlotSnapshot(slotSnapshot);
+ setSelectedSlot(null);
+ clearBookingContext();
+ await fetchVenueDetails({ preserveSelection: true });
+ } catch (err) {
+ setPaymentError(
+ err.response?.data?.message ||
+ err.message ||
+ "Payment failed. Please try again."
+ );
+ } finally {
+ setIsPaying(false);
+ }
+ };
+
+ const handleDismissBookingSuccess = () => {
+ setBookingConfirmation(null);
+ setConfirmedSlotSnapshot(null);
+ setPaymentError("");
+ setSlotUnavailableMessage("");
+ };
+
+ const handleBack = () => {
+ if (location.key !== "default") {
+ navigate(-1);
+ return;
+ }
+
+ navigate("/venues");
+ };
+
+ const scrollToAvailability = () => {
+ document.getElementById("availability")?.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ };
+
+ const renderAlerts = () => (
+
+ {!isRazorpayConfigured() && (
+
+ {PAYMENT_UNAVAILABLE_MESSAGE}
+
+ )}
+
+ {slotUnavailableMessage && (
+
+ {slotUnavailableMessage}
+
+ )}
+
+ {selectedSlot && !payloadValidation.valid && (
+
+ {payloadValidation.error}
+
+ )}
+
+ {paymentError && (
+
+ {paymentError}
+
+ )}
+
+ {bookingConfirmation && (
+
navigate("/my-bookings")}
+ onBookAnother={handleDismissBookingSuccess}
+ />
+ )}
+
+ );
+
+ const bookingCardProps = {
+ venue,
+ selectedSlot,
+ bookableSlotCount: bookableSlots.length,
+ isAuthenticated,
+ loginPath: location.pathname,
+ canBook,
+ isPaying,
+ onBookNow: handleBookNow,
+ onViewAvailability: scrollToAvailability,
+ };
+
+ return (
+
+
+
+
+ {loading && (
+
+
Venue Details
+
Loading venue...
+
+
+
+
+ )}
+
+ {!loading && (error || !venue) && (
+
+
Venue Details
+
+ fetchVenueDetails()}
+ />
+
+
+ )}
+
+ {!loading && !error && venue && (
+ <>
+
+
+
+
+
+
+
+ About this venue
+
+
+ {venue.description ||
+ "No description provided for this venue yet."}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Availability
+
+
+ One slot per booking — tap to select
+
+
+
+
+ {renderAlerts()}
+
+ {slotsError && (
+
+
+
+ )}
+
+ {!slotsError && availabilityEmptyState && (
+
+
+ {availabilityEmptyState.title}
+
+
+ {availabilityEmptyState.description}
+
+
+ Browse other venues
+
+
+ )}
+
+ {!slotsError && !availabilityEmptyState && (
+
+ {groupedSlots.map((group) => (
+
+
+ {formatSlotDateCompact(group.date)}
+
+
+ {group.slots.map((slot) => (
+
+ ))}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+export default VenueDetails;
diff --git a/frontend/src/pages/guest/Venues.jsx b/frontend/src/pages/guest/Venues.jsx
new file mode 100644
index 0000000000..b2803a0ae0
--- /dev/null
+++ b/frontend/src/pages/guest/Venues.jsx
@@ -0,0 +1,129 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { getAllVenues } from "../../services/venueService";
+import SearchBar from "../../components/venues/SearchBar";
+import VenueFilters from "../../components/venues/VenueFilters";
+import VenueGrid from "../../components/venues/VenueGrid";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import { DEFAULT_VENUE_FILTERS, countActiveFilters, extractCities, filterAndSortVenues, } from "../../utils/venueFilters";
+
+const Venues = () => {
+ const [venues, setVenues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [filters, setFilters] = useState(DEFAULT_VENUE_FILTERS);
+
+ const fetchVenues = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getAllVenues();
+
+ if (data.success) {
+ setVenues(data.data ?? []);
+ } else {
+ setVenues([]);
+ setError(data.message || "Failed to load venues.");
+ }
+ } catch (err) {
+ setVenues([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venues. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchVenues();
+ }, []);
+
+ const cities = useMemo(() => extractCities(venues), [venues]);
+
+ const filteredVenues = useMemo(
+ () => filterAndSortVenues(venues, filters),
+ [venues, filters]
+ );
+
+ const activeFilterCount = useMemo(
+ () => countActiveFilters(filters),
+ [filters]
+ );
+
+ const handleFilterChange = useCallback((name, value) => {
+ setFilters((prev) => ({ ...prev, [name]: value }));
+ }, []);
+
+ const handleClearFilters = useCallback(() => {
+ setFilters(DEFAULT_VENUE_FILTERS);
+ }, []);
+
+ return (
+
+
+
+
+
+ handleFilterChange("search", value)}
+ onCityChange={(value) => handleFilterChange("city", value)}
+ disabled={loading}
+ />
+
+
+
+
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && venues.length === 0 && (
+
+ )}
+
+ {!loading && !error && venues.length > 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default Venues;
diff --git a/frontend/src/pages/provider/CreateVenue.jsx b/frontend/src/pages/provider/CreateVenue.jsx
new file mode 100644
index 0000000000..780d229bd2
--- /dev/null
+++ b/frontend/src/pages/provider/CreateVenue.jsx
@@ -0,0 +1,69 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+import { createVenue } from "../../services/venueService";
+import VenueForm from "../../components/provider/venue-form/VenueForm";
+import { EMPTY_VENUE_FORM } from "../../utils/venueForm";
+
+const CreateVenue = () => {
+ const navigate = useNavigate();
+ const [submitError, setSubmitError] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const handleSubmit = async (formData) => {
+ try {
+ setSubmitting(true);
+ setSubmitError("");
+
+ const data = await createVenue(formData);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to create venue.");
+ }
+
+ toast.success(data.message || "Venue created successfully.");
+
+ const newVenueId = data.data?._id;
+ if (newVenueId) {
+ navigate(`/provider/venues/${newVenueId}/availability`);
+ } else {
+ navigate("/provider/venues");
+ }
+ } catch (err) {
+ const message =
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to create venue. Please try again.";
+ setSubmitError(message);
+ toast.error(message);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <>
+
+
+ Create Venue
+
+
+ Add a new venue to your provider inventory.
+
+
+
+ navigate("/provider/venues")}
+ submitLabel="Create Venue"
+ submittingLabel="Creating..."
+ />
+ >
+ );
+};
+
+export default CreateVenue;
diff --git a/frontend/src/pages/provider/Dashboard.jsx b/frontend/src/pages/provider/Dashboard.jsx
new file mode 100644
index 0000000000..4d75373382
--- /dev/null
+++ b/frontend/src/pages/provider/Dashboard.jsx
@@ -0,0 +1,383 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import {
+ Building2,
+ CalendarCheck,
+ CalendarClock,
+ CheckCircle2,
+ IndianRupee,
+ PlusCircle,
+ ArrowRight,
+ BadgeCheck,
+ Eye,
+ EyeOff,
+} from "lucide-react";
+import { getMyVenues } from "../../services/venueService";
+import { getProviderBookings } from "../../services/bookingService";
+import { getBookingStats } from "../../utils/providerBookingFilters";
+import { formatPrice } from "../../utils/formatPrice";
+import ProviderBookingRow, {
+ ProviderBookingTableHeader,
+} from "../../components/provider/bookings/ProviderBookingRow";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import EmptyState from "../../components/common/EmptyState";
+
+const isConfirmedPaidBooking = (booking) =>
+ booking?.bookingStatus === "confirmed" && booking?.paymentStatus === "paid";
+
+const getConfirmedPaidRevenueStats = (bookings = []) => {
+ let confirmedBookings = 0;
+ let totalRevenue = 0;
+
+ bookings.forEach((booking) => {
+ if (!isConfirmedPaidBooking(booking)) return;
+
+ confirmedBookings += 1;
+
+ const amount = Number(booking.amount);
+ if (Number.isFinite(amount) && amount > 0) {
+ totalRevenue += amount;
+ }
+ });
+
+ return { confirmedBookings, totalRevenue };
+};
+
+const statConfig = [
+ {
+ key: "totalVenues",
+ label: "Total venues",
+ icon: Building2,
+ iconClass: "bg-red-50 text-red-600",
+ },
+ {
+ key: "activeVenues",
+ label: "Active venues",
+ icon: CheckCircle2,
+ iconClass: "bg-emerald-50 text-emerald-600",
+ },
+ {
+ key: "totalBookings",
+ label: "Total bookings",
+ icon: CalendarCheck,
+ iconClass: "bg-sky-50 text-sky-600",
+ },
+ {
+ key: "confirmedBookings",
+ label: "Confirmed bookings",
+ icon: BadgeCheck,
+ iconClass: "bg-violet-50 text-violet-600",
+ },
+ {
+ key: "upcoming",
+ label: "Upcoming",
+ icon: CalendarClock,
+ iconClass: "bg-amber-50 text-amber-600",
+ },
+ {
+ key: "revenueCollected",
+ label: "Revenue collected",
+ icon: IndianRupee,
+ iconClass: "bg-emerald-50 text-emerald-600",
+ },
+];
+
+const DashboardStatGrid = ({
+ stats,
+ revenueVisible = false,
+ onToggleRevenueVisibility,
+}) => (
+
+ {statConfig.map(({ key, label, icon: Icon, iconClass }) => {
+ const isRevenue = key === "revenueCollected";
+ const displayValue =
+ isRevenue && !revenueVisible ? "₹••••••" : (stats[key] ?? 0);
+
+ return (
+
+
+
+
+
+
+
+
+ {displayValue}
+
+ {isRevenue && onToggleRevenueVisibility && (
+
+ )}
+
+
+ {label}
+
+
+
+
+ );
+ })}
+
+);
+
+const quickActions = [
+ {
+ to: "/provider/venues/new",
+ label: "Create venue",
+ icon: PlusCircle,
+ primary: true,
+ },
+ {
+ to: "/provider/venues",
+ label: "My venues",
+ icon: Building2,
+ primary: false,
+ },
+ {
+ to: "/provider/bookings",
+ label: "All bookings",
+ icon: CalendarCheck,
+ primary: false,
+ },
+];
+
+const QuickActions = () => (
+
+ {quickActions.map(({ to, label, icon: Icon, primary }) => (
+
+
+ {label}
+
+ ))}
+
+);
+
+const ProviderDashboard = () => {
+ const [venues, setVenues] = useState([]);
+ const [bookings, setBookings] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [bookingsError, setBookingsError] = useState("");
+ const [revenueVisible, setRevenueVisible] = useState(false);
+
+ const fetchBookingsOnly = async () => {
+ try {
+ setBookingsError("");
+
+ const bookingsData = await getProviderBookings();
+
+ if (bookingsData.success) {
+ setBookings(bookingsData.data ?? []);
+ } else {
+ setBookings([]);
+ setBookingsError(
+ bookingsData.message || "Failed to load bookings."
+ );
+ }
+ } catch (err) {
+ setBookings([]);
+ setBookingsError(
+ err.response?.data?.message ||
+ "Unable to load bookings. Please try again."
+ );
+ }
+ };
+
+ const fetchDashboardData = async () => {
+ try {
+ setLoading(true);
+ setError("");
+ setBookingsError("");
+
+ const [venuesData, bookingsData] = await Promise.all([
+ getMyVenues(),
+ getProviderBookings(),
+ ]);
+
+ if (venuesData.success) {
+ setVenues(venuesData.data ?? []);
+ } else {
+ setVenues([]);
+ setError(venuesData.message || "Failed to load venues.");
+ return;
+ }
+
+ if (bookingsData.success) {
+ setBookings(bookingsData.data ?? []);
+ } else {
+ setBookings([]);
+ setBookingsError(
+ bookingsData.message || "Failed to load bookings."
+ );
+ }
+ } catch (err) {
+ setVenues([]);
+ setBookings([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load dashboard data. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDashboardData();
+ }, []);
+
+ const bookingStats = useMemo(() => getBookingStats(bookings), [bookings]);
+
+ const revenueStats = useMemo(
+ () => getConfirmedPaidRevenueStats(bookings),
+ [bookings]
+ );
+
+ const dashboardStats = useMemo(
+ () => ({
+ totalVenues: venues.length,
+ activeVenues: venues.filter((v) => v.isActive === true).length,
+ totalBookings: bookingStats.total,
+ confirmedBookings: revenueStats.confirmedBookings,
+ upcoming: bookingStats.upcoming,
+ revenueCollected: formatPrice(revenueStats.totalRevenue).amount,
+ }),
+ [venues, bookingStats, revenueStats]
+ );
+
+ const recentBookings = useMemo(
+ () =>
+ [...bookings]
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+ .slice(0, 5),
+ [bookings]
+ );
+
+ const hasVenues = venues.length > 0;
+
+ return (
+ <>
+
+
+ Provider Dashboard
+
+
+ Overview of your venues and incoming bookings.
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && !hasVenues && (
+ <>
+
+
+
+
+
+ Create your first venue
+
+
+ >
+ )}
+
+ {!loading && !error && hasVenues && (
+
+
+ setRevenueVisible((visible) => !visible)
+ }
+ />
+
+
+
+ {bookingsError && (
+
+ )}
+
+
+
+
+
+ Recent bookings
+
+
+ Latest reservations across your venues
+
+
+
+
+ View all
+
+
+
+
+ {!bookingsError && recentBookings.length === 0 && (
+
+
+
+ )}
+
+ {!bookingsError && recentBookings.length > 0 && (
+ <>
+
+
+ {recentBookings.map((booking) => (
+
+ ))}
+
+ >
+ )}
+
+
+ )}
+ >
+ );
+};
+
+export default ProviderDashboard;
diff --git a/frontend/src/pages/provider/EditVenue.jsx b/frontend/src/pages/provider/EditVenue.jsx
new file mode 100644
index 0000000000..14f30e1abf
--- /dev/null
+++ b/frontend/src/pages/provider/EditVenue.jsx
@@ -0,0 +1,130 @@
+import { useEffect, useMemo, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getProviderVenueById, updateVenue, } from "../../services/venueService";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import VenueForm from "../../components/provider/venue-form/VenueForm";
+import { mapVenueToFormValues } from "../../utils/venueForm";
+
+const EditVenue = () => {
+ const { id: venueId } = useParams();
+ const navigate = useNavigate();
+
+ const [initialValues, setInitialValues] = useState(null);
+ const [existingImages, setExistingImages] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [fetchError, setFetchError] = useState("");
+ const [submitError, setSubmitError] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const fetchVenue = async () => {
+ if (!venueId) {
+ setFetchError("Invalid venue link.");
+ setInitialValues(null);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setFetchError("");
+
+ const data = await getProviderVenueById(venueId);
+
+ if (!data.success) {
+ setFetchError(data.message || "Venue not found.");
+ return;
+ }
+
+ setInitialValues(mapVenueToFormValues(data.data));
+ setExistingImages(
+ (data.data.images || []).map((img) => ({
+ url: img.url,
+ key: img.public_id || img.url,
+ }))
+ );
+ } catch (err) {
+ setFetchError(
+ err.response?.data?.message ||
+ "Unable to load venue. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (venueId) {
+ fetchVenue();
+ }
+ }, [venueId]);
+
+ const formKey = useMemo(
+ () => (initialValues ? `${venueId}-loaded` : "loading"),
+ [initialValues, venueId]
+ );
+
+ const handleSubmit = async (formData) => {
+ try {
+ setSubmitting(true);
+ setSubmitError("");
+
+ const data = await updateVenue(venueId, formData);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to update venue.");
+ }
+
+ toast.success(data.message || "Venue updated successfully.");
+ navigate("/provider/venues");
+ } catch (err) {
+ const message =
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to update venue. Please try again.";
+ setSubmitError(message);
+ toast.error(message);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ if (fetchError || !initialValues) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+ Edit Venue
+
+
+ Update your venue listing details.
+
+
+
+ navigate("/provider/venues")}
+ submitLabel="Save Changes"
+ submittingLabel="Updating..."
+ />
+ >
+ );
+};
+
+export default EditVenue;
diff --git a/frontend/src/pages/provider/ManageAvailability.jsx b/frontend/src/pages/provider/ManageAvailability.jsx
new file mode 100644
index 0000000000..a24decb5a8
--- /dev/null
+++ b/frontend/src/pages/provider/ManageAvailability.jsx
@@ -0,0 +1,445 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import toast from "react-hot-toast";
+import { ArrowLeft } from "lucide-react";
+import { getProviderVenueById } from "../../services/venueService";
+import { activateAvailability, createAvailability, deactivateAvailability, getVenueAvailability, } from "../../services/availabilityService";
+import ConfirmModal from "../../components/common/ConfirmModal";
+import ErrorState from "../../components/common/ErrorState";
+import CreateAvailabilityCard from "../../components/provider/availability/CreateAvailabilityCard";
+import VenueAvailabilityHeader from "../../components/provider/availability/VenueAvailabilityHeader";
+import AvailabilityListGrouped from "../../components/provider/availability/AvailabilityListGrouped";
+import AvailabilitySkeleton from "../../components/provider/availability/AvailabilitySkeleton";
+import AvailabilitySummary from "../../components/provider/availability/AvailabilitySummary";
+import {
+ areAllSlotsExpiredForToday, filterNonExpiredPresetIds, getPredefinedSlotById, getTodayDateInputValue,
+ groupSlotsByDate, getProviderAvailabilityEmptyState, getAvailabilityStats, resolveSlotSelection,
+} from "../../utils/predefinedSlots";
+
+const ManageAvailability = () => {
+ const { id: venueId } = useParams();
+ const navigate = useNavigate();
+
+ const [venue, setVenue] = useState(null);
+ const [slots, setSlots] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [slotsLoading, setSlotsLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [slotsError, setSlotsError] = useState("");
+ const [selectedDate, setSelectedDate] = useState("");
+ const [selectedSlotIds, setSelectedSlotIds] = useState([]);
+ const [dateError, setDateError] = useState("");
+ const [selectionError, setSelectionError] = useState("");
+ const [submitError, setSubmitError] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [processingSlotId, setProcessingSlotId] = useState(null);
+ const [deactivateTarget, setDeactivateTarget] = useState(null);
+
+ const fetchVenue = useCallback(async () => {
+ if (!venueId) {
+ setError("Invalid venue link.");
+ setVenue(null);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getProviderVenueById(venueId);
+
+ if (!data.success) {
+ setVenue(null);
+ setError(data.message || "Venue not found.");
+ return;
+ }
+
+ setVenue(data.data);
+ } catch (err) {
+ setVenue(null);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venue details. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, [venueId]);
+
+ const fetchSlots = useCallback(async () => {
+ if (!venueId) return;
+
+ try {
+ setSlotsLoading(true);
+ setSlotsError("");
+
+ const data = await getVenueAvailability(venueId);
+
+ if (data.success) {
+ setSlots(Array.isArray(data.data) ? data.data : []);
+ } else {
+ setSlots([]);
+ setSlotsError(data.message || "Failed to load availability.");
+ }
+ } catch (err) {
+ setSlots([]);
+ setSlotsError(
+ err.response?.data?.message ||
+ "Unable to load availability. Please try again."
+ );
+ } finally {
+ setSlotsLoading(false);
+ }
+ }, [venueId]);
+
+ useEffect(() => {
+ fetchVenue();
+ fetchSlots();
+ }, [fetchVenue, fetchSlots]);
+
+ useEffect(() => {
+ const intervalId = window.setInterval(() => {
+ setSelectedSlotIds((prev) => {
+ const next = filterNonExpiredPresetIds(selectedDate, prev);
+ return next.length === prev.length ? prev : next;
+ });
+ }, 60_000);
+
+ return () => window.clearInterval(intervalId);
+ }, [selectedDate]);
+
+ useEffect(() => {
+ setSelectedSlotIds((prev) => filterNonExpiredPresetIds(selectedDate, prev));
+ }, [selectedDate]);
+
+ const groupedSlots = useMemo(() => groupSlotsByDate(slots), [slots]);
+
+ const handleDateChange = (event) => {
+ setSelectedDate(event.target.value);
+ setSelectedSlotIds([]);
+ setDateError("");
+ setSelectionError("");
+ setSubmitError("");
+ };
+
+ const handleToggleSlot = (slotId) => {
+ const preset = getPredefinedSlotById(slotId);
+ if (preset && areAllSlotsExpiredForToday(selectedDate)) {
+ setSelectionError("No remaining slots available for today.");
+ return;
+ }
+
+ setSelectedSlotIds((prev) => resolveSlotSelection(prev, slotId));
+ setSelectionError("");
+ setSubmitError("");
+ };
+
+ const handleCreateAvailability = async (event) => {
+ event.preventDefault();
+
+ const minDate = getTodayDateInputValue();
+ if (!selectedDate) {
+ setDateError("Date is required.");
+ return;
+ }
+
+ if (selectedDate < minDate) {
+ setDateError("Past dates are not allowed.");
+ return;
+ }
+
+ if (selectedDate < minDate) {
+ setDateError("Past dates are not allowed.");
+ return;
+ }
+
+ if (areAllSlotsExpiredForToday(selectedDate)) {
+ setSelectionError("No remaining slots available for today. Choose tomorrow or a later date.");
+ return;
+ }
+
+ const validSlotIds = filterNonExpiredPresetIds(selectedDate, selectedSlotIds);
+
+ if (validSlotIds.length === 0) {
+ setSelectionError(
+ areAllSlotsExpiredForToday(selectedDate)
+ ? "No remaining slots available for today. Choose tomorrow or a later date."
+ : "Select at least one slot."
+ );
+ return;
+ }
+
+ if (validSlotIds.length !== selectedSlotIds.length) {
+ setSelectedSlotIds(validSlotIds);
+ setSelectionError(
+ "One or more selected slots have already ended for today. Please choose a different slot."
+ );
+ return;
+ }
+
+ const presets = validSlotIds
+ .map((id) => getPredefinedSlotById(id))
+ .filter(Boolean);
+
+ try {
+ setIsSubmitting(true);
+ setSubmitError("");
+ setDateError("");
+ setSelectionError("");
+
+ const results = await Promise.allSettled(
+ presets.map((preset) =>
+ createAvailability({
+ venueId,
+ date: selectedDate,
+ slotLabel: preset.apiLabel,
+ startTime: preset.startTime,
+ endTime: preset.endTime,
+ })
+ )
+ );
+
+ const failures = results.filter(
+ (result) =>
+ result.status === "rejected" ||
+ (result.status === "fulfilled" && !result.value?.success)
+ );
+
+ if (failures.length === results.length) {
+ const firstFailure = failures[0];
+ const message =
+ firstFailure.status === "rejected"
+ ? firstFailure.reason?.response?.data?.message ||
+ firstFailure.reason?.message
+ : firstFailure.value?.message;
+ throw new Error(message || "Failed to add availability.");
+ }
+
+ await fetchSlots();
+ setSelectedSlotIds([]);
+
+ if (failures.length > 0) {
+ toast.error("Some slots could not be added. Please review and try again.");
+ } else {
+ toast.success("Availability added successfully.");
+ }
+ } catch (err) {
+ const message =
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to add availability.";
+ setSubmitError(message);
+ toast.error(message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleActivate = async (slot) => {
+ if (processingSlotId) return;
+
+ try {
+ setProcessingSlotId(slot._id);
+
+ const data = await activateAvailability(slot._id);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to activate slot.");
+ }
+
+ setSlots((prev) =>
+ prev.map((item) =>
+ item._id === slot._id
+ ? { ...item, isActive: data.data?.isActive ?? true }
+ : item
+ )
+ );
+
+ toast.success(data.message || "Slot activated successfully.");
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to activate slot."
+ );
+ } finally {
+ setProcessingSlotId(null);
+ }
+ };
+
+ const handleConfirmDeactivate = async () => {
+ if (!deactivateTarget || processingSlotId) return;
+
+ const slot = deactivateTarget;
+
+ try {
+ setProcessingSlotId(slot._id);
+
+ const data = await deactivateAvailability(slot._id);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to deactivate slot.");
+ }
+
+ setSlots((prev) =>
+ prev.map((item) =>
+ item._id === slot._id
+ ? { ...item, isActive: data.data?.isActive ?? false }
+ : item
+ )
+ );
+
+ toast.success(data.message || "Slot deactivated successfully.");
+ setDeactivateTarget(null);
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to deactivate slot."
+ );
+ } finally {
+ setProcessingSlotId(null);
+ }
+ };
+
+ const emptyAvailability = useMemo(
+ () => getProviderAvailabilityEmptyState(slots),
+ [slots]
+ );
+
+ const availabilityStats = useMemo(() => getAvailabilityStats(slots), [slots]);
+
+ const scrollToCreateForm = () => {
+ document.getElementById("create-availability")?.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ if (error || !venue) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Manage Availability
+
+
{venue.title}
+
+
+
+
+
+
+
+
+
+
+ Your slots
+
+
+ Grouped by date — activate or deactivate as needed
+
+
+ {!slotsLoading && !slotsError && slots.length > 0 && (
+
+ {slots.length} total
+
+ )}
+
+
+
+ {!slotsLoading && !slotsError && slots.length > 0 && (
+
+ )}
+
+ {slotsLoading &&
}
+
+ {!slotsLoading && slotsError && (
+
+ )}
+
+ {!slotsLoading && !slotsError && emptyAvailability && (
+
+
+ {emptyAvailability.title}
+
+
+ {emptyAvailability.description}
+
+ {emptyAvailability.showCta && (
+
+ )}
+
+ )}
+
+ {!slotsLoading && !slotsError && groupedSlots.length > 0 && (
+
+ )}
+
+
+
+
setDeactivateTarget(null)}
+ />
+
+ );
+};
+
+export default ManageAvailability;
diff --git a/frontend/src/pages/provider/MyVenues.jsx b/frontend/src/pages/provider/MyVenues.jsx
new file mode 100644
index 0000000000..85289c5156
--- /dev/null
+++ b/frontend/src/pages/provider/MyVenues.jsx
@@ -0,0 +1,227 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import toast from "react-hot-toast";
+import { getMyVenues, activateVenue, deactivateVenue, } from "../../services/venueService";
+import Loader from "../../components/common/Loader";
+import ErrorState from "../../components/common/ErrorState";
+import EmptyState from "../../components/common/EmptyState";
+import { formatBookingPriceDisplay } from "../../utils/formatPrice";
+import { getVenueCoverUrl } from "../../utils/venue";
+
+const formatLocation = (city, state) => {
+ if (city && state) return `${city}, ${state}`;
+ return city || state || "Location not specified";
+};
+
+const ProviderVenueCard = ({
+ venue,
+ onToggleStatus,
+ isToggling,
+}) => {
+ const coverUrl = getVenueCoverUrl(venue);
+
+ return (
+
+
+
+ {coverUrl ? (
+

+ ) : (
+
+ No image
+
+ )}
+
+
+
+
+
+ {venue?.title || "Untitled venue"}
+
+
+
+ {venue.isActive ? "Active" : "Inactive"}
+
+
+
+
+ {formatLocation(venue.city, venue.state)}
+
+
+
+ {formatBookingPriceDisplay(venue.price)}
+
+
+
+
+ Edit Venue
+
+
+
+ Manage Availability
+
+
+
+
+
+
+
+ );
+};
+
+const MyVenues = () => {
+ const [venues, setVenues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [togglingVenueId, setTogglingVenueId] = useState(null);
+
+ const fetchVenues = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getMyVenues();
+
+ if (data.success) {
+ setVenues(data.data ?? []);
+ } else {
+ setVenues([]);
+ setError(data.message || "Failed to load venues.");
+ }
+ } catch (err) {
+ setVenues([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load venues. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchVenues();
+ }, []);
+
+ const handleToggleStatus = async (venue) => {
+ if (togglingVenueId) return;
+
+ const venueId = venue._id;
+ const wasActive = venue.isActive;
+
+ try {
+ setTogglingVenueId(venueId);
+
+ const data = wasActive
+ ? await deactivateVenue(venueId)
+ : await activateVenue(venueId);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to update venue status.");
+ }
+
+ setVenues((prev) =>
+ prev.map((v) =>
+ v._id === venueId
+ ? { ...v, isActive: data.data?.isActive ?? !wasActive }
+ : v
+ )
+ );
+
+ toast.success(data.message || "Venue status updated.");
+ } catch (err) {
+ toast.error(
+ err.response?.data?.message ||
+ err.message ||
+ "Failed to update venue status."
+ );
+ } finally {
+ setTogglingVenueId(null);
+ }
+ };
+
+ return (
+ <>
+
+
+
My Venues
+
+ Manage your listed venues and availability.
+
+
+
+
+ Create Venue
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && venues.length === 0 && (
+
+
+
+
+
+ Create Your First Venue
+
+
+
+ )}
+
+ {!loading && !error && venues.length > 0 && (
+
+ {venues.map((venue) => (
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export default MyVenues;
diff --git a/frontend/src/pages/provider/ProviderBookings.jsx b/frontend/src/pages/provider/ProviderBookings.jsx
new file mode 100644
index 0000000000..4bd5f9e7f2
--- /dev/null
+++ b/frontend/src/pages/provider/ProviderBookings.jsx
@@ -0,0 +1,129 @@
+import { useEffect, useMemo, useState } from "react";
+import { getProviderBookings } from "../../services/bookingService";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import BookingSummary from "../../components/bookings/BookingSummary";
+import BookingFiltersBar from "../../components/bookings/BookingFiltersBar";
+import ProviderBookingRow, { ProviderBookingTableHeader, } from "../../components/provider/bookings/ProviderBookingRow";
+import { filterProviderBookings, getBookingStats, } from "../../utils/providerBookingFilters";
+
+const ProviderBookings = () => {
+ const [bookings, setBookings] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [activeFilter, setActiveFilter] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const fetchBookings = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getProviderBookings();
+
+ if (data.success) {
+ setBookings(Array.isArray(data.data) ? data.data : []);
+ } else {
+ setBookings([]);
+ setError(data.message || "Failed to load bookings.");
+ }
+ } catch (err) {
+ setBookings([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load bookings. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchBookings();
+ }, []);
+
+ const sortedBookings = useMemo(
+ () =>
+ [...bookings].sort(
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
+ ),
+ [bookings]
+ );
+
+ const stats = useMemo(
+ () => getBookingStats(sortedBookings),
+ [sortedBookings]
+ );
+
+ const filteredBookings = useMemo(
+ () =>
+ filterProviderBookings(sortedBookings, {
+ statusFilter: activeFilter,
+ searchQuery,
+ }),
+ [sortedBookings, activeFilter, searchQuery]
+ );
+
+ return (
+ <>
+
+
+ Provider Bookings
+
+
+ All reservations on your venues — tap Call when a customer has added
+ their mobile number.
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && bookings.length === 0 && (
+
+ )}
+
+ {!loading && !error && bookings.length > 0 && (
+
+
+
+
+
+ {filteredBookings.length === 0 ? (
+
+ ) : (
+
+
+
+ {filteredBookings.map((booking) => (
+
+ ))}
+
+
+ )}
+
+ )}
+ >
+ );
+};
+
+export default ProviderBookings;
diff --git a/frontend/src/pages/user/MyBookings.jsx b/frontend/src/pages/user/MyBookings.jsx
new file mode 100644
index 0000000000..3515c4d216
--- /dev/null
+++ b/frontend/src/pages/user/MyBookings.jsx
@@ -0,0 +1,116 @@
+import { useEffect, useMemo, useState } from "react";
+import { getMyBookings } from "../../services/bookingService";
+import BookingCard from "../../components/common/BookingCard";
+import BookingSummary from "../../components/bookings/BookingSummary";
+import BookingFiltersBar from "../../components/bookings/BookingFiltersBar";
+import BookingsEmptyState from "../../components/bookings/BookingsEmptyState";
+import Loader from "../../components/common/Loader";
+import EmptyState from "../../components/common/EmptyState";
+import ErrorState from "../../components/common/ErrorState";
+import { filterBookings, getBookingStats, CUSTOMER_BOOKING_FILTERS, } from "../../utils/bookingFilters";
+
+const MyBookings = () => {
+ const [bookings, setBookings] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [activeFilter, setActiveFilter] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const fetchBookings = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getMyBookings();
+
+ if (data.success) {
+ setBookings(Array.isArray(data.data) ? data.data : []);
+ } else {
+ setBookings([]);
+ setError(data.message || "Failed to load bookings.");
+ }
+ } catch (err) {
+ setBookings([]);
+ setError(
+ err.response?.data?.message ||
+ "Unable to load bookings. Please try again."
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchBookings();
+ }, []);
+
+ const sortedBookings = useMemo(() => {
+ return [...bookings].sort(
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
+ );
+ }, [bookings]);
+
+ const stats = useMemo(() => getBookingStats(sortedBookings), [sortedBookings]);
+
+ const filteredBookings = useMemo(
+ () =>
+ filterBookings(sortedBookings, {
+ statusFilter: activeFilter,
+ searchQuery,
+ }),
+ [sortedBookings, activeFilter, searchQuery]
+ );
+
+ return (
+
+
+
+ My Bookings
+
+
+ View your venue reservations.
+
+
+
+ {loading && }
+
+ {!loading && error && (
+
+ )}
+
+ {!loading && !error && bookings.length === 0 && }
+
+ {!loading && !error && bookings.length > 0 && (
+
+
+
+
+
+ {filteredBookings.length === 0 ? (
+
+ ) : (
+
+ {filteredBookings.map((booking) => (
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+};
+
+export default MyBookings;
diff --git a/frontend/src/pages/user/Profile.jsx b/frontend/src/pages/user/Profile.jsx
new file mode 100644
index 0000000000..5c87e94c4c
--- /dev/null
+++ b/frontend/src/pages/user/Profile.jsx
@@ -0,0 +1,333 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { BadgeCheck, CircleCheck, CircleX, LogOut, Mail, } from "lucide-react";
+import toast from "react-hot-toast";
+import { useAuth } from "../../context/AuthContext";
+import { updateProfile, updateProfileImage } from "../../services/authService";
+import ProfileHeader from "../../components/profile/ProfileHeader";
+import ProfileSection from "../../components/profile/ProfileSection";
+import ProfileField from "../../components/profile/ProfileField";
+import StatusBadge from "../../components/profile/StatusBadge";
+
+const GENDER_OPTIONS = [
+ { value: "male", label: "Male" },
+ { value: "female", label: "Female" },
+ { value: "other", label: "Other" },
+];
+
+const formatGender = (gender) => {
+ if (!gender?.trim()) return null;
+ return gender.charAt(0).toUpperCase() + gender.slice(1);
+};
+
+const formatDateOfBirth = (dob) => {
+ if (!dob) return null;
+
+ const parsed = new Date(dob);
+ if (Number.isNaN(parsed.getTime())) return null;
+
+ return parsed.toLocaleDateString("en-IN", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ });
+};
+
+const toDateInputValue = (dob) => {
+ if (!dob) return "";
+
+ const parsed = new Date(dob);
+ if (Number.isNaN(parsed.getTime())) return "";
+
+ return parsed.toISOString().slice(0, 10);
+};
+
+const buildFormFromUser = (user) => ({
+ name: user?.name ?? "",
+ phone: user?.phone ?? "",
+ gender: user?.gender ?? "other",
+ dob: toDateInputValue(user?.dob),
+ city: user?.city ?? "",
+ state: user?.state ?? "",
+ address: user?.address ?? "",
+ bio: user?.bio ?? "",
+});
+
+const Profile = () => {
+ const navigate = useNavigate();
+ const { user, userRoles, logout, refreshUser } = useAuth();
+ const [isEditing, setIsEditing] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [isUploadingImage, setIsUploadingImage] = useState(false);
+ const [previewImageUrl, setPreviewImageUrl] = useState(null);
+ const [formData, setFormData] = useState(() => buildFormFromUser(user));
+
+ useEffect(() => {
+ return () => {
+ if (previewImageUrl) {
+ URL.revokeObjectURL(previewImageUrl);
+ }
+ };
+ }, [previewImageUrl]);
+
+ const clearPreviewImage = () => {
+ setPreviewImageUrl((current) => {
+ if (current) {
+ URL.revokeObjectURL(current);
+ }
+ return null;
+ });
+ };
+
+ const handleProfileImageSelect = async (file) => {
+ if (!file) return;
+
+ const localPreviewUrl = URL.createObjectURL(file);
+ setPreviewImageUrl((current) => {
+ if (current) {
+ URL.revokeObjectURL(current);
+ }
+ return localPreviewUrl;
+ });
+
+ try {
+ setIsUploadingImage(true);
+
+ const formDataPayload = new FormData();
+ formDataPayload.append("profileImage", file);
+
+ const data = await updateProfileImage(formDataPayload);
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to upload profile image.");
+ }
+
+ await refreshUser();
+ clearPreviewImage();
+ toast.success(data.message || "Profile image updated successfully");
+ } catch (error) {
+ clearPreviewImage();
+ toast.error(
+ error.response?.data?.message ||
+ error.message ||
+ "Unable to upload profile image. Please try again."
+ );
+ } finally {
+ setIsUploadingImage(false);
+ }
+ };
+
+ const scrollToDetails = () => {
+ document.getElementById("profile-details")?.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ };
+
+ const handleStartEdit = () => {
+ setFormData(buildFormFromUser(user));
+ setIsEditing(true);
+ scrollToDetails();
+ };
+
+ const handleCancelEdit = () => {
+ setFormData(buildFormFromUser(user));
+ setIsEditing(false);
+ };
+
+ const handleFieldChange = (event) => {
+ const { name, value } = event.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const handleSave = async () => {
+ try {
+ setIsSaving(true);
+
+ const data = await updateProfile({
+ name: formData.name,
+ phone: formData.phone,
+ gender: formData.gender,
+ dob: formData.dob || null,
+ city: formData.city,
+ state: formData.state,
+ address: formData.address,
+ bio: formData.bio,
+ });
+
+ if (!data.success) {
+ throw new Error(data.message || "Failed to update profile.");
+ }
+
+ await refreshUser();
+ setIsEditing(false);
+ toast.success(data.message || "Profile updated successfully");
+ } catch (error) {
+ toast.error(
+ error.response?.data?.message ||
+ error.message ||
+ "Unable to update profile. Please try again."
+ );
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ await logout();
+ navigate("/");
+ };
+
+ return (
+
+ My Profile
+
+
+
+
+
+
+
+ {isEditing && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {isEditing && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Profile;
diff --git a/frontend/src/services/adminService.js b/frontend/src/services/adminService.js
new file mode 100644
index 0000000000..14082b8953
--- /dev/null
+++ b/frontend/src/services/adminService.js
@@ -0,0 +1,89 @@
+import api from "./axios";
+import { buildAdminQueryParams } from "../utils/adminQuery";
+
+export const getDashboardStats = async () => {
+ const { data } = await api.get("/admin/dashboard/stats");
+ return data;
+};
+
+export const getRecentActivity = async () => {
+ const { data } = await api.get("/admin/dashboard/recent-activity");
+ return data;
+};
+
+export const getAdminUsers = async (params = {}) => {
+ const { data } = await api.get("/admin/users", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
+
+export const getAdminUserById = async (id) => {
+ const { data } = await api.get(`/admin/users/${id}`);
+ return data;
+};
+
+export const activateAdminUser = async (id) => {
+ const { data } = await api.patch(`/admin/users/${id}/activate`);
+ return data;
+};
+
+export const deactivateAdminUser = async (id) => {
+ const { data } = await api.patch(`/admin/users/${id}/deactivate`);
+ return data;
+};
+
+export const getAdminVenues = async (params = {}) => {
+ const { data } = await api.get("/admin/venues", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
+
+export const getAdminVenueById = async (id) => {
+ const { data } = await api.get(`/admin/venues/${id}`);
+ return data;
+};
+
+export const activateAdminVenue = async (id) => {
+ const { data } = await api.patch(`/admin/venues/${id}/activate`);
+ return data;
+};
+
+export const deactivateAdminVenue = async (id) => {
+ const { data } = await api.patch(`/admin/venues/${id}/deactivate`);
+ return data;
+};
+
+export const getAdminBookings = async (params = {}) => {
+ const { data } = await api.get("/admin/bookings", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
+
+export const getAdminBookingById = async (id) => {
+ const { data } = await api.get(`/admin/bookings/${id}`);
+ return data;
+};
+
+export const getAdminPaymentOrders = async (params = {}) => {
+ const { data } = await api.get("/admin/payments/orders", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
+
+export const getAdminPaymentHistory = async (params = {}) => {
+ const { data } = await api.get("/admin/payments/history", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
+
+export const getAdminAbandonedPayments = async (params = {}) => {
+ const { data } = await api.get("/admin/payments/abandoned", {
+ params: buildAdminQueryParams(params),
+ });
+ return data;
+};
diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js
new file mode 100644
index 0000000000..79a3f64cae
--- /dev/null
+++ b/frontend/src/services/authService.js
@@ -0,0 +1,26 @@
+import api from "./axios";
+
+export const getMe = async () => {
+ const { data } = await api.get("/auth/me");
+ return data;
+};
+
+export const logout = async () => {
+ const { data } = await api.post("/auth/logout");
+ return data;
+};
+
+export const becomeProvider = async () => {
+ const { data } = await api.patch("/auth/become-provider");
+ return data;
+};
+
+export const updateProfile = async (profileData) => {
+ const { data } = await api.patch("/auth/me", profileData);
+ return data;
+};
+
+export const updateProfileImage = async (formData) => {
+ const { data } = await api.patch("/auth/me/avatar", formData);
+ return data;
+};
diff --git a/frontend/src/services/availabilityService.js b/frontend/src/services/availabilityService.js
new file mode 100644
index 0000000000..ff0a91c78d
--- /dev/null
+++ b/frontend/src/services/availabilityService.js
@@ -0,0 +1,21 @@
+import api from "./axios";
+
+export const getVenueAvailability = async (venueId) => {
+ const { data } = await api.get(`/availability/${venueId}`);
+ return data;
+};
+
+export const createAvailability = async (payload) => {
+ const { data } = await api.post("/availability/create", payload);
+ return data;
+};
+
+export const deactivateAvailability = async (slotId) => {
+ const { data } = await api.patch(`/availability/deactivate/${slotId}`);
+ return data;
+};
+
+export const activateAvailability = async (slotId) => {
+ const { data } = await api.patch(`/availability/activate/${slotId}`);
+ return data;
+};
diff --git a/frontend/src/services/axios.js b/frontend/src/services/axios.js
new file mode 100644
index 0000000000..411d952250
--- /dev/null
+++ b/frontend/src/services/axios.js
@@ -0,0 +1,8 @@
+import axios from "axios";
+
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL,
+ withCredentials: true,
+});
+
+export default api;
\ No newline at end of file
diff --git a/frontend/src/services/bookingService.js b/frontend/src/services/bookingService.js
new file mode 100644
index 0000000000..afb7bd00c2
--- /dev/null
+++ b/frontend/src/services/bookingService.js
@@ -0,0 +1,11 @@
+import api from "./axios";
+
+export const getMyBookings = async () => {
+ const { data } = await api.get("/bookings/my-bookings");
+ return data;
+};
+
+export const getProviderBookings = async () => {
+ const { data } = await api.get("/bookings/provider-bookings");
+ return data;
+};
diff --git a/frontend/src/services/paymentService.js b/frontend/src/services/paymentService.js
new file mode 100644
index 0000000000..99a88fb547
--- /dev/null
+++ b/frontend/src/services/paymentService.js
@@ -0,0 +1,45 @@
+import api from "./axios";
+import { toPaymentRequestBody } from "../utils/booking";
+
+const hasValue = (value) =>
+ value !== undefined && value !== null && String(value).trim() !== "";
+
+const assertRazorpayResponse = (razorpayResponse) => {
+ const orderId = razorpayResponse?.razorpay_order_id;
+ const paymentId = razorpayResponse?.razorpay_payment_id;
+ const signature = razorpayResponse?.razorpay_signature;
+
+ if (!hasValue(orderId) || !hasValue(paymentId) || !hasValue(signature)) {
+ throw new Error("Incomplete payment response. Please try again.");
+ }
+
+ return {
+ razorpay_order_id: orderId,
+ razorpay_payment_id: paymentId,
+ razorpay_signature: signature,
+ };
+};
+
+export const createOrder = async (bookingPayload) => {
+ const { venueId, availabilityId } = toPaymentRequestBody(bookingPayload);
+
+ const { data } = await api.post("/payments/create-order", {
+ venueId,
+ availabilityId,
+ });
+
+ return data;
+};
+
+export const verifyPayment = async (bookingPayload, razorpayResponse) => {
+ const { venueId, availabilityId } = toPaymentRequestBody(bookingPayload);
+ const paymentFields = assertRazorpayResponse(razorpayResponse);
+
+ const { data } = await api.post("/payments/verify-payment", {
+ ...paymentFields,
+ venueId,
+ availabilityId,
+ });
+
+ return data;
+};
diff --git a/frontend/src/services/venueService.js b/frontend/src/services/venueService.js
new file mode 100644
index 0000000000..5e60a1eba1
--- /dev/null
+++ b/frontend/src/services/venueService.js
@@ -0,0 +1,41 @@
+import api from "./axios";
+
+export const getAllVenues = async () => {
+ const { data } = await api.get("/venues");
+ return data;
+};
+
+export const getPublicVenueById = async (venueId) => {
+ const { data } = await api.get(`/venues/${venueId}`);
+ return data;
+};
+
+export const getMyVenues = async () => {
+ const { data } = await api.get("/venues/my-venues");
+ return data;
+};
+
+export const activateVenue = async (venueId) => {
+ const { data } = await api.patch(`/venues/activate/${venueId}`);
+ return data;
+};
+
+export const deactivateVenue = async (venueId) => {
+ const { data } = await api.patch(`/venues/deactivate/${venueId}`);
+ return data;
+};
+
+export const createVenue = async (formData) => {
+ const { data } = await api.post("/venues/create", formData);
+ return data;
+};
+
+export const getProviderVenueById = async (venueId) => {
+ const { data } = await api.get(`/venues/provider/${venueId}`);
+ return data;
+};
+
+export const updateVenue = async (venueId, formData) => {
+ const { data } = await api.put(`/venues/update/${venueId}`, formData);
+ return data;
+};
diff --git a/frontend/src/utils/adminFormat.js b/frontend/src/utils/adminFormat.js
new file mode 100644
index 0000000000..30dbf718f5
--- /dev/null
+++ b/frontend/src/utils/adminFormat.js
@@ -0,0 +1,8 @@
+export const formatStatusLabel = (status) =>
+ status ? status.charAt(0).toUpperCase() + status.slice(1) : "—";
+
+export const formatRoleLabel = (role) =>
+ role ? role.charAt(0).toUpperCase() + role.slice(1) : "—";
+
+export const formatRoles = (roles = []) =>
+ roles.map(formatRoleLabel).join(", ");
diff --git a/frontend/src/utils/adminQuery.js b/frontend/src/utils/adminQuery.js
new file mode 100644
index 0000000000..254386f8a1
--- /dev/null
+++ b/frontend/src/utils/adminQuery.js
@@ -0,0 +1,11 @@
+export const buildAdminQueryParams = (params = {}) => {
+ const query = {};
+
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null && value !== "") {
+ query[key] = value;
+ }
+ });
+
+ return query;
+};
diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js
new file mode 100644
index 0000000000..7337d11ae2
--- /dev/null
+++ b/frontend/src/utils/auth.js
@@ -0,0 +1,19 @@
+export const getUserRoles = (user) =>
+ Array.isArray(user?.roles) ? user.roles : [];
+
+export const resolveEffectiveRoles = (roleOverride, user) => {
+ if (Array.isArray(roleOverride)) return roleOverride;
+ return getUserRoles(user);
+};
+
+export const isUserProvider = (user) =>
+ getUserRoles(user).includes("provider");
+
+export const isUserAdmin = (user) =>
+ getUserRoles(user).includes("admin");
+
+export const hasProviderRole = (roleOverride, user) =>
+ resolveEffectiveRoles(roleOverride, user).includes("provider");
+
+export const hasAdminRole = (roleOverride, user) =>
+ resolveEffectiveRoles(roleOverride, user).includes("admin");
diff --git a/frontend/src/utils/booking.js b/frontend/src/utils/booking.js
new file mode 100644
index 0000000000..cea014df29
--- /dev/null
+++ b/frontend/src/utils/booking.js
@@ -0,0 +1,81 @@
+export const resolvePopulatedRef = (ref) =>
+ ref && typeof ref === "object" && !Array.isArray(ref) ? ref : null;
+
+const hasValue = (value) =>
+ value !== undefined && value !== null && String(value).trim() !== "";
+
+/**
+ * Builds a client-side booking snapshot from loaded venue + selected slot.
+ * Returns null when required data is missing or invalid.
+ */
+export const buildBookingPayload = (venue, selectedSlot) => {
+ if (!venue || !selectedSlot) return null;
+
+ const venueId = venue._id;
+ const slotId = selectedSlot._id;
+ const price = Number(venue.price);
+
+ if (!hasValue(venueId) || !hasValue(slotId)) return null;
+ if (!Number.isFinite(price) || price < 0) return null;
+
+ return {
+ venueId,
+ slotId,
+ date: selectedSlot.date ?? null,
+ startTime: selectedSlot.startTime ?? null,
+ endTime: selectedSlot.endTime ?? null,
+ price,
+ };
+};
+
+/**
+ * Validates a booking payload before it reaches the payment layer.
+ */
+export const validateBookingPayload = (payload) => {
+ if (!payload) {
+ return { valid: false, error: "Please select a slot to continue." };
+ }
+
+ if (!hasValue(payload.venueId)) {
+ return {
+ valid: false,
+ error: "Venue information is missing. Please refresh and try again.",
+ };
+ }
+
+ if (!hasValue(payload.slotId)) {
+ return {
+ valid: false,
+ error: "Please select a valid time slot.",
+ };
+ }
+
+ const price = Number(payload.price);
+
+ if (!Number.isFinite(price) || price < 0) {
+ return {
+ valid: false,
+ error:
+ "This venue has invalid pricing and cannot be booked right now.",
+ };
+ }
+
+ return { valid: true, error: "" };
+};
+
+/**
+ * Maps booking payload to create-order / verify-payment body shape.
+ * Throws if payload is invalid — callers should validate first for UI errors.
+ */
+export const toPaymentRequestBody = (payload) => {
+ const validation = validateBookingPayload(payload);
+
+ if (!validation.valid) {
+ throw new Error(validation.error);
+ }
+
+ return {
+ venueId: payload.venueId,
+ availabilityId: payload.slotId,
+ };
+};
diff --git a/frontend/src/utils/bookingFilters.js b/frontend/src/utils/bookingFilters.js
new file mode 100644
index 0000000000..41d76bfb77
--- /dev/null
+++ b/frontend/src/utils/bookingFilters.js
@@ -0,0 +1,107 @@
+import { resolvePopulatedRef } from "./booking";
+import { isPastDate } from "./formatDate";
+
+export const BOOKING_FILTERS = [
+ { id: "all", label: "All" },
+ { id: "upcoming", label: "Upcoming" },
+ { id: "completed", label: "Completed" },
+ { id: "cancelled", label: "Cancelled" },
+];
+
+export const CUSTOMER_BOOKING_FILTERS = [
+ { id: "all", label: "All" },
+ { id: "upcoming", label: "Upcoming" },
+ { id: "completed", label: "Completed" },
+];
+
+const parseTimeOnDate = (date, time) => {
+ if (!date || !time) return null;
+
+ const base = new Date(date);
+ if (Number.isNaN(base.getTime())) return null;
+
+ const match = String(time).trim().match(/^(\d{1,2}):(\d{2})\s*(am|pm)?$/i);
+
+ if (match) {
+ let hours = Number(match[1]);
+ const minutes = Number(match[2]);
+ const meridiem = match[3]?.toLowerCase();
+
+ if (meridiem === "pm" && hours < 12) hours += 12;
+ if (meridiem === "am" && hours === 12) hours = 0;
+
+ base.setHours(hours, minutes, 0, 0);
+ return base;
+ }
+
+ base.setHours(23, 59, 59, 999);
+ return base;
+};
+
+export const getBookingCategory = (booking) => {
+ if (booking?.bookingStatus === "cancelled") return "cancelled";
+
+ const slot = resolvePopulatedRef(booking?.availabilityId);
+
+ if (!slot?.date) return "upcoming";
+
+ const endAt = parseTimeOnDate(slot.date, slot.endTime);
+ const compareAt = endAt ?? new Date(slot.date);
+
+ if (endAt) {
+ return compareAt < new Date() ? "completed" : "upcoming";
+ }
+
+ return isPastDate(slot.date) ? "completed" : "upcoming";
+};
+
+export const getBookingStats = (bookings) => {
+ const stats = {
+ total: bookings.length,
+ upcoming: 0,
+ completed: 0,
+ cancelled: 0,
+ };
+
+ bookings.forEach((booking) => {
+ const category = getBookingCategory(booking);
+ stats[category] += 1;
+ });
+
+ return stats;
+};
+
+export const getVenueTitle = (booking) => {
+ const venue = resolvePopulatedRef(booking?.venueId);
+ return venue?.title?.trim() || "";
+};
+
+export const filterBookings = (bookings, { statusFilter = "all", searchQuery = "" } = {}) => {
+ const query = searchQuery.trim().toLowerCase();
+
+ return bookings.filter((booking) => {
+ const category = getBookingCategory(booking);
+
+ if (statusFilter !== "all" && category !== statusFilter) {
+ return false;
+ }
+
+ if (!query) return true;
+
+ return getVenueTitle(booking).toLowerCase().includes(query);
+ });
+};
+
+export const getBookingDisplayStatus = (booking) => {
+ const category = getBookingCategory(booking);
+
+ if (category === "cancelled") {
+ return { label: "Cancelled", tone: "cancelled" };
+ }
+
+ if (category === "completed") {
+ return { label: "Completed", tone: "completed" };
+ }
+
+ return { label: "Upcoming", tone: "upcoming" };
+};
diff --git a/frontend/src/utils/customerSlots.js b/frontend/src/utils/customerSlots.js
new file mode 100644
index 0000000000..d08498d3aa
--- /dev/null
+++ b/frontend/src/utils/customerSlots.js
@@ -0,0 +1,110 @@
+import { isPastDate, isSlotExpired } from "./formatDate";
+import { getDisplayLabelForSlot } from "./predefinedSlots";
+
+export const isCustomerBookableSlot = (slot) =>
+ Boolean(slot?.isActive) &&
+ !slot?.isBooked &&
+ !isPastDate(slot.date) &&
+ !isSlotExpired(slot);
+
+export const filterCustomerBookableSlots = (slots) =>
+ (slots ?? []).filter(isCustomerBookableSlot);
+
+export const getCustomerAvailabilityEmptyState = (slots) => {
+ const list = slots ?? [];
+
+ if (list.length === 0) {
+ return {
+ title: "No availability yet",
+ description:
+ "This venue has not published any booking slots yet. Please check back later.",
+ };
+ }
+
+ const futureSlots = list.filter((slot) => !isPastDate(slot.date));
+
+ if (futureSlots.length === 0) {
+ return {
+ title: "No upcoming slots",
+ description:
+ "All listed slots are in the past. New dates may be added soon.",
+ };
+ }
+
+ const openSlots = futureSlots.filter(
+ (slot) => slot.isActive && !slot.isBooked && !isSlotExpired(slot)
+ );
+
+ if (openSlots.length === 0) {
+ const allBooked = futureSlots.every((slot) => slot.isBooked);
+ if (allBooked) {
+ return {
+ title: "Fully booked",
+ description:
+ "All upcoming slots are already reserved. Try another date or venue.",
+ };
+ }
+
+ return {
+ title: "No bookable slots",
+ description:
+ "Upcoming slots are inactive or unavailable right now. Please check back later.",
+ };
+ }
+
+ return null;
+};
+
+export const formatSlotSummary = (slot) => {
+ if (!slot) return null;
+
+ return {
+ date: slot.date,
+ label: getDisplayLabelForSlot(slot),
+ timeRange: [slot.startTime, slot.endTime].filter(Boolean).join(" – "),
+ slotId: slot._id,
+ };
+};
+
+export const findSlotById = (slots, slotId) =>
+ (slots ?? []).find((slot) => slot._id === slotId);
+
+export const isSlotStillBookable = (slots, selectedSlot) => {
+ if (!selectedSlot?._id) return false;
+ const current = findSlotById(slots, selectedSlot._id);
+ return current && isCustomerBookableSlot(current);
+};
+
+const BOOKING_CONTEXT_KEY = "bmv_booking_context";
+
+export const saveBookingContext = (venueId, slotId) => {
+ if (!venueId || !slotId) return;
+ try {
+ sessionStorage.setItem(
+ BOOKING_CONTEXT_KEY,
+ JSON.stringify({ venueId, slotId, savedAt: Date.now() })
+ );
+ } catch {
+ // Ignore storage errors
+ }
+};
+
+export const loadBookingContext = (venueId) => {
+ try {
+ const raw = sessionStorage.getItem(BOOKING_CONTEXT_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ if (parsed?.venueId !== venueId) return null;
+ return parsed;
+ } catch {
+ return null;
+ }
+};
+
+export const clearBookingContext = () => {
+ try {
+ sessionStorage.removeItem(BOOKING_CONTEXT_KEY);
+ } catch {
+ // Ignore
+ }
+};
diff --git a/frontend/src/utils/formatDate.js b/frontend/src/utils/formatDate.js
new file mode 100644
index 0000000000..3926af58eb
--- /dev/null
+++ b/frontend/src/utils/formatDate.js
@@ -0,0 +1,136 @@
+export const formatSlotDate = (date) => {
+ if (!date) return "";
+
+ return new Date(date).toLocaleDateString("en-IN", {
+ weekday: "long",
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ });
+};
+
+export const formatSlotDateCompact = (date) => {
+ if (!date) return "";
+
+ return new Date(date).toLocaleDateString("en-IN", {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ });
+};
+
+export const formatTimeRange = (startTime, endTime) => {
+ if (!startTime && !endTime) return "";
+ if (!endTime) return startTime;
+ if (!startTime) return endTime;
+ return `${startTime} – ${endTime}`;
+};
+
+export const formatSlotLabel = (label) => {
+ if (!label) return "";
+
+ const labels = {
+ morning: "Morning",
+ evening: "Evening",
+ night: "Night",
+ fullday: "Full Day",
+ };
+
+ return labels[label] || label;
+};
+
+export const toDateKey = (date) => {
+ if (!date) return "";
+
+ if (typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
+ return date;
+ }
+
+ return new Date(date).toISOString().split("T")[0];
+};
+
+export const getTodayDateKey = () => {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, "0");
+ const day = String(today.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+};
+
+export const parseLocalCalendarDate = (date) => {
+ if (!date) return null;
+
+ if (typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
+ const [year, month, day] = date.split("-").map(Number);
+ return new Date(year, month - 1, day);
+ }
+
+ const parsed = new Date(date);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+};
+
+export const isTodayCalendarDate = (date) => {
+ const parsed = parseLocalCalendarDate(date);
+ if (!parsed) return false;
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ parsed.setHours(0, 0, 0, 0);
+ return parsed.getTime() === today.getTime();
+};
+
+export const isPastDate = (date) => {
+ const slotDate = new Date(date);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ slotDate.setHours(0, 0, 0, 0);
+ return slotDate < today;
+};
+
+const parseTimeOnDate = (date, time) => {
+ if (!date || !time) return null;
+
+ const base = parseLocalCalendarDate(date) ?? new Date(date);
+ if (Number.isNaN(base.getTime())) return null;
+
+ base.setHours(0, 0, 0, 0);
+
+ const match = String(time).trim().match(/^(\d{1,2}):(\d{2})\s*(AM|PM|am|pm)?$/);
+
+ if (match) {
+ let hours = Number(match[1]);
+ const minutes = Number(match[2]);
+ const meridiem = match[3]?.toLowerCase();
+
+ if (meridiem === "pm" && hours < 12) hours += 12;
+ if (meridiem === "am" && hours === 12) hours = 0;
+
+ base.setHours(hours, minutes, 0, 0);
+ return base;
+ }
+
+ return null;
+};
+
+/** True when end time on the given calendar day has already passed (today only). */
+export const isEndTimePassedForDate = (date, endTime) => {
+ if (!isTodayCalendarDate(date)) return false;
+
+ const endAt = parseTimeOnDate(date, endTime);
+ if (!endAt) return false;
+
+ return endAt < new Date();
+};
+
+/** True when slot end time on its calendar day has passed. */
+export const isSlotExpired = (slot) => {
+ if (!slot?.date) return false;
+
+ const endAt = parseTimeOnDate(slot.date, slot.endTime);
+ if (!endAt) {
+ return isPastDate(slot.date);
+ }
+
+ return endAt < new Date();
+};
diff --git a/frontend/src/utils/formatPrice.js b/frontend/src/utils/formatPrice.js
new file mode 100644
index 0000000000..cbcb2826d5
--- /dev/null
+++ b/frontend/src/utils/formatPrice.js
@@ -0,0 +1,21 @@
+const isValidPrice = (price) =>
+ price != null && price !== "" && !Number.isNaN(Number(price));
+
+export const formatPrice = (price) => {
+ if (!isValidPrice(price)) {
+ return { amount: "N/A" };
+ }
+
+ return { amount: `₹${Number(price).toLocaleString("en-IN")}` };
+};
+
+/** Customer-facing and booking-path price label (slot-based backend). */
+export const formatBookingPriceDisplay = (price) => {
+ const { amount } = formatPrice(price);
+ if (amount === "N/A") return "N/A";
+ return `${amount} per slot`;
+};
+
+/** @deprecated Use formatBookingPriceDisplay for marketplace display */
+export const formatPriceDisplay = (price, _pricingUnit) =>
+ formatBookingPriceDisplay(price);
diff --git a/frontend/src/utils/geocode.js b/frontend/src/utils/geocode.js
new file mode 100644
index 0000000000..d75aa59879
--- /dev/null
+++ b/frontend/src/utils/geocode.js
@@ -0,0 +1,92 @@
+import { buildGeocodeQuery } from "./venueLocation";
+
+const NOMINATIM_ENDPOINT = "https://nominatim.openstreetmap.org/search";
+
+export const GEOCODE_ERROR = {
+ MISSING_REQUIRED: "MISSING_REQUIRED",
+ NOT_FOUND: "NOT_FOUND",
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
+ NETWORK: "NETWORK",
+};
+
+export class GeocodeError extends Error {
+ constructor(code) {
+ super(code);
+ this.name = "GeocodeError";
+ this.code = code;
+ }
+};
+
+export const hasRequiredGeocodeFields = (form) =>
+ Boolean(
+ form?.address?.trim() && form?.city?.trim() && form?.state?.trim()
+ );
+
+export const getGeocodeUserMessage = (code) => {
+ switch (code) {
+ case GEOCODE_ERROR.MISSING_REQUIRED:
+ return "Please enter Address, City, and State before detecting the location.";
+ case GEOCODE_ERROR.NOT_FOUND:
+ return "Couldn't find this location. Please check the address and try again.";
+ case GEOCODE_ERROR.SERVICE_UNAVAILABLE:
+ case GEOCODE_ERROR.NETWORK:
+ return "Unable to connect to the location service. Please try again later.";
+ default:
+ return "Couldn't find this location. Please check the address and try again.";
+ }
+};
+
+/**
+ * Geocode a venue address using OpenStreetMap Nominatim.
+ * Returns { latitude, longitude } or null when no match is found.
+ */
+export const geocodeVenueAddress = async (form) => {
+ if (!hasRequiredGeocodeFields(form)) {
+ throw new GeocodeError(GEOCODE_ERROR.MISSING_REQUIRED);
+ }
+
+ const query = buildGeocodeQuery(form);
+
+ let response;
+
+ try {
+ const url = new URL(NOMINATIM_ENDPOINT);
+ url.searchParams.set("q", query);
+ url.searchParams.set("format", "json");
+ url.searchParams.set("limit", "1");
+ url.searchParams.set("countrycodes", "in");
+
+ response = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/json",
+ },
+ });
+ } catch {
+ throw new GeocodeError(GEOCODE_ERROR.NETWORK);
+ }
+
+ if (!response.ok) {
+ throw new GeocodeError(GEOCODE_ERROR.SERVICE_UNAVAILABLE);
+ }
+
+ let results;
+
+ try {
+ results = await response.json();
+ } catch {
+ throw new GeocodeError(GEOCODE_ERROR.SERVICE_UNAVAILABLE);
+ }
+
+ if (!Array.isArray(results) || results.length === 0) {
+ return null;
+ }
+
+ const latitude = Number.parseFloat(results[0].lat);
+ const longitude = Number.parseFloat(results[0].lon);
+
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
+ return null;
+ }
+
+ return { latitude, longitude };
+};
diff --git a/frontend/src/utils/predefinedSlots.js b/frontend/src/utils/predefinedSlots.js
new file mode 100644
index 0000000000..360f3d47b6
--- /dev/null
+++ b/frontend/src/utils/predefinedSlots.js
@@ -0,0 +1,271 @@
+import { getTodayDateKey, isEndTimePassedForDate, toDateKey } from "./formatDate";
+
+export const PARTIAL_SLOT_IDS = ["morning", "evening", "night"];
+export const FULL_DAY_SLOT_ID = "fullday";
+
+export const PREDEFINED_SLOTS = [
+ {
+ id: "morning",
+ apiLabel: "morning",
+ label: "Morning",
+ startTime: "09:00 AM",
+ endTime: "12:00 PM",
+ isFullDay: false,
+ },
+ {
+ id: "evening",
+ apiLabel: "evening",
+ label: "Evening",
+ startTime: "03:00 PM",
+ endTime: "06:00 PM",
+ isFullDay: false,
+ },
+ {
+ id: "night",
+ apiLabel: "night",
+ label: "Night",
+ startTime: "06:00 PM",
+ endTime: "10:00 PM",
+ isFullDay: false,
+ },
+ {
+ id: "fullday",
+ apiLabel: "fullday",
+ label: "Full Day",
+ startTime: "09:00 AM",
+ endTime: "10:00 PM",
+ isFullDay: true,
+ },
+];
+
+export const SLOT_RULES_HELPER =
+ "Full Day covers the entire day and cannot be combined with Morning, Evening, or Night on the same date.";
+
+export const getPredefinedSlotById = (id) =>
+ PREDEFINED_SLOTS.find((slot) => slot.id === id);
+
+export const getDisplayLabelForSlot = (slot) => {
+ if (!slot) return "";
+
+ const match = PREDEFINED_SLOTS.find(
+ (preset) =>
+ preset.apiLabel === slot.slotLabel &&
+ preset.startTime === slot.startTime &&
+ preset.endTime === slot.endTime
+ );
+
+ if (match) return match.label;
+
+ const fallback = {
+ morning: "Morning",
+ evening: "Evening",
+ night: "Night",
+ fullday: "Full Day",
+ };
+
+ return fallback[slot.slotLabel] || slot.slotLabel;
+};
+
+export const slotsForDate = (slots, date) => {
+ const key = toDateKey(date);
+ return slots.filter((slot) => toDateKey(slot.date) === key);
+};
+
+export const slotMatchesPreset = (slot, preset) =>
+ slot.slotLabel === preset.apiLabel &&
+ slot.startTime === preset.startTime &&
+ slot.endTime === preset.endTime;
+
+export const findExistingPresetSlot = (slots, date, preset) =>
+ slotsForDate(slots, date).find((slot) => slotMatchesPreset(slot, preset));
+
+export const getSlotOptionState = (slots, date, preset) => {
+ if (!date) {
+ return { hidden: false, disabled: true, reason: "existing", existing: false };
+ }
+
+ const existing = findExistingPresetSlot(slots, date, preset);
+ if (existing) {
+ return { hidden: false, disabled: true, reason: "existing", existing: true };
+ }
+
+ if (isPresetExpiredForDate(date, preset)) {
+ return { hidden: true, disabled: true, reason: "expired", existing: false };
+ }
+
+ const dateSlots = slotsForDate(slots, date);
+ const fullDayExists = dateSlots.some((slot) => slot.slotLabel === "fullday");
+ const partialExists = dateSlots.some((slot) => slot.slotLabel !== "fullday");
+
+ if (preset.isFullDay) {
+ if (partialExists) {
+ return { hidden: true, disabled: true, reason: "conflict", existing: false };
+ }
+ return { hidden: false, disabled: false, reason: null, existing: false };
+ }
+
+ if (fullDayExists) {
+ return { hidden: true, disabled: true, reason: "conflict", existing: false };
+ }
+
+ const labelTaken = dateSlots.some((slot) => slot.slotLabel === preset.apiLabel);
+ if (labelTaken) {
+ return { hidden: true, disabled: true, reason: "conflict", existing: false };
+ }
+
+ return { hidden: false, disabled: false, reason: null, existing: false };
+};
+
+export const getVisibleSlotOptions = (slots, date, selectedSlotIds = []) =>
+ PREDEFINED_SLOTS.filter((preset) => {
+ const state = getSlotOptionStateWithSelection(
+ slots,
+ date,
+ preset,
+ selectedSlotIds
+ );
+ return state.existing || !state.hidden;
+ });
+
+/** Extends saved-slot rules with in-form checkbox selection conflicts. */
+export const getSlotOptionStateWithSelection = (
+ slots,
+ date,
+ preset,
+ selectedSlotIds = []
+) => {
+ const base = getSlotOptionState(slots, date, preset);
+ if (base.existing || base.hidden || !date) return base;
+
+ const selectedPresets = selectedSlotIds
+ .map((id) => getPredefinedSlotById(id))
+ .filter(Boolean);
+
+ const fullDaySelected = selectedPresets.some((item) => item.isFullDay);
+ const partialSelected = selectedPresets.some((item) => !item.isFullDay);
+ const isSelected = selectedSlotIds.includes(preset.id);
+
+ if (isSelected && isPresetExpiredForDate(date, preset)) {
+ return { hidden: true, disabled: true, reason: "expired", existing: false };
+ }
+
+ if (isSelected) {
+ return { ...base, hidden: false, disabled: false, reason: null };
+ }
+
+ if (preset.isFullDay && partialSelected) {
+ return { hidden: true, disabled: true, reason: "conflict", existing: false };
+ }
+
+ if (!preset.isFullDay && fullDaySelected) {
+ return { hidden: true, disabled: true, reason: "conflict", existing: false };
+ }
+
+ return base;
+};
+
+/** Apply Full Day ↔ partial mutual exclusion to checkbox selection. */
+export const resolveSlotSelection = (selectedIds, toggledId) => {
+ const isRemoving = selectedIds.includes(toggledId);
+ let next = isRemoving
+ ? selectedIds.filter((id) => id !== toggledId)
+ : [...selectedIds, toggledId];
+
+ const hasFullDay = next.includes(FULL_DAY_SLOT_ID);
+ const hasPartial = next.some((id) => PARTIAL_SLOT_IDS.includes(id));
+
+ if (hasFullDay && hasPartial) {
+ if (toggledId === FULL_DAY_SLOT_ID) {
+ next = next.filter((id) => !PARTIAL_SLOT_IDS.includes(id));
+ } else {
+ next = next.filter((id) => id !== FULL_DAY_SLOT_ID);
+ }
+ }
+
+ return next;
+};
+
+export const getTodayDateInputValue = () => getTodayDateKey();
+
+export const isPresetExpiredForDate = (date, preset) =>
+ Boolean(preset) && isEndTimePassedForDate(date, preset.endTime);
+
+export const areAllSlotsExpiredForToday = (date) => {
+ if (date !== getTodayDateKey()) return false;
+ return PREDEFINED_SLOTS.every((preset) => isPresetExpiredForDate(date, preset));
+};
+
+export const filterNonExpiredPresetIds = (date, presetIds) =>
+ presetIds.filter((id) => {
+ const preset = getPredefinedSlotById(id);
+ return preset && !isPresetExpiredForDate(date, preset);
+ });
+
+export const getCreatablePresetsForDate = (slots, date, selectedSlotIds = []) =>
+ PREDEFINED_SLOTS.filter((preset) => {
+ const state = getSlotOptionStateWithSelection(
+ slots,
+ date,
+ preset,
+ selectedSlotIds
+ );
+ return !state.existing && !state.hidden && !state.disabled;
+ });
+
+export const groupSlotsByDate = (slots) => {
+ const groups = {};
+
+ slots.forEach((slot) => {
+ const key = toDateKey(slot.date);
+ if (!groups[key]) {
+ groups[key] = { date: slot.date, slots: [] };
+ }
+ groups[key].slots.push(slot);
+ });
+
+ const sortPresets = PREDEFINED_SLOTS.map((preset) => preset.id);
+
+ Object.values(groups).forEach((group) => {
+ group.slots.sort((a, b) => {
+ const aPreset = PREDEFINED_SLOTS.find((preset) => slotMatchesPreset(a, preset));
+ const bPreset = PREDEFINED_SLOTS.find((preset) => slotMatchesPreset(b, preset));
+ const aIndex = sortPresets.indexOf(aPreset?.id ?? "");
+ const bIndex = sortPresets.indexOf(bPreset?.id ?? "");
+ return aIndex - bIndex;
+ });
+ });
+
+ return Object.values(groups).sort(
+ (a, b) => new Date(a.date) - new Date(b.date)
+ );
+};
+
+export const getSlotStatusLabel = (slot) => {
+ if (slot.isBooked) return "Booked";
+ if (!slot.isActive) return "Inactive";
+ return "Available";
+};
+
+export const getProviderAvailabilityEmptyState = (slots) => {
+ if (slots.length === 0) {
+ return {
+ title: "No availability created yet",
+ description:
+ "Add your first slot so guests can start booking this venue.",
+ showCta: true,
+ };
+ }
+
+ return null;
+};
+
+export const getAvailabilityStats = (slots) => {
+ const list = slots ?? [];
+
+ return {
+ total: list.length,
+ available: list.filter((slot) => slot.isActive && !slot.isBooked).length,
+ booked: list.filter((slot) => slot.isBooked).length,
+ inactive: list.filter((slot) => !slot.isActive && !slot.isBooked).length,
+ };
+};
diff --git a/frontend/src/utils/providerBookingFilters.js b/frontend/src/utils/providerBookingFilters.js
new file mode 100644
index 0000000000..8b9b24eb98
--- /dev/null
+++ b/frontend/src/utils/providerBookingFilters.js
@@ -0,0 +1,38 @@
+import { resolvePopulatedRef } from "./booking";
+import {
+ filterBookings,
+ getBookingStats,
+ getVenueTitle,
+} from "./bookingFilters";
+
+export { getBookingStats };
+
+export const filterProviderBookings = (
+ bookings,
+ { statusFilter = "all", searchQuery = "" } = {}
+) => {
+ const query = searchQuery.trim().toLowerCase();
+
+ if (!query) {
+ return filterBookings(bookings, { statusFilter, searchQuery: "" });
+ }
+
+ return filterBookings(bookings, { statusFilter, searchQuery: "" }).filter(
+ (booking) => {
+ const customer = resolvePopulatedRef(booking?.userId);
+ const haystack = [
+ customer?.name,
+ customer?.email,
+ customer?.phone,
+ getVenueTitle(booking),
+ booking?.bookingReference,
+ booking?._id,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+
+ return haystack.includes(query);
+ }
+ );
+};
diff --git a/frontend/src/utils/shareVenue.js b/frontend/src/utils/shareVenue.js
new file mode 100644
index 0000000000..c8de91d904
--- /dev/null
+++ b/frontend/src/utils/shareVenue.js
@@ -0,0 +1,25 @@
+export const shareVenue = async ({ title, url }) => {
+ const shareData = {
+ title: title || "Book My Venue",
+ text: `Check out ${title || "this venue"} on Book My Venue`,
+ url,
+ };
+
+ if (navigator.share) {
+ try {
+ await navigator.share(shareData);
+ return { method: "share" };
+ } catch (error) {
+ if (error?.name === "AbortError") {
+ return { method: "cancelled" };
+ }
+ }
+ }
+
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(url);
+ return { method: "clipboard" };
+ }
+
+ throw new Error("Sharing is not supported on this device.");
+};
diff --git a/frontend/src/utils/venue.js b/frontend/src/utils/venue.js
new file mode 100644
index 0000000000..31a8d786c4
--- /dev/null
+++ b/frontend/src/utils/venue.js
@@ -0,0 +1,70 @@
+const resolveOwnerRecord = (venue) => {
+ if (!venue) return null;
+
+ if (venue.owner?.name?.trim()) return venue.owner;
+ if (venue.provider?.name?.trim()) return venue.provider;
+
+ const { ownerId } = venue;
+ if (ownerId && typeof ownerId === "object" && ownerId.name?.trim()) {
+ return ownerId;
+ }
+
+ return null;
+};
+
+export const getVenueProvider = (venue) => {
+ const owner = resolveOwnerRecord(venue);
+ if (!owner?.name?.trim()) return null;
+
+ const trustIndicators = [];
+
+ if (owner.isEmailVerified === true || owner.isVerified === true) {
+ trustIndicators.push({ key: "verified", label: "Verified provider" });
+ }
+
+ if (owner.isActive === true) {
+ trustIndicators.push({ key: "active-host", label: "Active host" });
+ }
+
+ return {
+ name: owner.name.trim(),
+ initial: owner.name.trim().charAt(0).toUpperCase(),
+ profileImage: owner.profileImage || null,
+ trustIndicators,
+ };
+};
+
+export const formatVenueLocation = (venue) => {
+ if (!venue) return "";
+
+ const fullAddress = [venue.address, venue.city, venue.state, venue.pincode]
+ .filter(Boolean)
+ .join(", ");
+
+ if (fullAddress) return fullAddress;
+
+ const region = [venue.city, venue.state].filter(Boolean).join(", ");
+ return region ? `${region}, India` : "Location not specified";
+};
+
+export const getVenueCoverUrl = (venue) => {
+ if (!venue) return null;
+ return venue.coverImage?.url || venue.images?.[0]?.url || null;
+};
+
+export const getVenueImages = (venue) => {
+ if (!venue) return [];
+
+ const urls = [];
+
+ const addUrl = (url) => {
+ if (url && !urls.includes(url)) {
+ urls.push(url);
+ }
+ };
+
+ addUrl(venue.coverImage?.url);
+ (venue.images ?? []).forEach((image) => addUrl(image?.url));
+
+ return urls;
+};
diff --git a/frontend/src/utils/venueFilters.js b/frontend/src/utils/venueFilters.js
new file mode 100644
index 0000000000..e41606ec0d
--- /dev/null
+++ b/frontend/src/utils/venueFilters.js
@@ -0,0 +1,150 @@
+export const VENUE_CATEGORY_OPTIONS = [
+ { value: "wedding", label: "Wedding" },
+ { value: "corporate", label: "Corporate" },
+ { value: "birthday", label: "Birthday" },
+ { value: "party", label: "Party" },
+ { value: "function", label: "Function" },
+ { value: "photoshoot", label: "Photoshoot" },
+ { value: "other", label: "Other" },
+];
+
+export const VENUE_CATEGORIES = [
+ { value: "all", label: "All" },
+ ...VENUE_CATEGORY_OPTIONS,
+];
+
+const CATEGORY_LABEL_BY_VALUE = Object.fromEntries(
+ VENUE_CATEGORY_OPTIONS.map(({ value, label }) => [value, label])
+);
+
+const LEGACY_CATEGORY_SLUGS = {
+ meetings: "corporate",
+ meeting: "corporate",
+};
+
+export const normalizeCategorySlug = (value) => {
+ if (value == null || typeof value !== "string") return "";
+
+ const normalized = value.trim().toLowerCase();
+
+ if (!normalized) return "";
+
+ if (CATEGORY_LABEL_BY_VALUE[normalized]) {
+ return normalized;
+ }
+
+ return LEGACY_CATEGORY_SLUGS[normalized] || "";
+};
+
+export const getCategoryLabel = (value) => {
+ const slug = normalizeCategorySlug(value);
+
+ if (slug) {
+ return CATEGORY_LABEL_BY_VALUE[slug];
+ }
+
+ if (typeof value === "string" && value.trim()) {
+ return value.trim();
+ }
+
+ return "General";
+};
+
+export const isValidCategorySlug = (value) =>
+ Boolean(normalizeCategorySlug(value));
+
+export const SORT_OPTIONS = [
+ { value: "price-asc", label: "Price Low to High" },
+ { value: "price-desc", label: "Price High to Low" },
+ { value: "capacity-asc", label: "Capacity Low to High" },
+ { value: "capacity-desc", label: "Capacity High to Low" },
+];
+
+export const DEFAULT_VENUE_FILTERS = {
+ search: "",
+ category: "all",
+ city: "all",
+ minCapacity: "",
+ sort: "price-asc",
+};
+
+const matchesCategory = (venueCategory, filterCategory) => {
+ if (filterCategory === "all") return true;
+ if (!venueCategory) return false;
+
+ const venueSlug =
+ normalizeCategorySlug(venueCategory) || venueCategory.trim().toLowerCase();
+
+ return venueSlug === filterCategory.toLowerCase();
+};
+
+const parseOptionalNumber = (value) => {
+ if (value === "" || value == null) return null;
+ const parsed = Number(value);
+ return Number.isNaN(parsed) ? null : parsed;
+};
+
+export const extractCities = (venues) => {
+ const cities = new Set();
+
+ venues.forEach((venue) => {
+ const city = venue.city?.trim();
+ if (city) cities.add(city);
+ });
+
+ return Array.from(cities).sort((a, b) => a.localeCompare(b));
+};
+
+export const countActiveFilters = (filters) => {
+ let count = 0;
+
+ if (filters.search.trim()) count += 1;
+ if (filters.category !== "all") count += 1;
+ if (filters.city !== "all") count += 1;
+ if (filters.minCapacity !== "") count += 1;
+
+ return count;
+};
+
+const sortVenues = (venues, sort) => {
+ const sorted = [...venues];
+
+ switch (sort) {
+ case "price-desc":
+ return sorted.sort((a, b) => Number(b.price) - Number(a.price));
+ case "capacity-asc":
+ return sorted.sort((a, b) => Number(a.capacity) - Number(b.capacity));
+ case "capacity-desc":
+ return sorted.sort((a, b) => Number(b.capacity) - Number(a.capacity));
+ case "price-asc":
+ default:
+ return sorted.sort((a, b) => Number(a.price) - Number(b.price));
+ }
+};
+
+export const filterAndSortVenues = (venues, filters) => {
+ const query = filters.search.trim().toLowerCase();
+ const minCapacity = parseOptionalNumber(filters.minCapacity);
+
+ const filtered = venues.filter((venue) => {
+ if (query) {
+ const title = venue.title?.toLowerCase() ?? "";
+ if (!title.includes(query)) return false;
+ }
+
+ if (!matchesCategory(venue.category, filters.category)) return false;
+
+ if (filters.city !== "all") {
+ const city = venue.city?.trim().toLowerCase() ?? "";
+ if (city !== filters.city.toLowerCase()) return false;
+ }
+
+ if (minCapacity != null) {
+ if (Number(venue.capacity) < minCapacity) return false;
+ }
+
+ return true;
+ });
+
+ return sortVenues(filtered, filters.sort);
+};
diff --git a/frontend/src/utils/venueForm.js b/frontend/src/utils/venueForm.js
new file mode 100644
index 0000000000..ad498d8b03
--- /dev/null
+++ b/frontend/src/utils/venueForm.js
@@ -0,0 +1,196 @@
+import { isValidCategorySlug, normalizeCategorySlug } from "./venueFilters";
+
+export const MAX_VENUE_IMAGES = 5;
+export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
+
+export const VENUE_FORM_FIELD_ORDER = [
+ "title",
+ "description",
+ "category",
+ "images",
+ "price",
+ "capacity",
+ "address",
+ "city",
+ "state",
+ "pincode",
+ "amenities",
+ "rules",
+];
+
+export const EMPTY_VENUE_FORM = {
+ title: "",
+ description: "",
+ category: "",
+ capacity: "",
+ price: "",
+ city: "",
+ state: "",
+ pincode: "",
+ address: "",
+ latitude: "",
+ longitude: "",
+ amenities: [],
+ rules: [],
+};
+
+export const normalizeStringArray = (value) => {
+ if (Array.isArray(value)) {
+ return value.map((item) => String(item).trim()).filter(Boolean);
+ }
+
+ if (typeof value === "string") {
+ try {
+ const parsed = JSON.parse(value);
+ if (Array.isArray(parsed)) {
+ return parsed.map((item) => String(item).trim()).filter(Boolean);
+ }
+ } catch {
+ // Fall through to comma parsing
+ }
+
+ return parseCommaList(value);
+ }
+
+ return [];
+};
+
+export const mapVenueToFormValues = (venue) => ({
+ title: venue?.title || "",
+ description: venue?.description || "",
+ category: normalizeCategorySlug(venue?.category),
+ capacity: venue?.capacity != null ? String(venue.capacity) : "",
+ price: venue?.price != null ? String(venue.price) : "",
+ city: venue?.city || "",
+ state: venue?.state || "",
+ pincode: venue?.pincode || "",
+ address: venue?.address || "",
+ latitude:
+ venue?.location?.latitude != null && venue?.location?.latitude !== ""
+ ? String(venue.location.latitude)
+ : "",
+ longitude:
+ venue?.location?.longitude != null && venue?.location?.longitude !== ""
+ ? String(venue.location.longitude)
+ : "",
+ amenities: normalizeStringArray(venue?.amenities),
+ rules: normalizeStringArray(venue?.rules),
+});
+
+export const parseCommaList = (value) => {
+ if (value == null || typeof value !== "string") return [];
+ return value
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+};
+
+export const stringifyListField = (value) => {
+ const list = Array.isArray(value)
+ ? value.map((item) => String(item).trim()).filter(Boolean)
+ : parseCommaList(value);
+
+ return JSON.stringify(list);
+};
+
+export const addUniqueChipValue = (items, rawValue) => {
+ const next = normalizeStringArray(items);
+ const candidate = String(rawValue ?? "").trim();
+
+ if (!candidate) return next;
+
+ const exists = next.some(
+ (item) => item.toLowerCase() === candidate.toLowerCase()
+ );
+
+ if (exists) return next;
+
+ return [...next, candidate];
+};
+
+export const scrollToFirstFormError = (errors) => {
+ const firstKey = VENUE_FORM_FIELD_ORDER.find((key) => errors[key]);
+ if (!firstKey) return;
+
+ const field = document.getElementById(`venue-field-${firstKey}`);
+ field?.scrollIntoView({ behavior: "smooth", block: "center" });
+
+ const focusable = field?.querySelector(
+ "input:not([type=file]), textarea, select, [tabindex='0']"
+ );
+
+ focusable?.focus({ preventScroll: true });
+};
+
+export const validateVenueCoreFields = (form) => {
+ const errors = {};
+
+ if (!form?.title?.trim()) errors.title = "Title is required";
+ if (!form?.description?.trim())
+ errors.description = "Description is required";
+ if (!form?.category?.trim()) {
+ errors.category = "Category is required";
+ } else if (!isValidCategorySlug(form.category)) {
+ errors.category = "Please select a valid category";
+ }
+ if (!form?.address?.trim()) errors.address = "Address is required";
+
+ if (!form?.capacity?.toString().trim()) {
+ errors.capacity = "Capacity is required";
+ } else {
+ const capacity = Number(form.capacity);
+ if (
+ !Number.isFinite(capacity) ||
+ capacity < 1 ||
+ !Number.isInteger(capacity)
+ ) {
+ errors.capacity = "Enter a valid capacity (whole number, 1 or more)";
+ }
+ }
+
+ if (!form?.price?.toString().trim()) {
+ errors.price = "Price is required";
+ } else {
+ const price = Number(form.price);
+ if (!Number.isFinite(price) || price <= 0) {
+ errors.price = "Enter a valid price greater than 0";
+ }
+ }
+
+ return errors;
+};
+
+export const validateImageSelection = (
+ files,
+ { maxCount = MAX_VENUE_IMAGES, currentCount = 0 } = {}
+) => {
+ const selected = Array.from(files || []);
+
+ if (currentCount + selected.length > maxCount) {
+ return {
+ valid: false,
+ files: [],
+ message: `You can upload a maximum of ${maxCount} images.`,
+ };
+ }
+
+ for (const file of selected) {
+ if (!file.type?.startsWith("image/")) {
+ return {
+ valid: false,
+ files: [],
+ message: `"${file.name}" is not a valid image file.`,
+ };
+ }
+
+ if (file.size > MAX_IMAGE_SIZE_BYTES) {
+ return {
+ valid: false,
+ files: [],
+ message: `"${file.name}" exceeds the 5MB size limit.`,
+ };
+ }
+ }
+
+ return { valid: true, files: selected, message: "" };
+};
diff --git a/frontend/src/utils/venueLocation.js b/frontend/src/utils/venueLocation.js
new file mode 100644
index 0000000000..b56f7fcde8
--- /dev/null
+++ b/frontend/src/utils/venueLocation.js
@@ -0,0 +1,114 @@
+import { formatVenueLocation } from "./venue";
+
+const isValidCoordinate = (value) => {
+ const num = Number(value);
+ return Number.isFinite(num);
+};
+
+export const getVenueCoordinates = (venue) => {
+ const latitude = venue?.location?.latitude;
+ const longitude = venue?.location?.longitude;
+
+ if (!isValidCoordinate(latitude) || !isValidCoordinate(longitude)) {
+ return null;
+ }
+
+ return {
+ latitude: Number(latitude),
+ longitude: Number(longitude),
+ };
+};
+
+export const hasVenueLocationData = (venue) => {
+ if (getVenueCoordinates(venue)) return true;
+
+ const address = venue?.address?.trim();
+ const city = venue?.city?.trim();
+ const state = venue?.state?.trim();
+
+ return Boolean(address || city || state);
+};
+
+export const getVenueCityStateLabel = (venue) => {
+ if (!venue) return "Location not specified";
+
+ const parts = [venue.city, venue.state].filter(Boolean);
+ return parts.length > 0 ? parts.join(", ") : "Location not specified";
+};
+
+/**
+ * Multi-line address for display on venue details.
+ */
+export const getVenueAddressLines = (venue) => {
+ const display = getVenueAddressDisplay(venue);
+ return display.lines.map((line) => line.value);
+};
+
+/**
+ * Structured address rows for venue details UI.
+ */
+export const getVenueAddressDisplay = (venue) => {
+ if (!venue) {
+ return { title: null, lines: [] };
+ }
+
+ const lines = [];
+
+ if (venue.address?.trim()) {
+ lines.push({
+ key: "address",
+ label: null,
+ value: venue.address.trim(),
+ });
+ }
+
+ if (venue.city?.trim()) {
+ lines.push({
+ key: "city",
+ label: "City",
+ value: venue.city.trim(),
+ });
+ }
+
+ if (venue.state?.trim()) {
+ lines.push({
+ key: "state",
+ label: "State",
+ value: venue.state.trim(),
+ });
+ }
+
+ if (venue.pincode?.trim()) {
+ lines.push({
+ key: "pincode",
+ label: "Pincode",
+ value: venue.pincode.trim(),
+ });
+ }
+
+ return {
+ title: venue.title?.trim() || null,
+ lines,
+ };
+};
+
+export const getVenueGoogleMapsUrl = (venue) => {
+ const coords = getVenueCoordinates(venue);
+
+ if (coords) {
+ return `https://www.google.com/maps/search/?api=1&query=${coords.latitude},${coords.longitude}`;
+ }
+
+ const query = formatVenueLocation(venue);
+
+ if (!query || query === "Location not specified") {
+ return null;
+ }
+
+ return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
+};
+
+export const buildGeocodeQuery = ({ address, city, state, pincode }) =>
+ [address, city, state, pincode, "India"]
+ .filter((part) => part?.trim())
+ .join(", ");
diff --git a/frontend/vercel.json b/frontend/vercel.json
new file mode 100644
index 0000000000..0f32683a98
--- /dev/null
+++ b/frontend/vercel.json
@@ -0,0 +1,3 @@
+{
+ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000000..c4069b7700
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+})