diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7d70631d7..45b35fc55 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature / Module (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update +- [ ] Documentation update ## Checklist: - [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md). diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..865b91c45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# add the gitgnore files here + +.venv/ +backend/.venv/ +backend/.env +__pycache__/ +*.pyc +.pytest_cache/ + +# Sprint planning (local / team use) +documents/sprints/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..eaef9bf58 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python", + "python.venvPath": "${workspaceFolder}/backend", + "python.venvFolders": [".venv"], + "python.terminal.activateEnvironment": true, + "python.analysis.extraPaths": ["${workspaceFolder}/backend"], + "python.analysis.diagnosticMode": "workspace", + "pyright.venvPath": "${workspaceFolder}/backend", + "pyright.venv": ".venv" +} diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 000000000..1e40e1c57 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,150 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be installed by adding +# `alembic[tz]` to the pip requirements. +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +# Use backend/alembic.ini — DATABASE_URL comes from backend/.env +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 000000000..9ee505558 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,11 @@ +APP_NAME=BookMyVenue +APP_ENV=development +DEBUG=true +HOST=0.0.0.0 +PORT=8000 +DATABASE_URL=postgresql://postgres:password@localhost:5432/bookmyvenue +SECRET_KEY=change-me-to-a-long-random-string +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 +CORS_ORIGINS=http://localhost:5173,http://localhost:3000 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..15244ea04 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +*.pyc +.env +bookmyvenue.db \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 000000000..f07c16cb7 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,207 @@ +# BookMyVenue — Backend + +FastAPI backend for BookMyVenue. This folder holds the API server that powers venue discovery, bookings, and admin workflows. + +See the full architecture reference: [`documents/FolderArchitecture.md`](../documents/FolderArchitecture.md) + +--- + +## Prerequisites + +- Python 3.11+ (recommended) +- PostgreSQL +- Git + +--- + +## 1. Create the backend project + +From the repository root: + +```bash +cd backend +``` + +Scaffold the **modular monolithic** layout below. One deployable app, organized by feature modules — not microservices. + +```bash +backend/ +├── app/ +│ ├── main.py # FastAPI entry point — register routes here +│ ├── core/ +│ │ ├── config.py # Settings loaded from .env +│ │ └── security.py # JWT, password hashing +│ ├── db/ +│ │ ├── session.py # Database session / engine +│ │ └── base.py # SQLAlchemy declarative base +│ ├── modules/ # Feature-based modules (one folder per domain) +│ │ ├── auth/ +│ │ │ ├── routes.py # HTTP handlers only +│ │ │ ├── schemas.py # Pydantic request/response models +│ │ │ ├── service.py # Business logic +│ │ │ └── models.py # SQLAlchemy tables +│ │ ├── users/ +│ │ ├── venues/ +│ │ ├── bookings/ +│ │ └── admin/ +│ └── utils/ +│ └── helpers.py +├── migrations/ # Alembic migrations +├── tests/ +├── .env # Local secrets (never commit) +├── .env.example # Template for contributors +├── requirements.txt +└── README.md +``` + +### Modular monolithic rules + +| Layer | Responsibility | +|-------|----------------| +| `routes.py` | Accept requests, return responses. No business logic. | +| `service.py` | All business rules and orchestration. | +| `models.py` | Database tables (SQLAlchemy). | +| `schemas.py` | Input/output validation (Pydantic). | +| `core/` | App-wide config, security, shared settings. | +| `db/` | Database connection setup only. | + +**Do not** put logic in `main.py` or route handlers. **Do not** access the database directly from routes — go through services. + +Request flow: + +```text +Client → routes.py → service.py → database → response +``` + +--- + +## 2. Create a virtual environment + +```bash +python3 -m venv .venv +``` + +Activate it: + +**macOS / Linux** + +```bash +source .venv/bin/activate +``` + +**Windows** + +```bash +.venv\Scripts\activate +``` + +Your shell prompt should show `(.venv)` when active. + +--- + +## 3. Install dependencies + +Create a `requirements.txt` with at least: + +```txt +fastapi +uvicorn[standard] +sqlalchemy +psycopg2-binary +python-dotenv +pydantic-settings +python-jose[cryptography] +passlib[bcrypt] +alembic +``` + +Install: + +```bash +pip install -r requirements.txt +``` + +To save your current environment after adding packages: + +```bash +pip freeze > requirements.txt +``` + +--- + +## 4. Environment variables (`.env`) + +Copy the example file and fill in your local values: + +```bash +cp .env.example .env +``` + +Example `.env.example`: + +```env +# App +APP_NAME=BookMyVenue +APP_ENV=development +DEBUG=true + +# Server +HOST=0.0.0.0 +PORT=8000 + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/bookmyvenue + +# Security +SECRET_KEY=change-me-to-a-long-random-string +ACCESS_TOKEN_EXPIRE_MINUTES=30 +ALGORITHM=HS256 + +# CORS (frontend URL) +CORS_ORIGINS=http://localhost:5173,http://localhost:3000 +``` + +Load these in `app/core/config.py` using `pydantic-settings` or `python-dotenv`. **Never commit `.env`** — it is listed in `.gitignore`. + +--- + +## 5. Run the development server + +```bash +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +- API: http://localhost:8000 +- Interactive docs: http://localhost:8000/docs +- OpenAPI schema: http://localhost:8000/openapi.json + +--- + +## 6. Database migrations (Alembic) + +From the `backend/` directory: + +```bash +alembic revision --autogenerate -m "describe your change" +alembic upgrade head +``` + +--- + +## Adding a new feature module + +1. Create a folder under `app/modules//`. +2. Add `routes.py`, `schemas.py`, `service.py`, and `models.py`. +3. Register the router in `app/main.py`. +4. Add a migration if the module introduces new tables. + +Keep each module self-contained. Shared code belongs in `core/`, `db/`, or `utils/` — not copied across modules. + +--- + +## Related docs + +- [System Design](../documents/SystemDesign.md) +- [Folder Architecture](../documents/FolderArchitecture.md) +- [Database Design](../documents/DBDesign.md) +- [Contributing](../CONTRIBUTING.md) diff --git a/backend/SPRINT_02_PROGRESS.md b/backend/SPRINT_02_PROGRESS.md new file mode 100644 index 000000000..b6684e9cb --- /dev/null +++ b/backend/SPRINT_02_PROGRESS.md @@ -0,0 +1,17 @@ +Sprint 02 Progress + +Completed: +✓ Venue CRUD +✓ Amenities API +✓ Amenity seeding +✓ Search, filter, pagination +✓ Venue ↔ Amenity many-to-many relationship +✓ GET /venues/{id} returns amenities +✓ My Venues endpoint scaffold +✓ Availability endpoint scaffold + +Next: +1. Owner Profile model/table +2. Proper owner-specific My Venues +3. Real availability checking +4. Admin approval integration \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 000000000..7c913b6ab --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,39 @@ +[alembic] +script_location = migrations +prepend_sys_path = . +path_separator = os +sqlalchemy.url = driver://user:pass@localhost/dbname + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 000000000..6f7286743 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DATABASE_URL: str + SECRET_KEY: str + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + + class Config: + env_file = ".env" + +# Single instance — imported everywhere +settings = Settings() \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 000000000..8152f0840 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,70 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from app.core.config import settings +from app.db.deps import get_db +from app.models.user import User + +# Tells FastAPI where the login endpoint is +oauth2_scheme = HTTPBearer() + +# Creating a JWT token +def create_access_token(data: dict) -> str: + to_encode = data.copy() + + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode.update({"exp": expire}) + + token = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + return token + +# Decode token and return the current user +def get_current_user( + token = Depends(oauth2_scheme), + db: Session = Depends(get_db) +) -> User: + + # This is the error we raise if anything goes wrong + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + # Decode the token using our secret key + payload = jwt.decode( + token.credentials, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM] + ) + # Extract the user id we stored in "sub" + user_id: str = payload.get("sub") + + if user_id is None: + raise credentials_exception + + except JWTError: + raise credentials_exception + + # Look up the user in the database + user = db.query(User).filter(User.id == int(user_id)).first() + + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Account is inactive" + ) + + return user \ No newline at end of file diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/app/db/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 000000000..fa2b68a5d --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/backend/app/db/database.py b/backend/app/db/database.py new file mode 100644 index 000000000..8acb852fd --- /dev/null +++ b/backend/app/db/database.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, sessionmaker +from app.core.config import settings + + +# Creating the engine - the single connection to database +engine = create_engine( + settings.DATABASE_URL +) + +# Session factory - creating individual sessions per request +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine +) + + +# Base class - all our models will inherit from this +class Base(DeclarativeBase): + pass \ No newline at end of file diff --git a/backend/app/db/deps.py b/backend/app/db/deps.py new file mode 100644 index 000000000..8c69df981 --- /dev/null +++ b/backend/app/db/deps.py @@ -0,0 +1,17 @@ +# This function is a dependency. FastAPI calls it automatically + +# 1. FastAPI sees a route needs get_db +# 2. get_db() runs → creates a session (db = SessionLocal()) +# 3. yield db → hands the session to the route function +# 4. Route does its work (query users, create booking, etc.) +# 5. Route finishes +# 6. get_db() resumes after yield → runs db.close() + +from app.db.database import SessionLocal + +def get_db(): + db = SessionLocal() # Opens a session + try: + yield db # hand it to the route + finally: + db.close() # always close it, even if an error occured \ No newline at end of file diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 000000000..f8d37384f --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,23 @@ +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +from app.core.config import get_settings + +settings = get_settings() + +engine = create_engine( + settings.database_url, + pool_pre_ping=True, +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 000000000..9924200ec --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,95 @@ +import logging +from contextlib import asynccontextmanager + +from sqlalchemy import text +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.db.database import Base, engine, SessionLocal + +# Models +from app.models import ( + user, + venue, + amenity, + venue_amenity, + owner_profile, + booking, + payment, +) + +# Routers +from app.routers import ( + auth, + venue, + amenity, + venue_amenity, + owner_profile, + bookings, + payments, +) + +from app.seeds.amenity_seed import seed_amenities + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("bookmyvenue") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + try: + with engine.connect() as connection: + connection.execute(text("SELECT 1")) + + logger.info("Database connected successfully") + + # Create tables + Base.metadata.create_all(bind=engine) + + # Seed amenities + db = SessionLocal() + try: + seed_amenities(db) + finally: + db.close() + + except Exception as exc: + logger.error(f"Database startup failed: {exc}") + raise + + yield + + +app = FastAPI( + title="BookMyVenue API", + description="Backend for the BookMyVenue platform", + version="1.0.0", + lifespan=lifespan, +) + +origins = [ + "http://localhost:5173", + "http://localhost:3000", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Routers +app.include_router(auth.router) +app.include_router(venue.router) +app.include_router(amenity.router) +app.include_router(venue_amenity.router) +app.include_router(owner_profile.router) +app.include_router(bookings.router) +app.include_router(payments.router) + + +@app.get("/") +def root(): + return {"message": "BookMyVenue API is running"} \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/models/amenity.py b/backend/app/models/amenity.py new file mode 100644 index 000000000..c4e678af3 --- /dev/null +++ b/backend/app/models/amenity.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship +from app.db.database import Base + +class Amenity(Base): + __tablename__ = "amenities" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, nullable=False) + + venues = relationship( + "Venue", + secondary="venue_amenities" + ) \ No newline at end of file diff --git a/backend/app/models/booking.py b/backend/app/models/booking.py new file mode 100644 index 000000000..ec43a361c --- /dev/null +++ b/backend/app/models/booking.py @@ -0,0 +1,39 @@ +from decimal import Decimal +from datetime import date, datetime, time, timezone +from typing import Optional +from sqlalchemy import ( + Integer, String, Date, Time, Numeric, Text, DateTime, + ForeignKey, UniqueConstraint, CheckConstraint, +) +from sqlalchemy.orm import Mapped, mapped_column +from app.db.database import Base + + +class Booking(Base): + __tablename__ = "bookings" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + venue_id: Mapped[int] = mapped_column(Integer, ForeignKey("venues.id", ondelete="CASCADE"), nullable=False) + booking_date: Mapped[date] = mapped_column(Date, nullable=False) + time_slot: Mapped[time] = mapped_column(Time, nullable=False) + notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending_payment") + cancellation_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + cancelled_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + __table_args__ = ( + # one venue cannot be booked twice for the same date + time + UniqueConstraint("venue_id", "booking_date", "time_slot", name="uq_booking_slot"), + CheckConstraint( + "status IN ('pending_payment', 'booked', 'cancelled')", + name="ck_booking_status", + ), + ) \ No newline at end of file diff --git a/backend/app/models/owner_profile.py b/backend/app/models/owner_profile.py new file mode 100644 index 000000000..ad10f6a30 --- /dev/null +++ b/backend/app/models/owner_profile.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from app.db.database import Base + +class OwnerProfile(Base): + __tablename__ = "owner_profiles" + + id = Column(Integer, primary_key=True, index=True) + + user_id = Column( + Integer, + ForeignKey("users.id"), + nullable=False + ) + + business_name = Column( + String, + nullable=False + ) \ No newline at end of file diff --git a/backend/app/models/payment.py b/backend/app/models/payment.py new file mode 100644 index 000000000..5f2c289ad --- /dev/null +++ b/backend/app/models/payment.py @@ -0,0 +1,36 @@ +from decimal import Decimal +from typing import Optional +from sqlalchemy import ( + Integer, String, Numeric, Text, DateTime, ForeignKey, CheckConstraint, +) +from sqlalchemy.orm import Mapped, mapped_column +from datetime import datetime, timezone +from app.db.database import Base + + +class Payment(Base): + __tablename__ = "payments" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + payment_id: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False) # e.g. pay_abc123 + booking_id: Mapped[int] = mapped_column(Integer, ForeignKey("bookings.id", ondelete="CASCADE"), nullable=False) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) + currency: Mapped[str] = mapped_column(String(3), nullable=False, default="INR") + status: Mapped[str] = mapped_column(String(20), nullable=False, default="created") + gateway: Mapped[str] = mapped_column(String(30), nullable=False, default="razorpay") + failure_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + __table_args__ = ( + CheckConstraint( + "status IN ('created', 'paid', 'failed', 'refunded', 'refund_pending')", + name="ck_payment_status", + ), + ) \ No newline at end of file diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 000000000..429273d7b --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from datetime import datetime +from app.db.database import Base + + +# This class creates the table +class User(Base): + # Actual table name in the database + __tablename__ = "users" + + # Columns in the table + id = Column(Integer,primary_key=True, index=True) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + role = Column(String, default="user") + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/backend/app/models/venue.py b/backend/app/models/venue.py new file mode 100644 index 000000000..94b1757cb --- /dev/null +++ b/backend/app/models/venue.py @@ -0,0 +1,93 @@ +from datetime import datetime, timezone + +from sqlalchemy import ( + Column, + Integer, + String, + Numeric, + Text, + Boolean, + DateTime, + ForeignKey, + CheckConstraint, +) +from sqlalchemy.orm import relationship + +from app.db.database import Base + + +class Venue(Base): + __tablename__ = "venues" + + id = Column(Integer, primary_key=True, index=True) + + owner_id = Column( + Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ) + + name = Column(String(150), nullable=False) + location = Column(String(255), nullable=False) + + price_per_day = Column(Numeric(10, 2), nullable=False) + + description = Column(Text, nullable=True) + + approval_status = Column( + String(20), + nullable=False, + default="pending", + ) + + rejection_reason = Column(Text, nullable=True) + + average_rating = Column( + Numeric(3, 2), + default=0.00, + ) + + total_reviews = Column( + Integer, + nullable=False, + default=0, + ) + + is_active = Column( + Boolean, + nullable=False, + default=True, + ) + + created_at = Column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + ) + + updated_at = Column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + # Relationship + amenities = relationship( + "Amenity", + secondary="venue_amenities", + back_populates="venues", + ) + + __table_args__ = ( + CheckConstraint( + "price_per_day >= 0", + name="ck_venue_price_non_negative", + ), + CheckConstraint( + "approval_status IN ('pending', 'approved', 'rejected')", + name="ck_venue_approval_status", + ), + CheckConstraint( + "average_rating >= 0 AND average_rating <= 5", + name="ck_venue_average_rating", + ), + ) \ No newline at end of file diff --git a/backend/app/models/venue_amenity.py b/backend/app/models/venue_amenity.py new file mode 100644 index 000000000..5617627dc --- /dev/null +++ b/backend/app/models/venue_amenity.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, ForeignKey +from app.db.database import Base + +class VenueAmenity(Base): + __tablename__ = "venue_amenities" + + venue_id = Column( + Integer, + ForeignKey("venues.id"), + primary_key=True + ) + + amenity_id = Column( + Integer, + ForeignKey("amenities.id"), + primary_key=True + ) \ No newline at end of file diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/routers/amenity.py b/backend/app/routers/amenity.py new file mode 100644 index 000000000..ef81d350f --- /dev/null +++ b/backend/app/routers/amenity.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.deps import get_db +from app.schemas.amenity import AmenityOut +from app.services.amenity_service import get_amenities + +router = APIRouter( + prefix="/amenities", + tags=["Amenities"] +) + +@router.get("/", response_model=list[AmenityOut]) +def list_amenities( + db: Session = Depends(get_db) +): + return get_amenities(db) \ No newline at end of file diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 000000000..6ba079412 --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.deps import get_db +from app.schemas.user import UserCreate, UserLogin, UserOut, TokenOut +from app.services.auth_service import create_user, authenticate_user +from app.core.security import create_access_token, get_current_user +from app.models.user import User + + +router = APIRouter(prefix="/auth", tags=["Authentication"]) + +# Register router + +@router.post("/register", response_model = UserOut) +def register(user_data: UserCreate, db: Session = Depends(get_db)): + new_user = create_user(db, user_data) + return new_user + + +# Login router + +@router.post("/login", response_model = TokenOut) +def login(credential: UserLogin, db: Session = Depends(get_db)): + user = authenticate_user(db, credential.email, credential.password) + + access_token = create_access_token(data={"sub": str(user.id)}) + + return TokenOut(access_token=access_token) + + +# Get current logged in user + +@router.get("/me", response_model=UserOut) +def get_me(current_user: User = Depends(get_current_user)): + return current_user \ No newline at end of file diff --git a/backend/app/routers/bookings.py b/backend/app/routers/bookings.py new file mode 100644 index 000000000..6fa506b23 --- /dev/null +++ b/backend/app/routers/bookings.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.db.deps import get_db +from app.core.security import get_current_user +from app.models.user import User +from app.schemas.booking import BookingCreate, BookingOut +from app.services import booking_service + +router = APIRouter(prefix="/bookings", tags=["Bookings"]) + +@router.post("", response_model=BookingOut) +def create_booking( + data: BookingCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + return booking_service.create_booking(db, current_user, data) + + \ No newline at end of file diff --git a/backend/app/routers/owner_profile.py b/backend/app/routers/owner_profile.py new file mode 100644 index 000000000..b937e5b81 --- /dev/null +++ b/backend/app/routers/owner_profile.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.deps import get_db +from app.schemas.owner_profile import ( + OwnerProfileCreate, + OwnerProfileOut +) +from app.services.owner_profile_service import ( + create_owner_profile, + get_owner_profile +) + + + +router = APIRouter( + prefix="/owner-profile", + tags=["Owner Profile"] +) + +@router.post("/", response_model=OwnerProfileOut) +def create_profile( + profile: OwnerProfileCreate, + db: Session = Depends(get_db) +): + return create_owner_profile( + db, + user_id=1, + profile_data=profile + ) + +@router.get("/", response_model=OwnerProfileOut) +def get_profile( + db: Session = Depends(get_db) +): + return get_owner_profile( + db, + user_id=1 + ) \ No newline at end of file diff --git a/backend/app/routers/payments.py b/backend/app/routers/payments.py new file mode 100644 index 000000000..c54f523ca --- /dev/null +++ b/backend/app/routers/payments.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.db.deps import get_db +from app.core.security import get_current_user +from app.models.user import User +from app.schemas.payment import PaymentInitiate, PaymentConfirm, PaymentOut +from app.services import payment_service +router = APIRouter(prefix="/payments", tags=["Payments"]) + + +# initiate payment +@router.post("/initiate", response_model=PaymentOut) +def initiate( + data: PaymentInitiate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + return payment_service.initiate_payment(db, current_user, data) + +# confirm payment +@router.post("/confirm", response_model=PaymentOut) +def confirm( + data: PaymentConfirm, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + return payment_service.confirm_payment(db, current_user, data) + +# get payment status +@router.get("/{payment_id}/status", response_model=PaymentOut) +def payment_status( + payment_id: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + return payment_service.get_payment_status(db, current_user, payment_id) + diff --git a/backend/app/routers/venue.py b/backend/app/routers/venue.py new file mode 100644 index 000000000..bd0f94ba5 --- /dev/null +++ b/backend/app/routers/venue.py @@ -0,0 +1,107 @@ +from fastapi import APIRouter, Depends,Query +from sqlalchemy.orm import Session + +from app.db.deps import get_db +from app.schemas.venue import VenueCreate,VenueUpdate, VenueOut +from app.services.venue_service import( + create_venue, + get_venues, + get_venue_by_id, + update_venue, + delete_venue, + get_my_venues, + approve_venue, + get_pending_venues + +) + + +router = APIRouter(prefix="/venues", tags=["Venues"]) + +@router.get("/my-venues", response_model=list[VenueOut]) +def list_my_venues( + db: Session = Depends(get_db) +): + return get_my_venues( + db, + owner_id=1 + ) + + +@router.get("/pending", response_model=list[VenueOut]) +def list_pending_venues( + db: Session = Depends(get_db) +): + return get_pending_venues(db) +@router.post("/", response_model=VenueOut) +def create_new_venue( + venue: VenueCreate, + db: Session = Depends(get_db) +): + return create_venue(db, venue) + +@router.get("/", response_model=list[VenueOut]) +def list_venues( + location: str | None = Query(default=None, description="Filter by location"), + search: str | None = Query(default=None, description="Search by venue name"), + skip: int = Query(default=0), + limit: int = Query(default=10), + db: Session = Depends(get_db) +): + return get_venues( + db, + location=location, + search=search, + skip=skip, + limit=limit + ) + +@router.get("/{venue_id}", response_model=VenueOut) +def get_single_venue( + venue_id: int, + db: Session = Depends(get_db) +): + return get_venue_by_id(db, venue_id) + +@router.put("/{venue_id}", response_model=VenueOut) +def update_existing_venue( + venue_id: int, + venue: VenueUpdate, + db: Session = Depends(get_db) +): + return update_venue( + db, + venue_id, + venue + ) + +@router.delete("/{venue_id}") +def delete_existing_venue( + venue_id: int, + db: Session = Depends(get_db) +): + return delete_venue( + db, + venue_id + ) + + +@router.get("/{venue_id}/availability") +def check_availability( + venue_id: int +): + return { + "venue_id": venue_id, + "available": True + } + +@router.put("/{venue_id}/approve", response_model=VenueOut) +def approve_existing_venue( + venue_id: int, + db: Session = Depends(get_db) +): + return approve_venue( + db, + venue_id + ) + diff --git a/backend/app/routers/venue_amenity.py b/backend/app/routers/venue_amenity.py new file mode 100644 index 000000000..2be45e455 --- /dev/null +++ b/backend/app/routers/venue_amenity.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.deps import get_db +from app.services.venue_amenity_service import ( + add_amenity_to_venue +) + +router = APIRouter( + prefix="/venue-amenities", + tags=["Venue Amenities"] +) + +@router.post("/{venue_id}/{amenity_id}") +def link_amenity( + venue_id: int, + amenity_id: int, + db: Session = Depends(get_db) +): + return add_amenity_to_venue( + db, + venue_id, + amenity_id + ) \ No newline at end of file diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/schemas/amenity.py b/backend/app/schemas/amenity.py new file mode 100644 index 000000000..1246c1981 --- /dev/null +++ b/backend/app/schemas/amenity.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +class AmenityOut(BaseModel): + id: int + name: str + + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/backend/app/schemas/booking.py b/backend/app/schemas/booking.py new file mode 100644 index 000000000..a27088816 --- /dev/null +++ b/backend/app/schemas/booking.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, Field +from datetime import date, time, datetime +from typing import Optional + +class BookingCreate(BaseModel): + venue_id: int + booking_date: date # "YYYY-MM-DD" + time_slot: time # "HH:MM" + notes: Optional[str] = None + +class BookingOut(BaseModel): + id: int + venue_id: int + booking_date: date + time_slot: time + notes: Optional[str] = None + status: str + amount: float + created_at: datetime + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/backend/app/schemas/owner_profile.py b/backend/app/schemas/owner_profile.py new file mode 100644 index 000000000..d491f9670 --- /dev/null +++ b/backend/app/schemas/owner_profile.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +class OwnerProfileCreate(BaseModel): + business_name: str + +class OwnerProfileOut(BaseModel): + id: int + user_id: int + business_name: str + + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/backend/app/schemas/payment.py b/backend/app/schemas/payment.py new file mode 100644 index 000000000..73eb91990 --- /dev/null +++ b/backend/app/schemas/payment.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + + +class PaymentInitiate(BaseModel): + booking_id: int + + +class PaymentConfirm(BaseModel): + payment_id: str + # mock flag: let frontend simulate success or failure + success: bool = True + + +class PaymentOut(BaseModel): + payment_id: str + booking_id: int + amount: float + currency: str + status: str + paid_at: Optional[datetime] = None + created_at: datetime + + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 000000000..9b1d7f133 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, EmailStr, Field +from typing import Literal +from datetime import datetime + +# What react sends when registering + +class UserCreate(BaseModel): + email: EmailStr + password: str = Field(min_length=6) + role: Literal["user","owner"] = "user" + + +# What react send when logging in + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +# What FastAPI send back. It does not sends back password + +class UserOut(BaseModel): + id: int + email: EmailStr + role: str + is_active: bool + created_at: datetime + + model_config = {"from_attributes": True} + + +# What FastAPI sends back after login + +class TokenOut(BaseModel): + access_token: str + token_type: str = "bearer" \ No newline at end of file diff --git a/backend/app/schemas/venue.py b/backend/app/schemas/venue.py new file mode 100644 index 000000000..069ad7aec --- /dev/null +++ b/backend/app/schemas/venue.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel +from app.schemas.amenity import AmenityOut + +class VenueCreate(BaseModel): + name: str + location: str + price_per_day: float + + +class VenueUpdate(BaseModel): + name: str + location: str + price_per_day: float + +class VenueOut(BaseModel): + id: int + name: str + location: str + price_per_day: float + approval_status: str + amenities: list[AmenityOut] = [] + + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/backend/app/seeds/amenity_seed.py b/backend/app/seeds/amenity_seed.py new file mode 100644 index 000000000..e11bf6462 --- /dev/null +++ b/backend/app/seeds/amenity_seed.py @@ -0,0 +1,23 @@ +from sqlalchemy.orm import Session +from app.models.amenity import Amenity + +def seed_amenities(db: Session): + amenities = [ + "Wi-Fi", + "Parking", + "AC", + "Projector", + "Catering" + ] + + for amenity_name in amenities: + existing = db.query(Amenity).filter( + Amenity.name == amenity_name + ).first() + + if not existing: + db.add( + Amenity(name=amenity_name) + ) + + db.commit() \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/services/amenity_service.py b/backend/app/services/amenity_service.py new file mode 100644 index 000000000..6610ef03a --- /dev/null +++ b/backend/app/services/amenity_service.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session +from app.models.amenity import Amenity + +def get_amenities(db: Session): + return db.query(Amenity).all() \ No newline at end of file diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 000000000..22f02dee9 --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,86 @@ +from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from passlib.context import CryptContext +from app.models.user import User +from app.schemas.user import UserCreate + + +# Creating an instance of CryptoContext Class. + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +# function to hash password + +def hash_password(password:str) -> str: + return pwd_context.hash(password) + + +# function to verify the login password with the stored hashed password + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +# Registering a new user + +def create_user(db: Session, user_data: UserCreate) -> User: + # Checking whether email already registered + existing_user = db.query(User).filter(User.email == user_data.email).first() + + if existing_user: + raise HTTPException( + status_code = status.HTTP_400_BAD_REQUEST, + detail = "Email is already registered" + ) + + # Hashing the password + + hashed = hash_password(user_data.password) + + # Creating the User object + new_user = User( + email = user_data.email, + hashed_password = hashed, + role = user_data.role + ) + + # Saving to database + + db.add(new_user) + db.commit() + db.refresh(new_user) + + return new_user + + +# Authenticate user on login + +def authenticate_user(db: Session, email: str, password: str) -> User: + # Finding user by email + + user = db.query(User).filter(User.email == email).first() + + if not user: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid email or password" + ) + + # verifying password + + if not verify_password(password, user.hashed_password): + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid email or password" + ) + + # checking account is active + + if not user.is_active: + raise HTTPException( + status_code = status.HTTP_403_FORBIDDEN, + detail = "Account is inactive" + ) + + return user \ No newline at end of file diff --git a/backend/app/services/booking_service.py b/backend/app/services/booking_service.py new file mode 100644 index 000000000..ef035e9c3 --- /dev/null +++ b/backend/app/services/booking_service.py @@ -0,0 +1,66 @@ +from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from datetime import datetime, timezone +from app.models.booking import Booking +from app.models.user import User +from app.models.venue import Venue +from app.schemas.booking import BookingCreate + +def get_venue(db: Session, venue_id: int): + return db.query(Venue).filter(Venue.id == venue_id).first() + +def create_booking(db: Session, current_user: User, data: BookingCreate): + # 1. only normal users can book + if current_user.role != "user": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only users can create bookings", + ) + # 2. venue must exist and be approved + venue = get_venue(db, data.venue_id) + if venue is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Venue not found", + ) + if venue.approval_status != "approved": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Venue is not available for booking", + ) + # 3. booking date cannot be in the past + if data.booking_date < datetime.now(timezone.utc).date(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Booking date cannot be in the past", + ) + # 4. slot must be free (ignore cancelled bookings) + clash = ( + db.query(Booking) + .filter( + Booking.venue_id == data.venue_id, + Booking.booking_date == data.booking_date, + Booking.time_slot == data.time_slot, + Booking.status != "cancelled", + ) + .first() + ) + if clash: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="This slot is already booked", + ) + # 5. create the booking (price comes from the venue) + booking = Booking( + user_id=current_user.id, + venue_id=data.venue_id, + booking_date=data.booking_date, + time_slot=data.time_slot, + notes=data.notes, + amount=venue.price_per_day, + status="pending_payment", + ) + db.add(booking) + db.commit() + db.refresh(booking) + return booking \ No newline at end of file diff --git a/backend/app/services/owner_profile_service.py b/backend/app/services/owner_profile_service.py new file mode 100644 index 000000000..ff2b1b50c --- /dev/null +++ b/backend/app/services/owner_profile_service.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import Session +from app.models.owner_profile import OwnerProfile +from app.schemas.owner_profile import OwnerProfileCreate + +def create_owner_profile( + db: Session, + user_id: int, + profile_data: OwnerProfileCreate +): + profile = OwnerProfile( + user_id=user_id, + business_name=profile_data.business_name + ) + + db.add(profile) + db.commit() + db.refresh(profile) + + return profile + +def get_owner_profile( + db: Session, + user_id: int +): + return db.query(OwnerProfile).filter( + OwnerProfile.user_id == user_id + ).first() \ No newline at end of file diff --git a/backend/app/services/payment_service.py b/backend/app/services/payment_service.py new file mode 100644 index 000000000..9458691f4 --- /dev/null +++ b/backend/app/services/payment_service.py @@ -0,0 +1,93 @@ +import uuid +from datetime import datetime, timezone +from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from app.models.payment import Payment +from app.models.booking import Booking +from app.models.user import User +from app.schemas.payment import PaymentInitiate, PaymentConfirm + + +def generate_payment_id() : + return "pay_" + uuid.uuid4().hex[:12] + + +def initiate_payment(db: Session, current_user: User, data: PaymentInitiate) : + booking = db.query(Booking).filter(Booking.id == data.booking_id).first() + if booking is None: + raise HTTPException(status_code=404, detail="Booking not found") + if booking.user_id != current_user.id: + raise HTTPException(status_code=403, detail="This is not your booking") + if booking.status != "pending_payment": + raise HTTPException( + status_code=400, + detail="Booking is not awaiting payment", + ) + # reuse an existing 'created' payment if one already exists + existing = ( + db.query(Payment) + .filter(Payment.booking_id == booking.id, Payment.status == "created") + .first() + ) + if existing: + return existing + payment = Payment( + payment_id=generate_payment_id(), + booking_id=booking.id, + user_id=current_user.id, + amount=booking.amount, + currency="INR", + status="created", + gateway="razorpay", + ) + db.add(payment) + db.commit() + db.refresh(payment) + return payment + +# confirm the payment +def confirm_payment(db: Session, current_user: User, data: PaymentConfirm) -> Payment: + payment = ( + db.query(Payment).filter(Payment.payment_id == data.payment_id).first() + ) + if payment is None: + raise HTTPException(status_code=404, detail="Payment not found") + if payment.user_id != current_user.id: + raise HTTPException(status_code=403, detail="This is not your payment") + if payment.status != "created": + raise HTTPException( + status_code=400, + detail=f"Payment already {payment.status}", + ) + booking = db.query(Booking).filter(Booking.id == payment.booking_id).first() + if booking is None: + raise HTTPException(status_code=404, detail="Booking not found for this payment") + + if data.success: + # mock gateway success + if booking.status != "pending_payment": + raise HTTPException( + status_code=400, + detail=f"Booking is no longer awaiting payment (status: {booking.status})", + ) + payment.status = "paid" + payment.paid_at = datetime.now(timezone.utc) + payment.failure_reason = None + booking.status = "booked" + else: + # mock gateway failure — leave the booking as 'pending_payment' so the + # user can initiate a new payment and try again + payment.status = "failed" + payment.failure_reason = "Payment failed at gateway (mock)" + db.commit() + db.refresh(payment) + return payment + +# get the payment status +def get_payment_status(db: Session, current_user: User, payment_id: str) -> Payment: + payment = db.query(Payment).filter(Payment.payment_id == payment_id).first() + if payment is None: + raise HTTPException(status_code=404, detail="Payment not found") + if payment.user_id != current_user.id: + raise HTTPException(status_code=403, detail="This is not your payment") + return payment \ No newline at end of file diff --git a/backend/app/services/venue_amenity_service.py b/backend/app/services/venue_amenity_service.py new file mode 100644 index 000000000..2bff2272d --- /dev/null +++ b/backend/app/services/venue_amenity_service.py @@ -0,0 +1,19 @@ +from sqlalchemy.orm import Session +from app.models.venue_amenity import VenueAmenity + +def add_amenity_to_venue( + db: Session, + venue_id: int, + amenity_id: int +): + link = VenueAmenity( + venue_id=venue_id, + amenity_id=amenity_id + ) + + db.add(link) + db.commit() + + return { + "message": "Amenity linked to venue" + } \ No newline at end of file diff --git a/backend/app/services/venue_service.py b/backend/app/services/venue_service.py new file mode 100644 index 000000000..0075d860e --- /dev/null +++ b/backend/app/services/venue_service.py @@ -0,0 +1,130 @@ +from sqlalchemy.orm import Session +from app.models.venue import Venue +from app.schemas.venue import VenueCreate +from fastapi import HTTPException + +def create_venue(db: Session, venue_data: VenueCreate) -> Venue: + new_venue = Venue( + owner_id=1, + name=venue_data.name, + location=venue_data.location, + price_per_day=venue_data.price_per_day + ) + + db.add(new_venue) + db.commit() + db.refresh(new_venue) + + return new_venue + +def get_venues( + db: Session, + location: str = None, + search: str = None, + skip: int = 0, + limit: int = 10 +): + query = db.query(Venue).filter( + Venue.approval_status == "approved" +) + + if location: + query = query.filter( + Venue.location.ilike(f"%{location}%") + ) + + if search: + query = query.filter( + Venue.name.ilike(f"%{search}%") + ) + + return query.offset(skip).limit(limit).all() + + +def get_venue_by_id(db: Session, venue_id: int): + venue = db.query(Venue).filter( + Venue.id == venue_id + ).first() + + if not venue: + raise HTTPException( + status_code=404, + detail="Venue not found" + ) + + return venue + + +def update_venue( + db: Session, + venue_id: int, + venue_data +): + venue = db.query(Venue).filter( + Venue.id == venue_id + ).first() + + if not venue: + raise HTTPException( + status_code=404, + detail="Venue not found" + ) + + venue.name = venue_data.name + venue.location = venue_data.location + venue.price_per_day = venue_data.price_per_day + + db.commit() + db.refresh(venue) + + return venue + +def delete_venue(db: Session, venue_id: int): + venue = db.query(Venue).filter( + Venue.id == venue_id + ).first() + + if not venue: + raise HTTPException( + status_code=404, + detail="Venue not found" + ) + + db.delete(venue) + db.commit() + + return venue + +def get_my_venues( + db: Session, + owner_id: int +): + return db.query(Venue).filter( + Venue.owner_id == owner_id + ).all() + +def approve_venue( + db: Session, + venue_id: int +): + venue = db.query(Venue).filter( + Venue.id == venue_id + ).first() + + if not venue: + raise HTTPException( + status_code=404, + detail="Venue not found" + ) + + venue.approval_status = "approved" + + db.commit() + db.refresh(venue) + + return venue + +def get_pending_venues(db: Session): + return db.query(Venue).filter( + Venue.approval_status == "pending" + ).all() \ No newline at end of file diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/utils/errors.py b/backend/app/utils/errors.py new file mode 100644 index 000000000..f2b74f3d1 --- /dev/null +++ b/backend/app/utils/errors.py @@ -0,0 +1,23 @@ +class ErrorCode: + VALIDATION_ERROR = "VALIDATION_ERROR" + INVALID_CREDENTIALS = "INVALID_CREDENTIALS" + ACCOUNT_DISABLED = "ACCOUNT_DISABLED" + EMAIL_EXISTS = "EMAIL_EXISTS" + UNAUTHORIZED = "UNAUTHORIZED" + INTERNAL_ERROR = "INTERNAL_ERROR" + NOT_FOUND = "NOT_FOUND" + + +ERROR_MESSAGES = { + ErrorCode.VALIDATION_ERROR: "Please check your input and try again.", + ErrorCode.INVALID_CREDENTIALS: "Invalid email or password.", + ErrorCode.ACCOUNT_DISABLED: "Your account has been deactivated.", + ErrorCode.EMAIL_EXISTS: "Email already registered.", + ErrorCode.UNAUTHORIZED: "Please log in to continue.", + ErrorCode.INTERNAL_ERROR: "Something went wrong. Please try again later.", + ErrorCode.NOT_FOUND: "The requested resource was not found.", +} + + +def get_error_message(code: str, fallback: str = "An error occurred.") -> str: + return ERROR_MESSAGES.get(code, fallback) diff --git a/backend/app/utils/responses.py b/backend/app/utils/responses.py new file mode 100644 index 000000000..63234f009 --- /dev/null +++ b/backend/app/utils/responses.py @@ -0,0 +1,31 @@ +from typing import Any, Optional + +from fastapi.responses import JSONResponse + + +def success_response( + message: str, + data: Any = None, + status_code: int = 200, +) -> JSONResponse: + body = {"message": message, "data": data} + return JSONResponse(status_code=status_code, content=body) + + +def error_response( + code: str, + message: str, + status_code: int, + details: Optional[list] = None, +) -> JSONResponse: + error_body: dict = { + "code": code, + "message": message, + } + if details is not None: + error_body["details"] = details + + return JSONResponse( + status_code=status_code, + content={"error": error_body}, + ) diff --git a/backend/migrations/__init__.py b/backend/migrations/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 000000000..174846208 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,52 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from app.core.config import settings +from app.db.database import Base + +# Import all models so they register on Base.metadata (needed for autogenerate) +from app.models import user, venue, booking, payment # noqa: F401 + +config = context.config + +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 000000000..fbc4b07dc --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/001_create_users.py b/backend/migrations/versions/001_create_users.py new file mode 100644 index 000000000..9e300e098 --- /dev/null +++ b/backend/migrations/versions/001_create_users.py @@ -0,0 +1,46 @@ +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "001_create_users" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "users", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("email", sa.String(length=150), nullable=False), + sa.Column("mobile", sa.String(length=20), nullable=True), + sa.Column("password_hash", sa.Text(), nullable=False), + sa.Column("role", sa.String(length=20), nullable=False, server_default="user"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.CheckConstraint( + "role IN ('user', 'owner', 'admin')", + name="ck_users_role", + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), + ) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + + +def downgrade() -> None: + op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_table("users") diff --git a/backend/migrations/versions/002_create_refresh_tokens.py b/backend/migrations/versions/002_create_refresh_tokens.py new file mode 100644 index 000000000..56cca04b2 --- /dev/null +++ b/backend/migrations/versions/002_create_refresh_tokens.py @@ -0,0 +1,40 @@ +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "002_create_refresh_tokens" +down_revision: Union[str, None] = "001_create_users" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "refresh_tokens", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("token_hash", sa.Text(), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("token_hash"), + ) + op.create_index( + op.f("ix_refresh_tokens_user_id"), + "refresh_tokens", + ["user_id"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_refresh_tokens_user_id"), table_name="refresh_tokens") + op.drop_table("refresh_tokens") diff --git a/backend/migrations/versions/003_create_venues.py b/backend/migrations/versions/003_create_venues.py new file mode 100644 index 000000000..27ae00859 --- /dev/null +++ b/backend/migrations/versions/003_create_venues.py @@ -0,0 +1,75 @@ +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "003_create_venues" +down_revision: Union[str, None] = "002_create_refresh_tokens" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "venues", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("owner_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=150), nullable=False), + sa.Column("location", sa.String(length=255), nullable=False), + sa.Column("price_per_day", sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column( + "approval_status", + sa.String(length=20), + server_default="pending", + nullable=False, + ), + sa.Column("rejection_reason", sa.Text(), nullable=True), + sa.Column( + "average_rating", + sa.Numeric(precision=3, scale=2), + server_default="0.00", + nullable=True, + ), + sa.Column( + "total_reviews", + sa.Integer(), + server_default="0", + nullable=False, + ), + sa.Column( + "is_active", + sa.Boolean(), + server_default=sa.text("true"), + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.CheckConstraint("price_per_day >= 0", name="ck_venue_price_non_negative"), + sa.CheckConstraint( + "approval_status IN ('pending', 'approved', 'rejected')", + name="ck_venue_approval_status", + ), + sa.CheckConstraint( + "average_rating >= 0 AND average_rating <= 5", + name="ck_venue_average_rating", + ), + sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_venues_id"), "venues", ["id"], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f("ix_venues_id"), table_name="venues") + op.drop_table("venues") diff --git a/backend/migrations/versions/004_create_bookings.py b/backend/migrations/versions/004_create_bookings.py new file mode 100644 index 000000000..728146de2 --- /dev/null +++ b/backend/migrations/versions/004_create_bookings.py @@ -0,0 +1,58 @@ +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "004_create_bookings" +down_revision: Union[str, None] = "003_create_venues" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "bookings", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("venue_id", sa.Integer(), nullable=False), + sa.Column("booking_date", sa.Date(), nullable=False), + sa.Column("time_slot", sa.Time(), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("amount", sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column( + "status", + sa.String(length=20), + server_default="pending_payment", + nullable=False, + ), + sa.Column("cancellation_reason", sa.Text(), nullable=True), + sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.CheckConstraint( + "status IN ('pending_payment', 'booked', 'cancelled')", + name="ck_booking_status", + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["venue_id"], ["venues.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "venue_id", "booking_date", "time_slot", name="uq_booking_slot" + ), + ) + op.create_index(op.f("ix_bookings_id"), "bookings", ["id"], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f("ix_bookings_id"), table_name="bookings") + op.drop_table("bookings") diff --git a/backend/migrations/versions/005_create_payments.py b/backend/migrations/versions/005_create_payments.py new file mode 100644 index 000000000..e41f05485 --- /dev/null +++ b/backend/migrations/versions/005_create_payments.py @@ -0,0 +1,70 @@ +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "005_create_payments" +down_revision: Union[str, None] = "004_create_bookings" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "payments", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("payment_id", sa.String(length=50), nullable=False), + sa.Column("booking_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("amount", sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column( + "currency", + sa.String(length=3), + server_default="INR", + nullable=False, + ), + sa.Column( + "status", + sa.String(length=20), + server_default="created", + nullable=False, + ), + sa.Column( + "gateway", + sa.String(length=30), + server_default="razorpay", + nullable=False, + ), + sa.Column("failure_reason", sa.Text(), nullable=True), + sa.Column("paid_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.CheckConstraint( + "status IN ('created', 'paid', 'failed', 'refunded', 'refund_pending')", + name="ck_payment_status", + ), + sa.ForeignKeyConstraint(["booking_id"], ["bookings.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("payment_id"), + ) + op.create_index(op.f("ix_payments_id"), "payments", ["id"], unique=False) + op.create_index( + op.f("ix_payments_payment_id"), "payments", ["payment_id"], unique=True + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_payments_payment_id"), table_name="payments") + op.drop_index(op.f("ix_payments_id"), table_name="payments") + op.drop_table("payments") diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 000000000..feeee28cf --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,10 @@ +[tool.pyright] +venvPath = "." +venv = ".venv" +extraPaths = ["."] +pythonVersion = "3.11" + +[tool.basedpyright] +venvPath = "." +venv = ".venv" +extraPaths = ["."] diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json new file mode 100644 index 000000000..bfe1b4132 --- /dev/null +++ b/backend/pyrightconfig.json @@ -0,0 +1,5 @@ +{ + "venvPath": ".", + "venv": ".venv", + "extraPaths": ["."] +} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 000000000..69d85f7fa Binary files /dev/null and b/backend/requirements.txt differ diff --git a/backend/scripts/seed_user.py b/backend/scripts/seed_user.py new file mode 100644 index 000000000..df3361dc3 --- /dev/null +++ b/backend/scripts/seed_user.py @@ -0,0 +1,79 @@ +import sys +from pathlib import Path + +BACKEND_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(BACKEND_DIR)) + +from passlib.context import CryptContext +from sqlalchemy.orm import Session + +from app.db.session import SessionLocal +from app.modules.auth.models import User, UserRole + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +SEED_USERS = [ + { + "name": "Alan User", + "email": "alan@gmail.com", + "role": UserRole.USER.value, + "is_active": True, + }, + { + "name": "Venue Owner", + "email": "owner@bookmyvenue.com", + "role": UserRole.OWNER.value, + "is_active": True, + }, + { + "name": "Disabled User", + "email": "disabled@test.com", + "role": UserRole.USER.value, + "is_active": False, + }, +] + +DEFAULT_PASSWORD = "123456" + + +def hash_password(plain_password: str) -> str: + return pwd_context.hash(plain_password) + + +def seed_users(db: Session) -> None: + password_hash = hash_password(DEFAULT_PASSWORD) + + for user_data in SEED_USERS: + existing = db.query(User).filter(User.email == user_data["email"]).first() + if existing: + print(f"Skip (already exists): {user_data['email']}") + continue + + user = User( + name=user_data["name"], + email=user_data["email"], + password_hash=password_hash, + role=user_data["role"], + is_active=user_data["is_active"], + ) + db.add(user) + print(f"Created: {user_data['email']} ({user_data['role']})") + + db.commit() + + +def main() -> None: + db = SessionLocal() + try: + seed_users(db) + print("Seed complete.") + except Exception as exc: + db.rollback() + print(f"Seed failed: {exc}") + raise + finally: + db.close() + + +if __name__ == "__main__": + main() diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/documents/APIDocumentation.md b/documents/APIDocumentation.md new file mode 100644 index 000000000..137f5843d --- /dev/null +++ b/documents/APIDocumentation.md @@ -0,0 +1,1921 @@ +# BookMyVenue — API Documentation + +**Project:** BookMyVenue +**Version:** v1 +**Base URL:** `http://localhost:5000/api/v1` +**Format:** JSON +**Authentication:** JWT Bearer Token + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Conventions](#2-conventions) +3. [Authentication APIs](#3-authentication-apis) +4. [User Profile APIs](#4-user-profile-apis) +5. [Venue APIs](#5-venue-apis) +6. [Amenity APIs](#6-amenity-apis) +7. [Booking APIs](#7-booking-apis) +8. [Owner APIs](#8-owner-apis) +9. [Feedback & Rating APIs](#9-feedback--rating-apis) +10. [Issue APIs](#10-issue-apis) +11. [Admin APIs](#11-admin-apis) +12. [Payment APIs](#12-payment-apis) +13. [Error Reference](#13-error-reference) +14. [Role & Access Matrix](#14-role--access-matrix) + +--- + +## 1. Overview + +BookMyVenue is a venue discovery and booking platform with three roles: + +| Role | Description | +|--------|--------------------------------------------------| +| `user` | Browse venues, create bookings, pay, rate venues | +| `owner`| Manage venues and view bookings for their spaces | +| `admin`| Approve venues, manage users, oversee platform | + +All protected endpoints require: + +```http +Authorization: Bearer +``` + +--- + +## 2. Conventions + +### 2.1 Request headers + +| Header | Required | Description | +|-----------------|----------|--------------------------------------| +| `Content-Type` | Yes* | `application/json` for JSON bodies | +| `Authorization` | Conditional | `Bearer ` for protected routes | + +\* Not required for `GET` requests without a body. + +### 2.2 Date & time formats + +| Field | Format | Example | +|----------------|--------------|----------------| +| Date | `YYYY-MM-DD` | `2026-06-20` | +| Time | `HH:MM` | `18:00` | +| DateTime (ISO) | ISO 8601 | `2026-06-20T18:00:00Z` | + +### 2.3 Standard success response wrapper + +Most endpoints return data directly or use a simple message object: + +```json +{ + "message": "Operation completed successfully", + "data": {} +} +``` + +### 2.4 Standard error response + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Human-readable description", + "details": [ + { + "field": "email", + "message": "Invalid email format" + } + ] + } +} +``` + +### 2.5 Pagination (list endpoints) + +Query parameters: + +| Param | Type | Default | Description | +|---------|---------|---------|--------------------| +| `page` | integer | `1` | Page number | +| `limit` | integer | `20` | Items per page (max 100) | + +Paginated response: + +```json +{ + "data": [], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 150, + "total_pages": 8 + } +} +``` + +### 2.6 Roles + +Valid role values: `user`, `owner`, `admin` + +During registration, only `user` or `owner` can be self-selected. `admin` accounts are created internally. + +### 2.7 API ↔ Database field mapping + +JSON request/response fields use the **same names** as database columns unless noted as derived below. + +| API field | DB table.column | Notes | +|-----------|-----------------|-------| +| `name`, `email`, `mobile`, `role`, `is_active` | `users.*` | `password` in requests → `users.password_hash` (hashed) | +| `business_name`, `phone` | `owner_profiles.*` | Owner profile endpoints only | +| `price_per_day` | `venues.price_per_day` | Venue price (INR per day) | +| `approval_status` | `venues.approval_status` | `pending`, `approved`, `rejected` | +| `rejection_reason` | `venues.rejection_reason` | Set on admin reject | +| `average_rating`, `total_reviews` | `venues.*` | Denormalized from `venue_ratings` | +| `is_active` | `venues.is_active` | `false` when admin blocks venue | +| `image_url` | `venue_images.image_url` | | +| `display_order` | `venue_images.display_order` | | +| `amenity_ids` | `venue_amenities.amenity_id` | Array in request; rows in join table | +| `booking_date`, `time_slot`, `notes`, `amount`, `status` | `bookings.*` | | +| `cancellation_reason` | `bookings.cancellation_reason` | Cancel booking request body | +| `cancelled_at` | `bookings.cancelled_at` | Set when status → `cancelled` | +| `payment_id` | `payments.payment_id` | External payment ID string | +| `currency`, `gateway`, `gateway_*` | `payments.*` | | +| `payment_status` | `payments.status` | **Derived** — latest payment for booking (not on `bookings`) | +| `refund_id`, `refund` status fields | `refunds.*` | `refund_status` in cancel response = `refunds.status` | +| `rating`, `comment` | `venue_ratings.*` | | +| `message` | `venue_feedback.message` | Feedback endpoint only | +| `subject`, `description`, `admin_note` | `issues.*` | Issue `status`: `open`, `in_progress`, `resolved`, `closed` | +| `thumbnail_url` | — | **Derived** — first `venue_images.image_url` by `display_order` | + +### 2.8 Shared enum values + +Must match [DBDesign.md](./DBDesign.md) constraints exactly. + +| Field | Allowed values | +|-------|----------------| +| `users.role` | `user`, `owner`, `admin` | +| `venues.approval_status` | `pending`, `approved`, `rejected` | +| `bookings.status` | `pending_payment`, `booked`, `cancelled` | +| `payments.status` | `created`, `paid`, `failed`, `refunded`, `refund_pending` | +| `refunds.status` | `refund_pending`, `refunded`, `failed` | +| `issues.status` | `open`, `in_progress`, `resolved`, `closed` | +| `payments.gateway` | `razorpay`, `stripe` | + +--- + +## 3. Authentication APIs + +### 3.1 Register + +Create a new account. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/register` | +| **Auth** | None | + +**Request body** + +```json +{ + "name": "Alan", + "email": "alan@gmail.com", + "password": "123456", + "mobile": "9090900000", + "role": "user" +} +``` + +| Field | Type | Required | Rules | +|------------|--------|----------|------------------------------------| +| `name` | string | Yes | 2–100 characters | +| `email` | string | Yes | Valid email, unique | +| `password` | string | Yes | Min 6 characters | +| `mobile` | string | No | 10-digit phone number | +| `role` | string | Yes | `user` or `owner` | + +**Success response — `201 Created`** + +```json +{ + "message": "User registered successfully", + "data": { + "id": 1, + "name": "Alan", + "email": "alan@gmail.com", + "mobile": "9090900000", + "role": "user", + "is_active": true, + "created_at": "2026-06-01T10:00:00Z" + } +} +``` + +**Error responses** + +| Status | Code | When | +|--------|-------------------|-----------------------------| +| `400` | `VALIDATION_ERROR`| Invalid input | +| `409` | `EMAIL_EXISTS` | Email already registered | + +--- + +### 3.2 Login + +Authenticate and receive tokens. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/login` | +| **Auth** | None | + +**Request body** + +```json +{ + "email": "alan@gmail.com", + "password": "123456" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Login successful", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "bearer", + "expires_in": 1800 + } +} +``` + +| Field | Description | +|-----------------|--------------------------------------| +| `access_token` | JWT used for API requests (30 min) | +| `refresh_token` | Used to obtain a new access token | +| `expires_in` | Access token lifetime in seconds | + +**Error responses** + +| Status | Code | When | +|--------|-----------------|-------------------| +| `401` | `INVALID_CREDENTIALS` | Wrong email/password | +| `403` | `ACCOUNT_DISABLED` | User is inactive | + +--- + +### 3.3 Refresh Token + +Obtain a new access token without re-login. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/refresh` | +| **Auth** | None | + +**Request body** + +```json +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**Success response — `200 OK`** + +```json +{ + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "bearer", + "expires_in": 1800 + } +} +``` + +--- + +### 3.4 Logout + +Invalidate the current refresh token. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/logout` | +| **Auth** | Bearer token | + +**Request body** + +```json +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Logged out successfully" +} +``` + +--- + +### 3.5 Forgot Password + +Request a password reset link. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/forgot-password` | +| **Auth** | None | + +**Request body** + +```json +{ + "email": "alan@gmail.com" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "If the email exists, a reset link has been sent" +} +``` + +--- + +### 3.6 Reset Password + +Set a new password using a reset token. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/auth/reset-password` | +| **Auth** | None | + +**Request body** + +```json +{ + "token": "reset_token_from_email", + "new_password": "newSecurePassword123" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Password reset successfully" +} +``` + +--- + +## 4. User Profile APIs + +### 4.1 Get Current User + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/users/me` | +| **Auth** | Bearer token | + +**Success response — `200 OK`** + +```json +{ + "data": { + "id": 1, + "name": "Alan", + "email": "alan@gmail.com", + "mobile": "9090900000", + "role": "user", + "is_active": true, + "created_at": "2026-06-01T10:00:00Z", + "updated_at": "2026-06-01T10:00:00Z" + } +} +``` + +--- + +### 4.2 Update Profile + +| | | +|---|---| +| **Method** | `PUT` | +| **URL** | `/users/me` | +| **Auth** | Bearer token | + +**Request body** + +```json +{ + "name": "Alan Updated", + "mobile": "9090900001" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Profile updated successfully", + "data": { + "id": 1, + "name": "Alan Updated", + "email": "alan@gmail.com", + "mobile": "9090900001", + "role": "user", + "is_active": true, + "updated_at": "2026-06-10T12:00:00Z" + } +} +``` + +--- + +### 4.3 Change Password + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/users/me/password` | +| **Auth** | Bearer token | + +**Request body** + +```json +{ + "current_password": "123456", + "new_password": "newSecurePassword123" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Password changed successfully" +} +``` + +--- + +## 5. Venue APIs + +### 5.1 Get All Venues (Public) + +Browse approved venues. Supports search and filters. + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/venues` | +| **Auth** | None | + +**Query parameters** + +| Param | Type | Description | +|--------------|---------|--------------------------------------| +| `location` | string | Filter by city/area (partial match) | +| `min_price` | number | Minimum `price_per_day` | +| `max_price` | number | Maximum `price_per_day` | +| `amenity` | string | Filter by amenity name | +| `search` | string | Search name or description | +| `sort` | string | `price_asc`, `price_desc`, `rating` | +| `page` | integer | Page number | +| `limit` | integer | Items per page | + +**Example** + +```http +GET /venues?location=Kochi&min_price=5000&max_price=20000&page=1&limit=10 +``` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 1, + "name": "Grand Hall", + "location": "Kochi", + "price_per_day": 10000, + "approval_status": "approved", + "average_rating": 4.5, + "thumbnail_url": "https://cdn.example.com/venues/1/img1.jpg", + "amenities": ["Wi-Fi", "Parking", "AC"] + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total_items": 45, + "total_pages": 5 + } +} +``` + +--- + +### 5.2 Get Venue Details (Public) + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/venues/:id` | +| **Auth** | None | + +**Example** + +```http +GET /venues/1 +``` + +**Success response — `200 OK`** + +```json +{ + "data": { + "id": 1, + "owner_id": 5, + "name": "Grand Hall", + "location": "Kochi, Kerala", + "price_per_day": 10000, + "description": "Large event venue with stage and seating for 500", + "approval_status": "approved", + "is_active": true, + "average_rating": 4.5, + "total_reviews": 28, + "amenities": [ + { "id": 1, "name": "Wi-Fi" }, + { "id": 2, "name": "Parking" }, + { "id": 3, "name": "AC" } + ], + "images": [ + { "id": 1, "image_url": "https://cdn.example.com/venues/1/img1.jpg", "display_order": 0 }, + { "id": 2, "image_url": "https://cdn.example.com/venues/1/img2.jpg", "display_order": 1 } + ], + "created_at": "2026-05-15T08:00:00Z", + "updated_at": "2026-05-15T08:00:00Z" + } +} +``` + +**Error responses** + +| Status | Code | When | +|--------|----------------|-------------------| +| `404` | `VENUE_NOT_FOUND`| Venue does not exist | + +--- + +### 5.3 Create Venue + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/venues` | +| **Auth** | Bearer token — **Owner only** | + +**Request body** + +```json +{ + "name": "Grand Hall", + "location": "Kochi", + "price_per_day": 10000, + "description": "Large event venue", + "amenity_ids": [1, 2, 3] +} +``` + +| Field | Type | Required | DB column | +|-----------------|----------|----------|------------------------| +| `name` | string | Yes | `venues.name` | +| `location` | string | Yes | `venues.location` | +| `price_per_day` | number | Yes | `venues.price_per_day` | +| `description` | string | No | `venues.description` | +| `amenity_ids` | integer[]| No | `venue_amenities` | + +**Success response — `201 Created`** + +```json +{ + "message": "Venue added successfully. Pending admin approval.", + "data": { + "id": 12, + "name": "Grand Hall", + "location": "Kochi", + "price_per_day": 10000, + "approval_status": "pending", + "is_active": true, + "created_at": "2026-06-09T16:00:00Z" + } +} +``` + +--- + +### 5.4 Update Venue + +| | | +|---|---| +| **Method** | `PUT` | +| **URL** | `/venues/:id` | +| **Auth** | Bearer token — **Owner (own venue) or Admin** | + +**Example** + +```http +PUT /venues/12 +``` + +**Request body** + +```json +{ + "name": "Grand Hall Premium", + "location": "Kochi, Edapally", + "price_per_day": 12000, + "description": "Renovated large event venue", + "amenity_ids": [1, 2, 3, 4] +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Venue updated successfully", + "data": { + "id": 12, + "name": "Grand Hall Premium", + "location": "Kochi, Edapally", + "price_per_day": 12000, + "approval_status": "pending", + "updated_at": "2026-06-10T10:00:00Z" + } +} +``` + +> **Note:** Major updates may reset `approval_status` to `pending`. + +--- + +### 5.5 Delete Venue + +| | | +|---|---| +| **Method** | `DELETE` | +| **URL** | `/venues/:id` | +| **Auth** | Bearer token — **Owner (own venue) or Admin** | + +**Example** + +```http +DELETE /venues/12 +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Venue deleted successfully" +} +``` + +**Error responses** + +| Status | Code | When | +|--------|-----------------------|-----------------------------------| +| `403` | `FORBIDDEN` | Not the venue owner | +| `409` | `ACTIVE_BOOKINGS_EXIST`| Venue has upcoming bookings | + +--- + +### 5.6 Get My Venues (Owner) + +List venues owned by the authenticated user. + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/venues/my-venues` | +| **Auth** | Bearer token — **Owner only** | + +**Query parameters:** `page`, `limit`, `approval_status` (`approved`, `pending`, `rejected`) + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 12, + "name": "Grand Hall", + "location": "Kochi", + "price_per_day": 10000, + "approval_status": "pending", + "is_active": true, + "total_bookings": 5, + "created_at": "2026-06-09T16:00:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 3, + "total_pages": 1 + } +} +``` + +--- + +### 5.7 Upload Venue Images + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/venues/:id/images` | +| **Auth** | Bearer token — **Owner (own venue)** | +| **Content-Type** | `multipart/form-data` | + +**Form fields** + +| Field | Type | Required | Description | +|---------|------|----------|--------------------| +| `images`| file | Yes | One or more images (max 5 MB each, JPG/PNG/WebP) | + +**Success response — `201 Created`** + +```json +{ + "message": "Images uploaded successfully", + "data": { + "images": [ + { "id": 10, "image_url": "https://cdn.example.com/venues/12/img10.jpg", "display_order": 0 }, + { "id": 11, "image_url": "https://cdn.example.com/venues/12/img11.jpg", "display_order": 1 } + ] + } +} +``` + +--- + +### 5.8 Delete Venue Image + +| | | +|---|---| +| **Method** | `DELETE` | +| **URL** | `/venues/:id/images/:image_id` | +| **Auth** | Bearer token — **Owner (own venue)** | + +**Success response — `200 OK`** + +```json +{ + "message": "Image deleted successfully" +} +``` + +--- + +### 5.9 Check Venue Availability + +Check if a venue is available on a given date and time slot. + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/venues/:id/availability` | +| **Auth** | None | + +**Query parameters** + +| Param | Type | Required | Description | +|----------------|--------|----------|-----------------| +| `booking_date` | string | Yes | `YYYY-MM-DD` | +| `time_slot` | string | No | `HH:MM` | + +**Example** + +```http +GET /venues/1/availability?booking_date=2026-06-20&time_slot=18:00 +``` + +**Success response — `200 OK`** + +```json +{ + "data": { + "venue_id": 1, + "booking_date": "2026-06-20", + "time_slot": "18:00", + "is_available": true + } +} +``` + +--- + +## 6. Amenity APIs + +### 6.1 Get All Amenities + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/amenities` | +| **Auth** | None | + +**Success response — `200 OK`** + +```json +{ + "data": [ + { "id": 1, "name": "Wi-Fi" }, + { "id": 2, "name": "Parking" }, + { "id": 3, "name": "AC" }, + { "id": 4, "name": "Projector" } + ] +} +``` + +--- + +## 7. Booking APIs + +### 7.1 Create Booking + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/bookings` | +| **Auth** | Bearer token — **User only** | + +**Request body** + +```json +{ + "venue_id": 1, + "booking_date": "2026-06-20", + "time_slot": "18:00", + "notes": "Birthday party setup needed" +} +``` + +| Field | Type | Required | DB column | +|----------------|--------|----------|--------------------------| +| `venue_id` | integer| Yes | `bookings.venue_id` | +| `booking_date` | string | Yes | `bookings.booking_date` | +| `time_slot` | string | Yes | `bookings.time_slot` | +| `notes` | string | No | `bookings.notes` | + +**Success response — `201 Created`** + +```json +{ + "message": "Booking created", + "data": { + "id": 101, + "venue_id": 1, + "venue_name": "Grand Hall", + "booking_date": "2026-06-20", + "time_slot": "18:00", + "status": "pending_payment", + "amount": 10000, + "created_at": "2026-06-10T14:30:00Z" + } +} +``` + +> After creating a booking, proceed to [Payment APIs](#12-payment-apis) to complete payment. Booking status becomes `booked` only after successful payment. + +--- + +### 7.2 Get My Bookings + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/bookings/my-bookings` | +| **Auth** | Bearer token — **User only** | + +**Query parameters:** `status` (`booked`, `cancelled`, `pending_payment`), `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 101, + "venue_id": 1, + "venue_name": "Grand Hall", + "venue_location": "Kochi", + "booking_date": "2026-06-20", + "time_slot": "18:00", + "status": "booked", + "amount": 10000, + "payment_status": "paid", + "created_at": "2026-06-10T14:30:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 5, + "total_pages": 1 + } +} +``` + +--- + +### 7.3 Get Booking Details + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/bookings/:id` | +| **Auth** | Bearer token — **User (own booking), Owner (their venue), or Admin** | + +**Success response — `200 OK`** + +```json +{ + "data": { + "id": 101, + "user_id": 1, + "user_name": "Alan", + "venue_id": 1, + "venue_name": "Grand Hall", + "booking_date": "2026-06-20", + "time_slot": "18:00", + "status": "booked", + "amount": 10000, + "notes": "Birthday party setup needed", + "cancellation_reason": null, + "cancelled_at": null, + "payment_status": "paid", + "payment": { + "payment_id": "pay_abc123", + "status": "paid", + "paid_at": "2026-06-10T14:35:00Z" + }, + "created_at": "2026-06-10T14:30:00Z" + } +} +``` + +--- + +### 7.4 Cancel Booking + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/bookings/:id/cancel` | +| **Auth** | Bearer token — **User (own booking)** | + +**Request body (optional)** + +```json +{ + "cancellation_reason": "Schedule conflict" +} +``` + +| Field | Type | Required | DB column | +|-----------------------|--------|----------|--------------------------------| +| `cancellation_reason` | string | No | `bookings.cancellation_reason` | + +**Success response — `200 OK`** + +```json +{ + "message": "Booking cancelled successfully", + "data": { + "id": 101, + "status": "cancelled", + "cancellation_reason": "Schedule conflict", + "cancelled_at": "2026-06-11T09:00:00Z", + "refund_status": "refund_pending" + } +} +``` + +> `refund_status` maps to `refunds.status` when a refund row is created for the paid booking. + +> If payment was completed, a refund is initiated automatically. See [Refund Payment](#124-refund-payment). + +**Error responses** + +| Status | Code | When | +|--------|-------------------------|-----------------------------| +| `400` | `CANCELLATION_NOT_ALLOWED`| Booking date has passed | +| `404` | `BOOKING_NOT_FOUND` | Invalid booking ID | + +--- + +## 8. Owner APIs + +### 8.1 Get Bookings for My Venues + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/owner/bookings` | +| **Auth** | Bearer token — **Owner only** | + +**Query parameters:** `venue_id`, `status`, `booking_date`, `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 101, + "venue_id": 12, + "venue_name": "Grand Hall", + "user_name": "Alan", + "user_email": "alan@gmail.com", + "booking_date": "2026-06-20", + "time_slot": "18:00", + "status": "booked", + "amount": 10000, + "payment_status": "paid" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 8, + "total_pages": 1 + } +} +``` + +--- + +### 8.2 Get Owner Dashboard Stats + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/owner/dashboard` | +| **Auth** | Bearer token — **Owner only** | + +**Success response — `200 OK`** + +```json +{ + "data": { + "total_venues": 3, + "approved_venues": 2, + "pending_venues": 1, + "total_bookings": 25, + "upcoming_bookings": 8, + "total_revenue": 250000, + "average_rating": 4.3 + } +} +``` + +--- + +### 8.3 Update Owner Profile + +| | | +|---|---| +| **Method** | `PUT` | +| **URL** | `/owner/profile` | +| **Auth** | Bearer token — **Owner only** | + +**Request body** + +```json +{ + "business_name": "Alan Events Pvt Ltd", + "phone": "9090900000" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Owner profile updated successfully", + "data": { + "business_name": "Alan Events Pvt Ltd", + "phone": "9090900000" + } +} +``` + +--- + +## 9. Feedback & Rating APIs + +### 9.1 Submit Venue Rating + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/venues/:id/ratings` | +| **Auth** | Bearer token — **User only** | + +**Request body** + +```json +{ + "rating": 5, + "comment": "Excellent venue, great staff!" +} +``` + +| Field | Type | Required | DB column | +|-----------|---------|----------|------------------------| +| `rating` | integer | Yes | `venue_ratings.rating` (1–5) | +| `comment` | string | No | `venue_ratings.comment` (max 500) | + +**Success response — `201 Created`** + +```json +{ + "message": "Rating submitted successfully", + "data": { + "id": 50, + "venue_id": 1, + "rating": 5, + "comment": "Excellent venue, great staff!", + "created_at": "2026-06-21T10:00:00Z" + } +} +``` + +--- + +### 9.2 Get Venue Ratings + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/venues/:id/ratings` | +| **Auth** | None | + +**Query parameters:** `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": { + "average_rating": 4.5, + "total_reviews": 28, + "reviews": [ + { + "id": 50, + "user_name": "Alan", + "rating": 5, + "comment": "Excellent venue, great staff!", + "created_at": "2026-06-21T10:00:00Z" + } + ] + }, + "pagination": { + "page": 1, + "limit": 20, + "total_items": 28, + "total_pages": 2 + } +} +``` + +--- + +### 9.3 Submit Venue Feedback + +General feedback (not a star rating). + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/venues/:id/feedback` | +| **Auth** | Bearer token — **User only** | + +**Request body** + +```json +{ + "message": "Would be great to have more parking space." +} +``` + +**Success response — `201 Created`** + +```json +{ + "message": "Feedback submitted successfully" +} +``` + +--- + +## 10. Issue APIs + +### 10.1 Raise an Issue + +Report a problem with a venue or booking. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/issues` | +| **Auth** | Bearer token — **User only** | + +**Request body** + +```json +{ + "venue_id": 1, + "booking_id": 101, + "subject": "Venue not as described", + "description": "The venue did not have the promised AV equipment." +} +``` + +| Field | Type | Required | DB column | +|---------------|---------|----------|---------------------| +| `venue_id` | integer | Yes | `issues.venue_id` | +| `booking_id` | integer | No | `issues.booking_id` | +| `subject` | string | Yes | `issues.subject` | +| `description` | string | Yes | `issues.description`| + +**Success response — `201 Created`** + +```json +{ + "message": "Issue raised successfully", + "data": { + "id": 7, + "status": "open", + "created_at": "2026-06-21T12:00:00Z" + } +} +``` + +--- + +### 10.2 Get My Issues + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/issues/my-issues` | +| **Auth** | Bearer token — **User only** | + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 7, + "venue_id": 1, + "venue_name": "Grand Hall", + "subject": "Venue not as described", + "status": "open", + "created_at": "2026-06-21T12:00:00Z" + } + ] +} +``` + +--- + +### 10.3 Get All Issues (Admin) + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/issues` | +| **Auth** | Bearer token — **Admin only** | + +**Query parameters:** `status` (`open`, `in_progress`, `resolved`, `closed`), `page`, `limit` + +--- + +### 10.4 Update Issue Status (Admin) + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/admin/issues/:id` | +| **Auth** | Bearer token — **Admin only** | + +**Request body** + +```json +{ + "status": "resolved", + "admin_note": "Contacted owner, issue addressed." +} +``` + +| Field | Type | Required | DB column | +|--------------|--------|----------|------------------------| +| `status` | string | Yes | `issues.status` | +| `admin_note` | string | No | `issues.admin_note` | + +When `status` is `resolved`, the API sets `issues.resolved_at` to the current timestamp. + +--- + +## 11. Admin APIs + +All admin endpoints require `Authorization: Bearer ` with role `admin`. + +--- + +### 11.1 Get Pending Venues + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/pending-venues` | +| **Auth** | Admin | + +**Query parameters:** `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 12, + "owner_id": 5, + "owner_name": "Venue Owner", + "name": "Grand Hall", + "location": "Kochi", + "price_per_day": 10000, + "approval_status": "pending", + "created_at": "2026-06-09T16:00:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 4, + "total_pages": 1 + } +} +``` + +--- + +### 11.2 Approve Venue + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/admin/venues/:id/approve` | +| **Auth** | Admin | + +**Example** + +```http +PATCH /admin/venues/12/approve +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Venue approved", + "data": { + "id": 12, + "approval_status": "approved", + "updated_at": "2026-06-10T11:00:00Z" + } +} +``` + +Sets `venues.approval_status = 'approved'`. + +--- + +### 11.3 Reject Venue + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/admin/venues/:id/reject` | +| **Auth** | Admin | + +**Request body (optional)** + +```json +{ + "rejection_reason": "Incomplete venue information or misleading photos" +} +``` + +| Field | Type | Required | DB column | +|--------------------|--------|----------|---------------------------| +| `rejection_reason` | string | No | `venues.rejection_reason` | + +**Success response — `200 OK`** + +```json +{ + "message": "Venue rejected", + "data": { + "id": 12, + "approval_status": "rejected", + "rejection_reason": "Incomplete venue information or misleading photos", + "updated_at": "2026-06-10T11:00:00Z" + } +} +``` + +--- + +### 11.4 Get All Users + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/users` | +| **Auth** | Admin | + +**Query parameters:** `role`, `is_active`, `search`, `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "id": 1, + "name": "Alan", + "email": "alan@gmail.com", + "role": "user", + "is_active": true, + "created_at": "2026-06-01T10:00:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 120, + "total_pages": 6 + } +} +``` + +--- + +### 11.5 Get User by ID + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/users/:id` | +| **Auth** | Admin | + +--- + +### 11.6 Deactivate User + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/admin/users/:id/deactivate` | +| **Auth** | Admin | + +**Success response — `200 OK`** + +```json +{ + "message": "User deactivated successfully" +} +``` + +--- + +### 11.7 Activate User + +| | | +|---|---| +| **Method** | `PATCH` | +| **URL** | `/admin/users/:id/activate` | +| **Auth** | Admin | + +--- + +### 11.8 Get All Venues (Admin) + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/venues` | +| **Auth** | Admin | + +**Query parameters:** `approval_status`, `owner_id`, `page`, `limit` + +--- + +### 11.9 Remove / Block Venue + +| | | +|---|---| +| **Method** | `DELETE` | +| **URL** | `/admin/venues/:id` | +| **Auth** | Admin | + +Sets `venues.is_active = false` (soft block). Venue row is retained for audit. + +**Success response — `200 OK`** + +```json +{ + "message": "Venue removed from platform", + "data": { + "id": 12, + "is_active": false, + "updated_at": "2026-06-10T12:00:00Z" + } +} +``` + +--- + +### 11.10 Admin Dashboard Stats + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/admin/dashboard` | +| **Auth** | Admin | + +**Success response — `200 OK`** + +```json +{ + "data": { + "total_users": 120, + "total_owners": 35, + "total_venues": 80, + "pending_venues": 4, + "total_bookings": 450, + "total_revenue": 4500000, + "open_issues": 3 + } +} +``` + +--- + +## 12. Payment APIs + +Payment integration supports online booking checkout. Bookings start as `pending_payment` and move to `booked` after successful payment. + +**Supported gateways (planned):** Razorpay / Stripe + +--- + +### 12.1 Create Payment Order + +Initiate payment for a booking. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/payments/create-order` | +| **Auth** | Bearer token — **User only** | + +**Request body** + +```json +{ + "booking_id": 101, + "currency": "INR" +} +``` + +| Field | Type | Required | Description | +|--------------|---------|----------|--------------------------| +| `booking_id` | integer | Yes | Booking to pay for | +| `currency` | string | No | Default: `INR` | + +**Success response — `201 Created`** + +```json +{ + "message": "Payment order created", + "data": { + "payment_id": "pay_abc123", + "booking_id": 101, + "amount": 10000, + "currency": "INR", + "gateway": "razorpay", + "gateway_order_id": "order_Mxyz123", + "status": "created", + "expires_at": "2026-06-10T15:00:00Z" + } +} +``` + +Use `gateway_order_id` on the frontend to open the payment gateway checkout. + +--- + +### 12.2 Verify Payment + +Confirm payment after the user completes checkout on the gateway. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/payments/verify` | +| **Auth** | Bearer token — **User only** | + +**Request body (Razorpay example)** + +```json +{ + "payment_id": "pay_abc123", + "gateway_order_id": "order_Mxyz123", + "gateway_payment_id": "pay_Rxyz456", + "gateway_signature": "signature_from_gateway" +} +``` + +**Success response — `200 OK`** + +```json +{ + "message": "Payment verified successfully", + "data": { + "payment_id": "pay_abc123", + "booking_id": 101, + "status": "paid", + "amount": 10000, + "paid_at": "2026-06-10T14:35:00Z", + "booking_status": "booked" + } +} +``` + +**Error responses** + +| Status | Code | When | +|--------|-------------------|-------------------------------| +| `400` | `PAYMENT_FAILED` | Gateway reported failure | +| `400` | `INVALID_SIGNATURE`| Signature verification failed| +| `409` | `ALREADY_PAID` | Booking already paid | + +--- + +### 12.3 Get Payment Details + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/payments/:payment_id` | +| **Auth** | Bearer token — **User (own payment), Owner, or Admin** | + +**Success response — `200 OK`** + +```json +{ + "data": { + "payment_id": "pay_abc123", + "booking_id": 101, + "user_id": 1, + "venue_name": "Grand Hall", + "amount": 10000, + "currency": "INR", + "status": "paid", + "gateway": "razorpay", + "gateway_payment_id": "pay_Rxyz456", + "paid_at": "2026-06-10T14:35:00Z", + "created_at": "2026-06-10T14:30:00Z" + } +} +``` + +Payment status values: `created`, `paid`, `failed`, `refunded`, `refund_pending` + +--- + +### 12.4 Get My Payments + +| | | +|---|---| +| **Method** | `GET` | +| **URL** | `/payments/my-payments` | +| **Auth** | Bearer token — **User only** | + +**Query parameters:** `status`, `page`, `limit` + +**Success response — `200 OK`** + +```json +{ + "data": [ + { + "payment_id": "pay_abc123", + "booking_id": 101, + "venue_name": "Grand Hall", + "amount": 10000, + "status": "paid", + "paid_at": "2026-06-10T14:35:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 3, + "total_pages": 1 + } +} +``` + +--- + +### 12.5 Refund Payment + +Triggered automatically on booking cancellation, or manually by admin. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/payments/:payment_id/refund` | +| **Auth** | Bearer token — **Admin**, or **User** (own payment via cancelled booking) | + +**Request body** + +```json +{ + "reason": "Booking cancelled by user", + "amount": 10000 +} +``` + +| Field | Type | Required | DB column | +|----------|--------|----------|------------------| +| `reason` | string | Yes | `refunds.reason` | +| `amount` | number | No | `refunds.amount` (full payment amount if omitted) | + +**Success response — `200 OK`** + +```json +{ + "message": "Refund initiated successfully", + "data": { + "payment_id": "pay_abc123", + "refund_id": "rfnd_xyz789", + "amount": 10000, + "status": "refund_pending", + "created_at": "2026-06-11T09:05:00Z" + } +} +``` + +> On refund initiation, `payments.status` is updated to `refund_pending`. When complete, `refunds.status` → `refunded` and `payments.status` → `refunded`. + +--- + +### 12.6 Payment Webhook (Gateway → Server) + +Called by the payment gateway — not by the frontend. + +| | | +|---|---| +| **Method** | `POST` | +| **URL** | `/payments/webhook` | +| **Auth** | Gateway signature verification | + +**Headers** + +```http +X-Razorpay-Signature: +``` + +**Request body** + +Gateway-specific payload (e.g. `payment.captured`, `payment.failed`, `refund.processed`). + +**Success response — `200 OK`** + +```json +{ + "message": "Webhook processed" +} +``` + +--- + +### 12.7 Payment Flow (End-to-End) + +```text +1. User creates booking → POST /bookings (status: pending_payment) +2. User initiates payment → POST /payments/create-order +3. Frontend opens gateway checkout using gateway_order_id +4. User pays on gateway +5. Frontend verifies payment → POST /payments/verify +6. Booking confirmed → status: booked +7. (Optional) Gateway webhook → POST /payments/webhook (server-side confirmation) +``` + +--- + +## 13. Error Reference + +| HTTP Status | Error Code | Description | +|-------------|-------------------------|------------------------------------| +| `400` | `VALIDATION_ERROR` | Invalid request body or params | +| `401` | `UNAUTHORIZED` | Missing or invalid token | +| `401` | `INVALID_CREDENTIALS` | Wrong email or password | +| `403` | `FORBIDDEN` | Insufficient role/permission | +| `403` | `ACCOUNT_DISABLED` | User account is inactive | +| `404` | `NOT_FOUND` | Resource does not exist | +| `404` | `VENUE_NOT_FOUND` | Venue does not exist | +| `404` | `BOOKING_NOT_FOUND` | Booking does not exist | +| `409` | `EMAIL_EXISTS` | Email already registered | +| `409` | `ALREADY_BOOKED` | Venue/date slot already taken | +| `409` | `ALREADY_PAID` | Payment already completed | +| `409` | `ACTIVE_BOOKINGS_EXIST` | Cannot delete venue with bookings | +| `422` | `UNPROCESSABLE` | Business rule violation | +| `500` | `INTERNAL_ERROR` | Unexpected server error | + +--- + +## 14. Role & Access Matrix + +| Endpoint | Public | User | Owner | Admin | +|---------------------------------------|:------:|:----:|:-----:|:-----:| +| `POST /auth/register` | ✅ | — | — | — | +| `POST /auth/login` | ✅ | — | — | — | +| `GET /venues` | ✅ | ✅ | ✅ | ✅ | +| `GET /venues/:id` | ✅ | ✅ | ✅ | ✅ | +| `POST /venues` | — | — | ✅ | ✅ | +| `PUT /venues/:id` | — | — | ✅* | ✅ | +| `DELETE /venues/:id` | — | — | ✅* | ✅ | +| `GET /venues/my-venues` | — | — | ✅ | — | +| `POST /bookings` | — | ✅ | — | — | +| `GET /bookings/my-bookings` | — | ✅ | — | — | +| `PATCH /bookings/:id/cancel` | — | ✅* | — | — | +| `GET /owner/bookings` | — | — | ✅ | — | +| `POST /venues/:id/ratings` | — | ✅ | — | — | +| `POST /issues` | — | ✅ | — | — | +| `POST /payments/create-order` | — | ✅ | — | — | +| `POST /payments/verify` | — | ✅ | — | — | +| `GET /payments/my-payments` | — | ✅ | — | — | +| `GET /admin/pending-venues` | — | — | — | ✅ | +| `PATCH /admin/venues/:id/approve` | — | — | — | ✅ | +| `PATCH /admin/venues/:id/reject` | — | — | — | ✅ | +| `GET /admin/users` | — | — | — | ✅ | +| `DELETE /admin/venues/:id` | — | — | — | ✅ | + +\* Own resource only + +--- + +## Related Documents + +- [Product Requirements (PRD)](./PRD.md) +- [Database Design](./DBDesign.md) +- [System Design](./SystemDesign.md) +- [Folder Architecture](./FolderArchitecture.md) +- [Backend Setup](../backend/README.md) + +--- + +**Last updated:** June 2026 diff --git a/documents/DBDesign.md b/documents/DBDesign.md new file mode 100644 index 000000000..4552ca4e4 --- /dev/null +++ b/documents/DBDesign.md @@ -0,0 +1,855 @@ +# BookMyVenue — Database Design (PostgreSQL) + +**Project:** BookMyVenue +**Database:** PostgreSQL 15+ +**ORM:** SQLAlchemy (FastAPI backend) + +This document defines all tables, relationships, constraints, and indexes for the platform. It aligns with the [API Documentation](./APIDocumentation.md). + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Entity Relationship Diagram](#2-entity-relationship-diagram) +3. [Tables](#3-tables) + - [users](#31-users) + - [refresh_tokens](#32-refresh_tokens) + - [password_reset_tokens](#33-password_reset_tokens) + - [owner_profiles](#34-owner_profiles) + - [venues](#35-venues) + - [venue_images](#36-venue_images) + - [amenities](#37-amenities) + - [venue_amenities](#38-venue_amenities) + - [bookings](#39-bookings) + - [payments](#310-payments) + - [refunds](#311-refunds) + - [venue_ratings](#312-venue_ratings) + - [venue_feedback](#313-venue_feedback) + - [issues](#314-issues) +4. [API ↔ Database Field Mapping](#4-api--database-field-mapping) +5. [Relationships Summary](#5-relationships-summary) +6. [Indexes](#6-indexes) +7. [Enums & Status Values](#7-enums--status-values) +8. [Sample Seed Data](#8-sample-seed-data) +9. [Migration Notes](#9-migration-notes) + +--- + +## 1. Overview + +| Domain | Tables | +|---------------|-----------------------------------------------------| +| Auth & Users | `users`, `refresh_tokens`, `password_reset_tokens`, `owner_profiles` | +| Venues | `venues`, `venue_images`, `amenities`, `venue_amenities` | +| Bookings | `bookings` | +| Payments | `payments`, `refunds` | +| Feedback | `venue_ratings`, `venue_feedback`, `issues` | + +**Design principles** + +- Use `SERIAL` / `BIGSERIAL` primary keys for internal IDs. +- Use UUID or prefixed string IDs (`pay_abc123`) for external-facing payment IDs. +- Store passwords as bcrypt hashes — never plain text. +- Use `TIMESTAMP WITH TIME ZONE` for all datetime columns. +- Soft-delete is not used in MVP; use `is_active` on users and status fields elsewhere. + +--- + +## 2. Entity Relationship Diagram + +```mermaid +erDiagram + users ||--o{ refresh_tokens : has + users ||--o{ password_reset_tokens : has + users ||--o| owner_profiles : has + users ||--o{ venues : owns + users ||--o{ bookings : makes + users ||--o{ venue_ratings : writes + users ||--o{ venue_feedback : writes + users ||--o{ issues : raises + users ||--o{ payments : pays + + venues ||--o{ venue_images : has + venues ||--o{ venue_amenities : has + venues ||--o{ bookings : receives + venues ||--o{ venue_ratings : receives + venues ||--o{ venue_feedback : receives + venues ||--o{ issues : related_to + + amenities ||--o{ venue_amenities : tagged_on + + bookings ||--o{ payments : has + bookings ||--o{ issues : related_to + + payments ||--o{ refunds : has + + users { + serial id PK + varchar name + varchar email UK + varchar mobile + text password_hash + varchar role + boolean is_active + } + + venues { + serial id PK + int owner_id FK + varchar name + varchar location + numeric price_per_day + varchar approval_status + } + + bookings { + serial id PK + int user_id FK + int venue_id FK + date booking_date + time time_slot + varchar status + numeric amount + } + + payments { + serial id PK + varchar payment_id UK + int booking_id FK + int user_id FK + numeric amount + varchar status + } +``` + +--- + +## 3. Tables + +### 3.1 `users` + +Stores all platform users: normal users, venue owners, and admins. + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(150) NOT NULL UNIQUE, + mobile VARCHAR(20), + password_hash TEXT NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'user' + CHECK (role IN ('user', 'owner', 'admin')), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|-----------------|----------------|----------|------------------------------------------| +| `id` | SERIAL | No | Primary key | +| `name` | VARCHAR(100) | No | Full name | +| `email` | VARCHAR(150) | No | Unique login email | +| `mobile` | VARCHAR(20) | Yes | Phone number | +| `password_hash` | TEXT | No | Bcrypt-hashed password | +| `role` | VARCHAR(20) | No | `user`, `owner`, or `admin` | +| `is_active` | BOOLEAN | No | `false` when admin deactivates account | +| `created_at` | TIMESTAMPTZ | No | Account creation time | +| `updated_at` | TIMESTAMPTZ | No | Last profile update | + +--- + +### 3.2 `refresh_tokens` + +Stores refresh tokens for JWT session management and logout invalidation. + +```sql +CREATE TABLE refresh_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|--------------|-------------|----------|---------------------------------------| +| `id` | SERIAL | No | Primary key | +| `user_id` | INTEGER | No | Token owner | +| `token_hash` | TEXT | No | Hashed refresh token (never store raw)| +| `expires_at` | TIMESTAMPTZ | No | Token expiry | +| `revoked_at` | TIMESTAMPTZ | Yes | Set on logout | +| `created_at` | TIMESTAMPTZ | No | Issue time | + +--- + +### 3.3 `password_reset_tokens` + +Temporary tokens for forgot-password flow. + +```sql +CREATE TABLE password_reset_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|--------------|-------------|----------|----------------------------| +| `id` | SERIAL | No | Primary key | +| `user_id` | INTEGER | No | User requesting reset | +| `token_hash` | TEXT | No | Hashed reset token | +| `expires_at` | TIMESTAMPTZ | No | Typically 1 hour validity | +| `used_at` | TIMESTAMPTZ | Yes | Set when password changed | +| `created_at` | TIMESTAMPTZ | No | Token creation time | + +--- + +### 3.4 `owner_profiles` + +Extended profile data for venue owners. + +```sql +CREATE TABLE owner_profiles ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + business_name VARCHAR(150), + phone VARCHAR(20), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|-----------------|--------------|----------|--------------------------| +| `id` | SERIAL | No | Primary key | +| `user_id` | INTEGER | No | One profile per owner | +| `business_name` | VARCHAR(150) | Yes | Registered business name | +| `phone` | VARCHAR(20) | Yes | Business contact number | +| `created_at` | TIMESTAMPTZ | No | Record creation | +| `updated_at` | TIMESTAMPTZ | No | Last update | + +--- + +### 3.5 `venues` + +Venue listings created by owners. Requires admin approval before going public. + +```sql +CREATE TABLE venues ( + id SERIAL PRIMARY KEY, + owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(150) NOT NULL, + location VARCHAR(255) NOT NULL, + price_per_day NUMERIC(10, 2) NOT NULL CHECK (price_per_day >= 0), + description TEXT, + approval_status VARCHAR(20) NOT NULL DEFAULT 'pending' + CHECK (approval_status IN ('pending', 'approved', 'rejected')), + rejection_reason TEXT, + average_rating NUMERIC(3, 2) DEFAULT 0.00 CHECK (average_rating >= 0 AND average_rating <= 5), + total_reviews INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|---------------------|----------------|----------|------------------------------------------| +| `id` | SERIAL | No | Primary key | +| `owner_id` | INTEGER | No | FK → `users.id` (owner role) | +| `name` | VARCHAR(150) | No | Venue display name | +| `location` | VARCHAR(255) | No | City / address | +| `price_per_day` | NUMERIC(10,2) | No | Daily rental price (INR) | +| `description` | TEXT | Yes | Full venue description | +| `approval_status` | VARCHAR(20) | No | `pending`, `approved`, `rejected` | +| `rejection_reason` | TEXT | Yes | Admin reason when rejected | +| `average_rating` | NUMERIC(3,2) | Yes | Denormalized avg rating (updated on review) | +| `total_reviews` | INTEGER | No | Count of ratings | +| `is_active` | BOOLEAN | No | `false` when admin blocks/removes venue | +| `created_at` | TIMESTAMPTZ | No | Submission time | +| `updated_at` | TIMESTAMPTZ | No | Last edit time | + +> Public venue queries should filter: `approval_status = 'approved' AND is_active = TRUE`. + +--- + +### 3.6 `venue_images` + +Multiple images per venue. + +```sql +CREATE TABLE venue_images ( + id SERIAL PRIMARY KEY, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + image_url TEXT NOT NULL, + display_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|-----------------|-------------|----------|--------------------------------| +| `id` | SERIAL | No | Primary key | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `image_url` | TEXT | No | CDN / storage URL | +| `display_order` | INTEGER | No | Sort order in gallery (0 first)| +| `created_at` | TIMESTAMPTZ | No | Upload time | + +--- + +### 3.7 `amenities` + +Master list of venue amenities (Wi-Fi, Parking, AC, etc.). + +```sql +CREATE TABLE amenities ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|--------------|--------------|----------|-----------------------| +| `id` | SERIAL | No | Primary key | +| `name` | VARCHAR(100) | No | Unique amenity label | +| `created_at` | TIMESTAMPTZ | No | Record creation | + +--- + +### 3.8 `venue_amenities` + +Many-to-many join between venues and amenities. + +```sql +CREATE TABLE venue_amenities ( + id SERIAL PRIMARY KEY, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + amenity_id INTEGER NOT NULL REFERENCES amenities(id) ON DELETE CASCADE, + UNIQUE (venue_id, amenity_id) +); +``` + +| Column | Type | Nullable | Description | +|--------------|---------|----------|----------------------| +| `id` | SERIAL | No | Primary key | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `amenity_id` | INTEGER | No | FK → `amenities.id` | + +--- + +### 3.9 `bookings` + +Core booking records linking users to venues. + +```sql +CREATE TABLE bookings ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + booking_date DATE NOT NULL, + time_slot TIME NOT NULL, + notes TEXT, + amount NUMERIC(10, 2) NOT NULL CHECK (amount >= 0), + status VARCHAR(20) NOT NULL DEFAULT 'pending_payment' + CHECK (status IN ('pending_payment', 'booked', 'cancelled')), + cancellation_reason TEXT, + cancelled_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (venue_id, booking_date, time_slot) +); +``` + +| Column | Type | Nullable | Description | +|-----------------------|---------------|----------|------------------------------------------| +| `id` | SERIAL | No | Primary key | +| `user_id` | INTEGER | No | FK → `users.id` (booker) | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `booking_date` | DATE | No | Event date | +| `time_slot` | TIME | No | Start time (24-hour) | +| `notes` | TEXT | Yes | Special requests from user | +| `amount` | NUMERIC(10,2) | No | Booking price (copied from venue at creation) | +| `status` | VARCHAR(20) | No | `pending_payment`, `booked`, `cancelled` | +| `cancellation_reason` | TEXT | Yes | Reason provided on cancel | +| `cancelled_at` | TIMESTAMPTZ | Yes | When booking was cancelled | +| `created_at` | TIMESTAMPTZ | No | Booking creation time | +| `updated_at` | TIMESTAMPTZ | No | Last status change | + +> The unique constraint on `(venue_id, booking_date, time_slot)` prevents double-booking the same slot. + +--- + +### 3.10 `payments` + +Payment records for booking checkout (Razorpay / Stripe). + +```sql +CREATE TABLE payments ( + id SERIAL PRIMARY KEY, + payment_id VARCHAR(50) NOT NULL UNIQUE, + booking_id INTEGER NOT NULL REFERENCES bookings(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + amount NUMERIC(10, 2) NOT NULL CHECK (amount > 0), + currency VARCHAR(3) NOT NULL DEFAULT 'INR', + status VARCHAR(20) NOT NULL DEFAULT 'created' + CHECK (status IN ('created', 'paid', 'failed', 'refunded', 'refund_pending')), + gateway VARCHAR(30) NOT NULL DEFAULT 'razorpay' + CHECK (gateway IN ('razorpay', 'stripe')), + gateway_order_id VARCHAR(100), + gateway_payment_id VARCHAR(100), + gateway_signature TEXT, + failure_reason TEXT, + paid_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|----------------------|---------------|----------|------------------------------------------| +| `id` | SERIAL | No | Internal primary key | +| `payment_id` | VARCHAR(50) | No | External ID (e.g. `pay_abc123`) | +| `booking_id` | INTEGER | No | FK → `bookings.id` | +| `user_id` | INTEGER | No | FK → `users.id` (payer) | +| `amount` | NUMERIC(10,2) | No | Payment amount | +| `currency` | VARCHAR(3) | No | ISO currency code (default `INR`) | +| `status` | VARCHAR(20) | No | Payment lifecycle status | +| `gateway` | VARCHAR(30) | No | `razorpay`, `stripe` | +| `gateway_order_id` | VARCHAR(100) | Yes | Order ID from payment gateway | +| `gateway_payment_id` | VARCHAR(100) | Yes | Payment ID from gateway after checkout | +| `gateway_signature` | TEXT | Yes | Signature for verification | +| `failure_reason` | TEXT | Yes | Reason if payment failed | +| `paid_at` | TIMESTAMPTZ | Yes | Successful payment timestamp | +| `expires_at` | TIMESTAMPTZ | Yes | Order expiry (unpaid orders) | +| `created_at` | TIMESTAMPTZ | No | Order creation time | +| `updated_at` | TIMESTAMPTZ | No | Last status update | + +--- + +### 3.11 `refunds` + +Refund records linked to payments (manual or auto on cancellation). + +```sql +CREATE TABLE refunds ( + id SERIAL PRIMARY KEY, + refund_id VARCHAR(50) NOT NULL UNIQUE, + payment_id INTEGER NOT NULL REFERENCES payments(id) ON DELETE CASCADE, + amount NUMERIC(10, 2) NOT NULL CHECK (amount > 0), + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'refund_pending' + CHECK (status IN ('refund_pending', 'refunded', 'failed')), + gateway_refund_id VARCHAR(100), + initiated_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|---------------------|---------------|----------|--------------------------------| +| `id` | SERIAL | No | Primary key | +| `refund_id` | VARCHAR(50) | No | External ID (e.g. `rfnd_xyz789`) | +| `payment_id` | INTEGER | No | FK → `payments.id` | +| `amount` | NUMERIC(10,2) | No | Refund amount (full or partial)| +| `reason` | TEXT | No | Refund reason | +| `status` | VARCHAR(20) | No | Refund lifecycle status | +| `gateway_refund_id` | VARCHAR(100) | Yes | ID from payment gateway | +| `initiated_by` | INTEGER | Yes | FK → `users.id` (admin/user) | +| `completed_at` | TIMESTAMPTZ | Yes | When refund completed | +| `created_at` | TIMESTAMPTZ | No | Refund initiation time | + +--- + +### 3.12 `venue_ratings` + +Star ratings and review comments from users. + +```sql +CREATE TABLE venue_ratings ( + id SERIAL PRIMARY KEY, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5), + comment VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (venue_id, user_id) +); +``` + +| Column | Type | Nullable | Description | +|--------------|--------------|----------|--------------------------------| +| `id` | SERIAL | No | Primary key | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `user_id` | INTEGER | No | FK → `users.id` (reviewer) | +| `rating` | SMALLINT | No | 1–5 stars | +| `comment` | VARCHAR(500) | Yes | Optional review text | +| `created_at` | TIMESTAMPTZ | No | Review submission time | +| `updated_at` | TIMESTAMPTZ | No | Last edit | + +> One rating per user per venue. Update `venues.average_rating` and `venues.total_reviews` when a rating is added or changed. + +--- + +### 3.13 `venue_feedback` + +General text feedback (separate from star ratings). + +```sql +CREATE TABLE venue_feedback ( + id SERIAL PRIMARY KEY, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + message TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|--------------|-------------|----------|--------------------------| +| `id` | SERIAL | No | Primary key | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `user_id` | INTEGER | No | FK → `users.id` | +| `message` | TEXT | No | Feedback message | +| `created_at` | TIMESTAMPTZ | No | Submission time | + +--- + +### 3.14 `issues` + +User-reported problems related to venues or bookings. + +```sql +CREATE TABLE issues ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, + booking_id INTEGER REFERENCES bookings(id) ON DELETE SET NULL, + subject VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'open' + CHECK (status IN ('open', 'in_progress', 'resolved', 'closed')), + admin_note TEXT, + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +| Column | Type | Nullable | Description | +|---------------|--------------|----------|--------------------------------------| +| `id` | SERIAL | No | Primary key | +| `user_id` | INTEGER | No | FK → `users.id` (reporter) | +| `venue_id` | INTEGER | No | FK → `venues.id` | +| `booking_id` | INTEGER | Yes | FK → `bookings.id` (optional link) | +| `subject` | VARCHAR(200) | No | Short issue title | +| `description` | TEXT | No | Full issue details | +| `status` | VARCHAR(20) | No | `open`, `in_progress`, `resolved`, `closed` | +| `admin_note` | TEXT | Yes | Admin resolution notes | +| `resolved_at` | TIMESTAMPTZ | Yes | When issue was resolved | +| `created_at` | TIMESTAMPTZ | No | Report time | +| `updated_at` | TIMESTAMPTZ | No | Last status update | + +--- + +## 4. API ↔ Database Field Mapping + +Column names in the [API Documentation](./APIDocumentation.md) match database columns directly. Use this table when implementing serializers and request handlers. + +| DB table.column | API JSON field | Notes | +|-----------------|----------------|-------| +| `users.name` | `name` | | +| `users.email` | `email` | | +| `users.mobile` | `mobile` | | +| `users.password_hash` | `password` | Request only; never returned in responses | +| `users.role` | `role` | `user`, `owner`, `admin` | +| `users.is_active` | `is_active` | | +| `users.created_at` | `created_at` | ISO 8601 | +| `users.updated_at` | `updated_at` | ISO 8601 | +| `owner_profiles.business_name` | `business_name` | | +| `owner_profiles.phone` | `phone` | | +| `venues.price_per_day` | `price_per_day` | Not `price` | +| `venues.approval_status` | `approval_status` | `pending`, `approved`, `rejected` | +| `venues.rejection_reason` | `rejection_reason` | Admin reject body/response | +| `venues.is_active` | `is_active` | Admin block sets `false` | +| `venues.average_rating` | `average_rating` | | +| `venues.total_reviews` | `total_reviews` | | +| `venue_images.image_url` | `image_url` | Not `url` | +| `venue_images.display_order` | `display_order` | | +| `bookings.booking_date` | `booking_date` | `YYYY-MM-DD` | +| `bookings.time_slot` | `time_slot` | `HH:MM` in API; `TIME` in DB | +| `bookings.notes` | `notes` | | +| `bookings.amount` | `amount` | Copied from `venues.price_per_day` at creation | +| `bookings.status` | `status` | `pending_payment`, `booked`, `cancelled` | +| `bookings.cancellation_reason` | `cancellation_reason` | Cancel booking request | +| `bookings.cancelled_at` | `cancelled_at` | | +| `payments.payment_id` | `payment_id` | External string ID | +| `payments.status` | `status` / `payment_status` | `payment_status` on booking lists = joined `payments.status` | +| `payments.gateway` | `gateway` | `razorpay`, `stripe` | +| `payments.currency` | `currency` | Default `INR` | +| `payments.gateway_order_id` | `gateway_order_id` | | +| `payments.gateway_payment_id` | `gateway_payment_id` | | +| `payments.gateway_signature` | `gateway_signature` | Verify endpoint only | +| `payments.paid_at` | `paid_at` | | +| `payments.expires_at` | `expires_at` | | +| `refunds.refund_id` | `refund_id` | | +| `refunds.status` | `status` / `refund_status` | `refund_pending`, `refunded`, `failed` | +| `refunds.reason` | `reason` | Refund request body | +| `refunds.amount` | `amount` | | +| `venue_ratings.rating` | `rating` | 1–5 | +| `venue_ratings.comment` | `comment` | Max 500 chars | +| `venue_feedback.message` | `message` | | +| `issues.subject` | `subject` | | +| `issues.description` | `description` | | +| `issues.status` | `status` | `open`, `in_progress`, `resolved`, `closed` | +| `issues.admin_note` | `admin_note` | | +| `issues.resolved_at` | `resolved_at` | Set when status → `resolved` | + +### Derived API fields (not stored) + +| API field | Source | +|-----------|--------| +| `thumbnail_url` | First `venue_images.image_url` ordered by `display_order` | +| `venue_name`, `user_name`, `owner_name` | Joined from related tables | +| `total_bookings`, `total_revenue` | Aggregated counts/sums | +| `payment_status` on booking responses | Latest `payments.status` for `booking_id` | + +### Status transition rules + +| Table | Transition | +|-------|------------| +| `venues.approval_status` | `pending` → `approved` (admin approve) or `rejected` (admin reject) | +| `bookings.status` | `pending_payment` → `booked` (payment verified) → `cancelled` (user cancel) | +| `payments.status` | `created` → `paid` or `failed`; `paid` → `refund_pending` → `refunded` | +| `refunds.status` | `refund_pending` → `refunded` or `failed` | + +--- + +## 5. Relationships Summary + +| Parent | Child | Relationship | On Delete | +|-----------------|---------------------|--------------|-------------| +| `users` | `venues` | One → Many | CASCADE | +| `users` | `bookings` | One → Many | CASCADE | +| `users` | `payments` | One → Many | CASCADE | +| `users` | `venue_ratings` | One → Many | CASCADE | +| `users` | `venue_feedback` | One → Many | CASCADE | +| `users` | `issues` | One → Many | CASCADE | +| `users` | `owner_profiles` | One → One | CASCADE | +| `users` | `refresh_tokens` | One → Many | CASCADE | +| `venues` | `venue_images` | One → Many | CASCADE | +| `venues` | `bookings` | One → Many | CASCADE | +| `venues` | `venue_ratings` | One → Many | CASCADE | +| `venues` | `venue_feedback` | One → Many | CASCADE | +| `venues` | `issues` | One → Many | CASCADE | +| `venues` | `venue_amenities` | One → Many | CASCADE | +| `amenities` | `venue_amenities` | One → Many | CASCADE | +| `bookings` | `payments` | One → Many | CASCADE | +| `bookings` | `issues` | One → Many | SET NULL | +| `payments` | `refunds` | One → Many | CASCADE | + +--- + +## 6. Indexes + +Recommended indexes for query performance: + +```sql +-- Users +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_role ON users(role); + +-- Venues +CREATE INDEX idx_venues_owner_id ON venues(owner_id); +CREATE INDEX idx_venues_approval_status ON venues(approval_status); +CREATE INDEX idx_venues_location ON venues(location); +CREATE INDEX idx_venues_price ON venues(price_per_day); + +-- Bookings +CREATE INDEX idx_bookings_user_id ON bookings(user_id); +CREATE INDEX idx_bookings_venue_id ON bookings(venue_id); +CREATE INDEX idx_bookings_date ON bookings(booking_date); +CREATE INDEX idx_bookings_status ON bookings(status); + +-- Payments +CREATE INDEX idx_payments_booking_id ON payments(booking_id); +CREATE INDEX idx_payments_user_id ON payments(user_id); +CREATE INDEX idx_payments_status ON payments(status); +CREATE INDEX idx_payments_gateway_order_id ON payments(gateway_order_id); + +-- Ratings & Issues +CREATE INDEX idx_venue_ratings_venue_id ON venue_ratings(venue_id); +CREATE INDEX idx_issues_status ON issues(status); +CREATE INDEX idx_issues_user_id ON issues(user_id); + +-- Tokens +CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); +CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id); +``` + +--- + +## 7. Enums & Status Values + +> These values are identical to [API section 2.8](./APIDocumentation.md#28-shared-enum-values). + +### User roles + +| Value | Description | +|---------|--------------------| +| `user` | Normal booker | +| `owner` | Venue owner | +| `admin` | Platform admin | + +### Venue approval status + +| Value | Description | +|------------|--------------------------------| +| `pending` | Awaiting admin review | +| `approved` | Live on platform | +| `rejected` | Rejected by admin | + +### Booking status + +| Value | Description | +|--------------------|--------------------------------------| +| `pending_payment` | Created, awaiting payment | +| `booked` | Payment confirmed, slot reserved | +| `cancelled` | Cancelled by user or system | + +### Payment status + +| Value | Description | +|------------------|----------------------------| +| `created` | Order created, not paid | +| `paid` | Payment successful | +| `failed` | Payment attempt failed | +| `refund_pending` | Refund in progress | +| `refunded` | Fully refunded | + +### Refund status (`refunds.status`) + +| Value | Description | +|------------------|--------------------------| +| `refund_pending` | Initiated, processing | +| `refunded` | Completed | +| `failed` | Refund failed | + +### Payment gateway (`payments.gateway`) + +| Value | Description | +|------------|--------------------| +| `razorpay` | Default gateway | +| `stripe` | Alternate gateway | + +### Issue status (`issues.status`) + +| Value | Description | +|---------------|--------------------------| +| `open` | Newly reported | +| `in_progress` | Admin is investigating | +| `resolved` | Issue fixed | +| `closed` | Closed without action | + +--- + +## 8. Sample Seed Data + +Use for local development and testing: + +```sql +-- Admin user (password: admin123 — hash in app) +INSERT INTO users (name, email, mobile, password_hash, role) +VALUES ('Admin User', 'admin@bookmyvenue.com', '9000000001', '$2b$12$placeholder', 'admin'); + +-- Owner +INSERT INTO users (name, email, mobile, password_hash, role) +VALUES ('Venue Owner', 'owner@bookmyvenue.com', '9000000002', '$2b$12$placeholder', 'owner'); + +-- Normal user +INSERT INTO users (name, email, mobile, password_hash, role) +VALUES ('Alan', 'alan@gmail.com', '9090900000', '$2b$12$placeholder', 'user'); + +-- Amenities +INSERT INTO amenities (name) VALUES + ('Wi-Fi'), ('Parking'), ('AC'), ('Projector'), ('Stage'), ('Catering'); + +-- Owner profile +INSERT INTO owner_profiles (user_id, business_name, phone) +VALUES (2, 'Alan Events Pvt Ltd', '9000000002'); + +-- Venue (approved) +INSERT INTO venues (owner_id, name, location, price_per_day, description, approval_status) +VALUES (2, 'Grand Hall', 'Kochi, Kerala', 10000.00, 'Large event venue with stage and seating for 500', 'approved'); + +-- Venue amenities +INSERT INTO venue_amenities (venue_id, amenity_id) VALUES (1, 1), (1, 2), (1, 3); + +-- Venue image +INSERT INTO venue_images (venue_id, image_url, display_order) +VALUES (1, 'https://cdn.example.com/venues/1/img1.jpg', 0); +``` + +> Replace `$2b$12$placeholder` with real bcrypt hashes generated by the application. + +--- + +## 9. Migration Notes + +### Development + +For quick local setup only: + +```python +Base.metadata.create_all(bind=engine) +``` + +### Production + +Always use Alembic migrations: + +```bash +cd backend +alembic init migrations +alembic revision --autogenerate -m "initial schema" +alembic upgrade head +``` + +See [database production guide](./database/database_production.md) for details. + +### Suggested migration order + +1. `users` +2. `refresh_tokens`, `password_reset_tokens` +3. `owner_profiles` +4. `venues`, `amenities`, `venue_amenities`, `venue_images` +5. `bookings` +6. `payments`, `refunds` +7. `venue_ratings`, `venue_feedback`, `issues` + +--- + +## Related Documents + +- [API Documentation](./APIDocumentation.md) +- [System Design](./SystemDesign.md) +- [Product Requirements (PRD)](./PRD.md) +- [Folder Architecture](./FolderArchitecture.md) +- [Backend Setup](../backend/README.md) + +--- + +**Last updated:** June 2026 diff --git a/documents/PRD.md b/documents/PRD.md new file mode 100644 index 000000000..ae0519370 --- /dev/null +++ b/documents/PRD.md @@ -0,0 +1,190 @@ + +# 📄 Product Requirement Document (PRD) + +## Project: BookMyVenue (MVP) + +## 1. Purpose + +BookMyVenue is a simple platform where users can: + +* Find venues +* View details +* Book them online. + +It also allows venue owners to: + +* List their spaces +* Manage bookings + +And a super admin to: + +* Control and approve everything +* Manage the venues if needed block them + + +## 2. 👥 Users (Roles) + +### 1. Normal User + +* Can register and login +* Can browse venues +* Can book a venue +* Provide the Rating +* Raise issues + +### 2. Venue Owner + +* Can add and manage venues +* Can see bookings for their venues +* get the user feedbacks + +### 3. Super Admin + +* Can approve or reject venues +* Can manage users and owners +* Has full control over the platform + +## 3. Core Features (MVP Only) + +### Authentication + +* User can register +* User can login +* Role-based access (user / owner / admin) + +### Venue Management + +* Owner can: + + * Add a venue + * Edit venue details + * Delete venue + * Get user feedback + +* Venue includes: + + * Name + * Location + * Price + * Description + * Amenities + * Images + * Rating + + +### Venue Browsing + +* Users can: + + * View all venues + * View venue details + * Search/filter (basic: location, price) + * Send Feedback + + + +### Booking System + +* User can: + + * Book a venue for a date +* System stores: + + * User + * Venue + * Date + * Status (booked/cancelled) + + +### Admin Control + +* Admin can: + + * Approve or reject venues before they go live + * View all users + * Remove bad listings + + +## 4. Out of Scope (NOT in MVP) + +Do NOT build these now: + +* Online payments +* Reviews and ratings +* Chat system +* Notifications +* AI recommendations +* Advanced search + + +## 5. ⚙️ Basic Flow + +### User Flow: + +1. User signs up / logs in +2. User browses venues +3. User selects a venue +4. User books a date + + +### Owner Flow: + +1. Owner logs in +2. Owner adds venue +3. Waits for admin approval +4. Manages bookings + + +### Admin Flow: + +1. Admin logs in +2. Reviews new venues +3. Approves or rejects them + + +## 6. 📦 Data (Simple Overview) + +### User + +* id +* name +* email +* password +* role + +### Venue + +* id +* owner_id +* name +* location +* price +* description +* approved (true/false) + +### Booking + +* id +* user_id +* venue_id +* date +* status + +--- + +## 7. Goal of MVP + +* Build a working backend API +* Keep it simple and clean +* Make it easy for contributors to understand +* No overengineering + +--- + +## 8. Key Rules + +* Keep logic simple +* Write clean APIs +* Avoid unnecessary features +* Focus on functionality, not perfection + diff --git a/documents/SystemDesign.md b/documents/SystemDesign.md new file mode 100644 index 000000000..93659297b --- /dev/null +++ b/documents/SystemDesign.md @@ -0,0 +1,141 @@ +# 📄 System Design / Architecture + +## Project: BookMyVenue (MVP) + +## 1. 🎯 Goal + +Build a simple backend system where: + +* Users can find and book venues +* Owners can list venues +* Admin can control everything + +System should be: + +* Easy to understand +* Easy to contribute +* Easy to scale later (not now) + + +## 2. 🧱 Tech Stack + +* Backend: FastAPI ( why because in future implementing an ai will be easy to configure ) +* Database: PostgreSQL ( structured way to store datas , in future nosql can be added) +* Authentication: JWT (token-based login) + + +### Flow: + +``` +Client (Frontend / Postman) + ↓ + FastAPI Server + ↓ + PostgreSQL Database +``` + +## 3. ⚙️ How It Works (Step by Step) + +### Example: Booking a Venue + +1. User sends request → `POST /bookings/` +2. FastAPI: + + * Checks user login (JWT) + * Validates data +3. Backend saves booking in database +4. Response sent back to user + +--- + +## 4. 🧩 Main Components + +### 1. API Layer (Routes) + +* Handles incoming requests +* Example: + + * `/auth` + * `/venues` + * `/bookings` + * `/admin` + +--- + +### 2. Service Layer (Logic) + +* Contains business logic +* Example: + + * Check venue availability + * Validate booking + * Handle permissions + +--- + +### 3. Database Layer + +* Stores all data +* Tables: + + * Users + * Venues + * Bookings + +--- + +### 4. Authentication System + +* Uses JWT tokens +* Flow: + + 1. User logs in + 2. Server returns token + 3. User sends token in future requests + +--- + +## 5. Role-Based Access + +System checks user role before actions: + +* User → can book +* Owner → can manage venues +* Admin → can approve/reject + +--- + + +## 6. Data Flow Example + +### Add Venue (Owner) + +1. Owner → sends `POST /venues` +2. API receives request +3. Service validates data +4. Save in DB (status = not approved) +5. Admin later approves + +--- + +## 7. 🚫 What We Are NOT Doing Now + +* Microservices +* Caching (Redis) +* Message queues +* Real-time systems +* Load balancing + +You don’t need them. Adding them now = wasted time. + +--- + +## 8. 🚀 Future Scalability (Later, Not Now) + +When system grows: + +* Add caching +* Split services +* Use cloud deployment + + diff --git a/frontend/BMV/.env.example b/frontend/BMV/.env.example new file mode 100644 index 000000000..2d1bf5ff5 --- /dev/null +++ b/frontend/BMV/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL= \ No newline at end of file diff --git a/frontend/BMV/.gitignore b/frontend/BMV/.gitignore new file mode 100644 index 000000000..50c8dda2a --- /dev/null +++ b/frontend/BMV/.gitignore @@ -0,0 +1,26 @@ +# 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? + +.env diff --git a/frontend/BMV/README.md b/frontend/BMV/README.md new file mode 100644 index 000000000..e16d995e3 --- /dev/null +++ b/frontend/BMV/README.md @@ -0,0 +1,18 @@ +# 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 enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend 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/BMV/eslint.config.js b/frontend/BMV/eslint.config.js new file mode 100644 index 000000000..ea36dd3dc --- /dev/null +++ b/frontend/BMV/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/BMV/index.html b/frontend/BMV/index.html new file mode 100644 index 000000000..4cbb2a2f5 --- /dev/null +++ b/frontend/BMV/index.html @@ -0,0 +1,13 @@ + + + + + + + bmv + + +
+ + + diff --git a/frontend/BMV/package-lock.json b/frontend/BMV/package-lock.json new file mode 100644 index 000000000..b16170b9f --- /dev/null +++ b/frontend/BMV/package-lock.json @@ -0,0 +1,3607 @@ +{ + "name": "bmv", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bmv", + "version": "0.0.0", + "dependencies": { + "@reduxjs/toolkit": "^2.12.0", + "axios": "^1.17.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-redux": "^9.3.0", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@eslint/js": "^10.0.1", + "@rolldown/plugin-babel": "^0.2.3", + "@tailwindcss/vite": "^4.3.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tailwindcss": "^4.3.1", + "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", + "peer": true, + "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/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "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==", + "dev": true, + "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.135.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.135.0.tgz", + "integrity": "sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.1.tgz", + "integrity": "sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.1.tgz", + "integrity": "sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.1.tgz", + "integrity": "sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/plugin-babel": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/plugin-babel/-/plugin-babel-0.2.3.tgz", + "integrity": "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=22.12.0 || ^24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.29.0 || ^8.0.0-rc.1", + "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", + "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", + "rolldown": "^1.0.0-rc.5", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@babel/plugin-transform-runtime": { + "optional": true + }, + "@babel/runtime": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "dev": true, + "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==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@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", + "peer": true, + "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/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.26.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", + "peer": true, + "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==", + "devOptional": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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", + "peer": true, + "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.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "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==", + "dev": true, + "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/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/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/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==", + "dev": true, + "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/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "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==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/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==", + "dev": true, + "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/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-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/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.1.tgz", + "integrity": "sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.135.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.1.1", + "@rolldown/binding-darwin-arm64": "1.1.1", + "@rolldown/binding-darwin-x64": "1.1.1", + "@rolldown/binding-freebsd-x64": "1.1.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.1", + "@rolldown/binding-linux-arm64-gnu": "1.1.1", + "@rolldown/binding-linux-arm64-musl": "1.1.1", + "@rolldown/binding-linux-ppc64-gnu": "1.1.1", + "@rolldown/binding-linux-s390x-gnu": "1.1.1", + "@rolldown/binding-linux-x64-gnu": "1.1.1", + "@rolldown/binding-linux-x64-musl": "1.1.1", + "@rolldown/binding-openharmony-arm64": "1.1.1", + "@rolldown/binding-wasm32-wasi": "1.1.1", + "@rolldown/binding-win32-arm64-msvc": "1.1.1", + "@rolldown/binding-win32-x64-msvc": "1.1.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "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/vite/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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/vite/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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/vite/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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/vite/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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "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/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "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/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", + "peer": true, + "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/BMV/package.json b/frontend/BMV/package.json new file mode 100644 index 000000000..1870232bc --- /dev/null +++ b/frontend/BMV/package.json @@ -0,0 +1,36 @@ +{ + "name": "bmv", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@reduxjs/toolkit": "^2.12.0", + "axios": "^1.17.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-redux": "^9.3.0", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@eslint/js": "^10.0.1", + "@rolldown/plugin-babel": "^0.2.3", + "@tailwindcss/vite": "^4.3.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tailwindcss": "^4.3.1", + "vite": "^8.0.12" + } +} diff --git a/frontend/BMV/public/favicon.svg b/frontend/BMV/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/frontend/BMV/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/BMV/public/icons.svg b/frontend/BMV/public/icons.svg new file mode 100644 index 000000000..e9522193d --- /dev/null +++ b/frontend/BMV/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/BMV/src/App.css b/frontend/BMV/src/App.css new file mode 100644 index 000000000..f90339d8f --- /dev/null +++ b/frontend/BMV/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/BMV/src/App.jsx b/frontend/BMV/src/App.jsx new file mode 100644 index 000000000..9d882cbb7 --- /dev/null +++ b/frontend/BMV/src/App.jsx @@ -0,0 +1,21 @@ +import { Routes, Route, Navigate } from "react-router-dom"; +import LoginPage from "./pages/LoginPage"; +import RegisterPage from "./pages/RegisterPage"; +import MyBookingsPage from "./pages/MyBookingsPage"; +import BookingDetailPage from "./pages/BookingDetailPage"; +import CheckoutPage from "./pages/CheckoutPage"; + +function App() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + + ); +} + +export default App; \ No newline at end of file diff --git a/frontend/BMV/src/app/store.js b/frontend/BMV/src/app/store.js new file mode 100644 index 000000000..5adecafb2 --- /dev/null +++ b/frontend/BMV/src/app/store.js @@ -0,0 +1,15 @@ + +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from '../modules/auth/authSlice' +import bookingReducer from '../modules/bookings/bookingSlice' +import paymentReducer from '../modules/payments/paymentSlice' + +const store = configureStore({ + reducer: { + auth: authReducer, + bookings: bookingReducer, + payments: paymentReducer, + }, +}); + +export default store; diff --git a/frontend/BMV/src/assets/hero.png b/frontend/BMV/src/assets/hero.png new file mode 100644 index 000000000..02251f4b9 Binary files /dev/null and b/frontend/BMV/src/assets/hero.png differ diff --git a/frontend/BMV/src/assets/react.svg b/frontend/BMV/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/frontend/BMV/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/BMV/src/assets/vite.svg b/frontend/BMV/src/assets/vite.svg new file mode 100644 index 000000000..5101b674d --- /dev/null +++ b/frontend/BMV/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/BMV/src/components/BookingForm.jsx b/frontend/BMV/src/components/BookingForm.jsx new file mode 100644 index 000000000..0b65c965d --- /dev/null +++ b/frontend/BMV/src/components/BookingForm.jsx @@ -0,0 +1,85 @@ +import { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { createBookingAsync } from "../modules/bookings/bookingSlice"; + +function BookingForm({ venueId }) { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { loading, error } = useSelector((state) => state.bookings); + + const [bookingDate, setBookingDate] = useState(""); + const [timeSlot, setTimeSlot] = useState(""); + const [notes, setNotes] = useState(""); + + const handleSubmit = async (e) => { + e.preventDefault(); + + const result = await dispatch( + createBookingAsync({ + venue_id: venueId, + booking_date: bookingDate, + time_slot: timeSlot, + notes: notes || null, + }), + ); + + if (createBookingAsync.fulfilled.match(result)) { + navigate(`/checkout/${result.payload.id}`); + } + }; + + return ( +
+

Book this venue

+ +
+ + setBookingDate(e.target.value)} + className="w-full rounded border border-gray-300 px-3 py-2" + /> +
+ +
+ + setTimeSlot(e.target.value)} + className="w-full rounded border border-gray-300 px-3 py-2" + /> +
+ +
+ +