From b944585d73febdf488005b217c39b2a25337dbbb Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 21:44:36 +0100 Subject: [PATCH 001/243] feat: initialize booking monorepo with core API, multi-app frontends, and shared packages --- .dockerignore | 6 + .editorconfig | 12 ++ .env.example | 17 ++ .gitignore | 10 ++ .prettierrc | 7 + apps/admin-panel/.env.example | 1 + apps/admin-panel/index.html | 12 ++ apps/admin-panel/package.json | 25 +++ apps/admin-panel/src/App.tsx | 6 + apps/admin-panel/src/main.tsx | 10 ++ apps/admin-panel/src/pages/Dashboard.tsx | 3 + apps/admin-panel/src/pages/Users.tsx | 3 + apps/admin-panel/src/pages/VenueApprovals.tsx | 3 + apps/admin-panel/src/routes.tsx | 10 ++ apps/admin-panel/src/styles/index.css | 9 + apps/admin-panel/tsconfig.json | 8 + apps/admin-panel/vite.config.ts | 12 ++ apps/api/.env.example | 10 ++ apps/api/.python-version | 1 + apps/api/Dockerfile | 10 ++ apps/api/alembic.ini | 38 +++++ apps/api/alembic/versions/.gitkeep | 0 apps/api/app/__init__.py | 0 apps/api/app/core/config.py | 20 +++ apps/api/app/core/database.py | 18 ++ apps/api/app/core/exceptions.py | 21 +++ apps/api/app/core/middleware.py | 12 ++ apps/api/app/core/security.py | 24 +++ apps/api/app/jobs/booking_completion.py | 3 + apps/api/app/jobs/hold_expiry.py | 3 + apps/api/app/jobs/payment_reminders.py | 3 + apps/api/app/jobs/scheduler.py | 12 ++ apps/api/app/jobs/stale_requests.py | 3 + apps/api/app/main.py | 25 +++ apps/api/app/modules/admin/routes.py | 11 ++ apps/api/app/modules/admin/schemas.py | 7 + apps/api/app/modules/admin/service.py | 5 + apps/api/app/modules/auth/__init__.py | 0 apps/api/app/modules/auth/dependencies.py | 21 +++ apps/api/app/modules/auth/routes.py | 15 ++ apps/api/app/modules/auth/schemas.py | 17 ++ apps/api/app/modules/auth/service.py | 9 + apps/api/app/modules/availability/models.py | 22 +++ apps/api/app/modules/availability/routes.py | 16 ++ apps/api/app/modules/availability/schemas.py | 12 ++ apps/api/app/modules/availability/service.py | 10 ++ apps/api/app/modules/booking/models.py | 36 ++++ apps/api/app/modules/booking/routes.py | 16 ++ apps/api/app/modules/booking/schemas.py | 18 ++ apps/api/app/modules/booking/service.py | 9 + apps/api/app/modules/booking/state_machine.py | 13 ++ apps/api/app/modules/notification/models.py | 13 ++ apps/api/app/modules/notification/routes.py | 16 ++ apps/api/app/modules/notification/schemas.py | 10 ++ apps/api/app/modules/notification/service.py | 10 ++ .../modules/notification/templates/.gitkeep | 0 apps/api/app/modules/payment/models.py | 30 ++++ apps/api/app/modules/payment/routes.py | 16 ++ apps/api/app/modules/payment/schemas.py | 14 ++ apps/api/app/modules/payment/service.py | 5 + apps/api/app/modules/payment/webhooks.py | 5 + apps/api/app/modules/profile/models.py | 21 +++ apps/api/app/modules/profile/routes.py | 17 ++ apps/api/app/modules/profile/schemas.py | 13 ++ apps/api/app/modules/profile/service.py | 9 + apps/api/app/modules/search/routes.py | 15 ++ apps/api/app/modules/search/schemas.py | 17 ++ apps/api/app/modules/search/service.py | 6 + apps/api/app/modules/venue/models.py | 36 ++++ apps/api/app/modules/venue/routes.py | 16 ++ apps/api/app/modules/venue/schemas.py | 21 +++ apps/api/app/modules/venue/service.py | 9 + apps/api/app/shared/models.py | 11 ++ apps/api/app/shared/pagination.py | 16 ++ apps/api/app/shared/utils.py | 5 + apps/api/pyproject.toml | 23 +++ apps/api/tests/conftest.py | 8 + apps/api/tests/test_booking.py | 2 + apps/api/tests/test_venue.py | 2 + apps/owner-portal/.env.example | 1 + apps/owner-portal/index.html | 12 ++ apps/owner-portal/package.json | 25 +++ apps/owner-portal/src/App.tsx | 6 + apps/owner-portal/src/main.tsx | 10 ++ apps/owner-portal/src/pages/Bookings.tsx | 3 + apps/owner-portal/src/pages/Dashboard.tsx | 3 + apps/owner-portal/src/pages/ManageVenues.tsx | 3 + apps/owner-portal/src/routes.tsx | 10 ++ apps/owner-portal/src/styles/index.css | 9 + apps/owner-portal/tsconfig.json | 8 + apps/owner-portal/vite.config.ts | 12 ++ apps/user-web/.env.example | 1 + apps/user-web/index.html | 12 ++ apps/user-web/package.json | 25 +++ apps/user-web/src/App.tsx | 6 + apps/user-web/src/main.tsx | 10 ++ apps/user-web/src/pages/Home.tsx | 3 + apps/user-web/src/pages/MyBookings.tsx | 3 + apps/user-web/src/pages/VenueDetails.tsx | 3 + apps/user-web/src/routes.tsx | 10 ++ apps/user-web/src/styles/index.css | 9 + apps/user-web/tsconfig.json | 8 + apps/user-web/vite.config.ts | 12 ++ docker-compose.yml | 29 ++++ eslint.config.js | 17 ++ package.json | 18 ++ packages/api-client/package.json | 14 ++ packages/api-client/scripts/generate.ts | 10 ++ packages/api-client/src/client.ts | 23 +++ packages/api-client/src/endpoints/auth.ts | 8 + packages/api-client/src/endpoints/bookings.ts | 6 + packages/api-client/src/endpoints/payments.ts | 5 + packages/api-client/src/endpoints/venues.ts | 10 ++ packages/api-client/src/index.ts | 6 + packages/api-client/src/types.ts | 1 + packages/api-client/tsconfig.json | 8 + packages/ui/package.json | 16 ++ packages/ui/src/Button.tsx | 13 ++ packages/ui/src/Card.tsx | 5 + packages/ui/src/DatePicker.tsx | 16 ++ packages/ui/src/Input.tsx | 14 ++ packages/ui/src/Modal.tsx | 16 ++ packages/ui/src/index.ts | 5 + packages/ui/tsconfig.json | 9 + pnpm-workspace.yaml | 3 + prd.txt | 161 ++++++++++++++++++ tsconfig.base.json | 15 ++ 127 files changed, 1592 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 apps/admin-panel/.env.example create mode 100644 apps/admin-panel/index.html create mode 100644 apps/admin-panel/package.json create mode 100644 apps/admin-panel/src/App.tsx create mode 100644 apps/admin-panel/src/main.tsx create mode 100644 apps/admin-panel/src/pages/Dashboard.tsx create mode 100644 apps/admin-panel/src/pages/Users.tsx create mode 100644 apps/admin-panel/src/pages/VenueApprovals.tsx create mode 100644 apps/admin-panel/src/routes.tsx create mode 100644 apps/admin-panel/src/styles/index.css create mode 100644 apps/admin-panel/tsconfig.json create mode 100644 apps/admin-panel/vite.config.ts create mode 100644 apps/api/.env.example create mode 100644 apps/api/.python-version create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/alembic.ini create mode 100644 apps/api/alembic/versions/.gitkeep create mode 100644 apps/api/app/__init__.py create mode 100644 apps/api/app/core/config.py create mode 100644 apps/api/app/core/database.py create mode 100644 apps/api/app/core/exceptions.py create mode 100644 apps/api/app/core/middleware.py create mode 100644 apps/api/app/core/security.py create mode 100644 apps/api/app/jobs/booking_completion.py create mode 100644 apps/api/app/jobs/hold_expiry.py create mode 100644 apps/api/app/jobs/payment_reminders.py create mode 100644 apps/api/app/jobs/scheduler.py create mode 100644 apps/api/app/jobs/stale_requests.py create mode 100644 apps/api/app/main.py create mode 100644 apps/api/app/modules/admin/routes.py create mode 100644 apps/api/app/modules/admin/schemas.py create mode 100644 apps/api/app/modules/admin/service.py create mode 100644 apps/api/app/modules/auth/__init__.py create mode 100644 apps/api/app/modules/auth/dependencies.py create mode 100644 apps/api/app/modules/auth/routes.py create mode 100644 apps/api/app/modules/auth/schemas.py create mode 100644 apps/api/app/modules/auth/service.py create mode 100644 apps/api/app/modules/availability/models.py create mode 100644 apps/api/app/modules/availability/routes.py create mode 100644 apps/api/app/modules/availability/schemas.py create mode 100644 apps/api/app/modules/availability/service.py create mode 100644 apps/api/app/modules/booking/models.py create mode 100644 apps/api/app/modules/booking/routes.py create mode 100644 apps/api/app/modules/booking/schemas.py create mode 100644 apps/api/app/modules/booking/service.py create mode 100644 apps/api/app/modules/booking/state_machine.py create mode 100644 apps/api/app/modules/notification/models.py create mode 100644 apps/api/app/modules/notification/routes.py create mode 100644 apps/api/app/modules/notification/schemas.py create mode 100644 apps/api/app/modules/notification/service.py create mode 100644 apps/api/app/modules/notification/templates/.gitkeep create mode 100644 apps/api/app/modules/payment/models.py create mode 100644 apps/api/app/modules/payment/routes.py create mode 100644 apps/api/app/modules/payment/schemas.py create mode 100644 apps/api/app/modules/payment/service.py create mode 100644 apps/api/app/modules/payment/webhooks.py create mode 100644 apps/api/app/modules/profile/models.py create mode 100644 apps/api/app/modules/profile/routes.py create mode 100644 apps/api/app/modules/profile/schemas.py create mode 100644 apps/api/app/modules/profile/service.py create mode 100644 apps/api/app/modules/search/routes.py create mode 100644 apps/api/app/modules/search/schemas.py create mode 100644 apps/api/app/modules/search/service.py create mode 100644 apps/api/app/modules/venue/models.py create mode 100644 apps/api/app/modules/venue/routes.py create mode 100644 apps/api/app/modules/venue/schemas.py create mode 100644 apps/api/app/modules/venue/service.py create mode 100644 apps/api/app/shared/models.py create mode 100644 apps/api/app/shared/pagination.py create mode 100644 apps/api/app/shared/utils.py create mode 100644 apps/api/pyproject.toml create mode 100644 apps/api/tests/conftest.py create mode 100644 apps/api/tests/test_booking.py create mode 100644 apps/api/tests/test_venue.py create mode 100644 apps/owner-portal/.env.example create mode 100644 apps/owner-portal/index.html create mode 100644 apps/owner-portal/package.json create mode 100644 apps/owner-portal/src/App.tsx create mode 100644 apps/owner-portal/src/main.tsx create mode 100644 apps/owner-portal/src/pages/Bookings.tsx create mode 100644 apps/owner-portal/src/pages/Dashboard.tsx create mode 100644 apps/owner-portal/src/pages/ManageVenues.tsx create mode 100644 apps/owner-portal/src/routes.tsx create mode 100644 apps/owner-portal/src/styles/index.css create mode 100644 apps/owner-portal/tsconfig.json create mode 100644 apps/owner-portal/vite.config.ts create mode 100644 apps/user-web/.env.example create mode 100644 apps/user-web/index.html create mode 100644 apps/user-web/package.json create mode 100644 apps/user-web/src/App.tsx create mode 100644 apps/user-web/src/main.tsx create mode 100644 apps/user-web/src/pages/Home.tsx create mode 100644 apps/user-web/src/pages/MyBookings.tsx create mode 100644 apps/user-web/src/pages/VenueDetails.tsx create mode 100644 apps/user-web/src/routes.tsx create mode 100644 apps/user-web/src/styles/index.css create mode 100644 apps/user-web/tsconfig.json create mode 100644 apps/user-web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 packages/api-client/package.json create mode 100644 packages/api-client/scripts/generate.ts create mode 100644 packages/api-client/src/client.ts create mode 100644 packages/api-client/src/endpoints/auth.ts create mode 100644 packages/api-client/src/endpoints/bookings.ts create mode 100644 packages/api-client/src/endpoints/payments.ts create mode 100644 packages/api-client/src/endpoints/venues.ts create mode 100644 packages/api-client/src/index.ts create mode 100644 packages/api-client/src/types.ts create mode 100644 packages/api-client/tsconfig.json create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/Button.tsx create mode 100644 packages/ui/src/Card.tsx create mode 100644 packages/ui/src/DatePicker.tsx create mode 100644 packages/ui/src/Input.tsx create mode 100644 packages/ui/src/Modal.tsx create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 prd.txt create mode 100644 tsconfig.base.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..62e979706 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env +__pycache__/ +.venv/ +*.pyc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..4a7ea3036 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..c475e8ec4 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Database +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bookmyvenue + +# Auth +JWT_SECRET=your-secret-here +JWT_ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# Stripe +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... + +# Email (SMTP) +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=noreply@example.com +SMTP_PASSWORD=your-smtp-password diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f684534cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.env +.env.local +__pycache__/ +*.pyc +.venv/ +.pytest_cache/ +.mypy_cache/ +*.egg-info/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..238d4d936 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/apps/admin-panel/.env.example b/apps/admin-panel/.env.example new file mode 100644 index 000000000..c2058b5ea --- /dev/null +++ b/apps/admin-panel/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/apps/admin-panel/index.html b/apps/admin-panel/index.html new file mode 100644 index 000000000..e176e6af2 --- /dev/null +++ b/apps/admin-panel/index.html @@ -0,0 +1,12 @@ + + + + + + BookMyVenue — Admin + + +
+ + + diff --git a/apps/admin-panel/package.json b/apps/admin-panel/package.json new file mode 100644 index 000000000..77efbcdd9 --- /dev/null +++ b/apps/admin-panel/package.json @@ -0,0 +1,25 @@ +{ + "name": "@venue404/admin-panel", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src", + "preview": "vite preview" + }, + "dependencies": { + "@venue404/api-client": "workspace:*", + "@venue404/ui": "workspace:*", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.3.0" + } +} \ No newline at end of file diff --git a/apps/admin-panel/src/App.tsx b/apps/admin-panel/src/App.tsx new file mode 100644 index 000000000..4e4b9ecb7 --- /dev/null +++ b/apps/admin-panel/src/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from 'react-router-dom' +import { router } from './routes' + +export default function App() { + return +} diff --git a/apps/admin-panel/src/main.tsx b/apps/admin-panel/src/main.tsx new file mode 100644 index 000000000..fda61b388 --- /dev/null +++ b/apps/admin-panel/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/apps/admin-panel/src/pages/Dashboard.tsx b/apps/admin-panel/src/pages/Dashboard.tsx new file mode 100644 index 000000000..77a19ea53 --- /dev/null +++ b/apps/admin-panel/src/pages/Dashboard.tsx @@ -0,0 +1,3 @@ +export default function Dashboard() { + return
Admin Dashboard
+} diff --git a/apps/admin-panel/src/pages/Users.tsx b/apps/admin-panel/src/pages/Users.tsx new file mode 100644 index 000000000..6e60af6c0 --- /dev/null +++ b/apps/admin-panel/src/pages/Users.tsx @@ -0,0 +1,3 @@ +export default function Users() { + return
Users
+} diff --git a/apps/admin-panel/src/pages/VenueApprovals.tsx b/apps/admin-panel/src/pages/VenueApprovals.tsx new file mode 100644 index 000000000..66050882f --- /dev/null +++ b/apps/admin-panel/src/pages/VenueApprovals.tsx @@ -0,0 +1,3 @@ +export default function VenueApprovals() { + return
VenueApprovals
+} diff --git a/apps/admin-panel/src/routes.tsx b/apps/admin-panel/src/routes.tsx new file mode 100644 index 000000000..4d6cb038e --- /dev/null +++ b/apps/admin-panel/src/routes.tsx @@ -0,0 +1,10 @@ +import { createBrowserRouter } from 'react-router-dom' +import Dashboard from './pages/Dashboard' +import VenueApprovals from './pages/VenueApprovals' +import Users from './pages/Users' + +export const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/approvals', element: }, + { path: '/users', element: }, +]) diff --git a/apps/admin-panel/src/styles/index.css b/apps/admin-panel/src/styles/index.css new file mode 100644 index 000000000..09a262f28 --- /dev/null +++ b/apps/admin-panel/src/styles/index.css @@ -0,0 +1,9 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: sans-serif; +} diff --git a/apps/admin-panel/tsconfig.json b/apps/admin-panel/tsconfig.json new file mode 100644 index 000000000..351ec2e37 --- /dev/null +++ b/apps/admin-panel/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/apps/admin-panel/vite.config.ts b/apps/admin-panel/vite.config.ts new file mode 100644 index 000000000..aad496d54 --- /dev/null +++ b/apps/admin-panel/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3002, + proxy: { + '/api': 'http://localhost:8000', + }, + }, +}) diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 000000000..9f9b69cbb --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,10 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/venue404 +JWT_SECRET=change-me +JWT_ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=noreply@example.com +SMTP_PASSWORD=secret diff --git a/apps/api/.python-version b/apps/api/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/apps/api/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 000000000..7f76005f8 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY pyproject.toml . +RUN pip install --no-cache-dir uv && uv pip install --system -e . + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/apps/api/alembic.ini b/apps/api/alembic.ini new file mode 100644 index 000000000..1d20bcd15 --- /dev/null +++ b/apps/api/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/bookmyvenue + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +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/apps/api/alembic/versions/.gitkeep b/apps/api/alembic/versions/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/app/__init__.py b/apps/api/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/app/core/config.py b/apps/api/app/core/config.py new file mode 100644 index 000000000..13f8df454 --- /dev/null +++ b/apps/api/app/core/config.py @@ -0,0 +1,20 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + database_url: str + jwt_secret: str + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + stripe_secret_key: str = "" + stripe_webhook_secret: str = "" + smtp_host: str = "" + smtp_port: int = 587 + smtp_user: str = "" + smtp_password: str = "" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/apps/api/app/core/database.py b/apps/api/app/core/database.py new file mode 100644 index 000000000..df02deb2f --- /dev/null +++ b/apps/api/app/core/database.py @@ -0,0 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from app.core.config import settings + +engine = create_engine(settings.database_url) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +class Base(DeclarativeBase): + pass + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/apps/api/app/core/exceptions.py b/apps/api/app/core/exceptions.py new file mode 100644 index 000000000..20e55d565 --- /dev/null +++ b/apps/api/app/core/exceptions.py @@ -0,0 +1,21 @@ +from fastapi import HTTPException, status + + +class NotFoundError(HTTPException): + def __init__(self, detail: str = "Not found"): + super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail) + + +class UnauthorizedError(HTTPException): + def __init__(self, detail: str = "Unauthorized"): + super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail) + + +class ForbiddenError(HTTPException): + def __init__(self, detail: str = "Forbidden"): + super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail) + + +class ConflictError(HTTPException): + def __init__(self, detail: str = "Conflict"): + super().__init__(status_code=status.HTTP_409_CONFLICT, detail=detail) diff --git a/apps/api/app/core/middleware.py b/apps/api/app/core/middleware.py new file mode 100644 index 000000000..fa5980cdd --- /dev/null +++ b/apps/api/app/core/middleware.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + + +def register_middleware(app: FastAPI) -> None: + app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:3001", "http://localhost:3002"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) diff --git a/apps/api/app/core/security.py b/apps/api/app/core/security.py new file mode 100644 index 000000000..5602c1e2e --- /dev/null +++ b/apps/api/app/core/security.py @@ -0,0 +1,24 @@ +from datetime import datetime, timedelta +from jose import jwt +from passlib.context import CryptContext +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain: str, hashed: str) -> bool: + return pwd_context.verify(plain, hashed) + + +def create_access_token(data: dict) -> str: + payload = data.copy() + payload["exp"] = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) + + +def decode_token(token: str) -> dict: + return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) diff --git a/apps/api/app/jobs/booking_completion.py b/apps/api/app/jobs/booking_completion.py new file mode 100644 index 000000000..20989ca9d --- /dev/null +++ b/apps/api/app/jobs/booking_completion.py @@ -0,0 +1,3 @@ +def run(): + """Mark bookings as completed once the event date has passed.""" + pass diff --git a/apps/api/app/jobs/hold_expiry.py b/apps/api/app/jobs/hold_expiry.py new file mode 100644 index 000000000..9876d2da6 --- /dev/null +++ b/apps/api/app/jobs/hold_expiry.py @@ -0,0 +1,3 @@ +def run(): + """Cancel bookings whose 24-hour advance payment window has expired.""" + pass diff --git a/apps/api/app/jobs/payment_reminders.py b/apps/api/app/jobs/payment_reminders.py new file mode 100644 index 000000000..9664b4376 --- /dev/null +++ b/apps/api/app/jobs/payment_reminders.py @@ -0,0 +1,3 @@ +def run(): + """Send payment reminders at T-7, T-3, and T-1 days before event.""" + pass diff --git a/apps/api/app/jobs/scheduler.py b/apps/api/app/jobs/scheduler.py new file mode 100644 index 000000000..04675939d --- /dev/null +++ b/apps/api/app/jobs/scheduler.py @@ -0,0 +1,12 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from app.jobs import hold_expiry, stale_requests, payment_reminders, booking_completion + +scheduler = BackgroundScheduler() + + +def start(): + scheduler.add_job(hold_expiry.run, "interval", hours=1, id="hold_expiry") + scheduler.add_job(stale_requests.run, "interval", hours=6, id="stale_requests") + scheduler.add_job(payment_reminders.run, "cron", hour=8, id="payment_reminders") + scheduler.add_job(booking_completion.run, "cron", hour=0, id="booking_completion") + scheduler.start() diff --git a/apps/api/app/jobs/stale_requests.py b/apps/api/app/jobs/stale_requests.py new file mode 100644 index 000000000..1f2c40837 --- /dev/null +++ b/apps/api/app/jobs/stale_requests.py @@ -0,0 +1,3 @@ +def run(): + """Auto-expire booking requests that have been pending for 7 days.""" + pass diff --git a/apps/api/app/main.py b/apps/api/app/main.py new file mode 100644 index 000000000..727fedccb --- /dev/null +++ b/apps/api/app/main.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from app.core.middleware import register_middleware +from app.modules.auth.routes import router as auth_router +from app.modules.profile.routes import router as profile_router +from app.modules.venue.routes import router as venue_router +from app.modules.search.routes import router as search_router +from app.modules.booking.routes import router as booking_router +from app.modules.availability.routes import router as availability_router +from app.modules.notification.routes import router as notification_router +from app.modules.admin.routes import router as admin_router +from app.modules.payment.routes import router as payment_router + +app = FastAPI(title="BookMyVenue API") + +register_middleware(app) + +app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) +app.include_router(profile_router, prefix="/api/profile", tags=["profile"]) +app.include_router(venue_router, prefix="/api/venues", tags=["venues"]) +app.include_router(search_router, prefix="/api/search", tags=["search"]) +app.include_router(booking_router, prefix="/api/bookings", tags=["bookings"]) +app.include_router(availability_router, prefix="/api/availability", tags=["availability"]) +app.include_router(notification_router, prefix="/api/notifications", tags=["notifications"]) +app.include_router(admin_router, prefix="/api/admin", tags=["admin"]) +app.include_router(payment_router, prefix="/api/payments", tags=["payments"]) diff --git a/apps/api/app/modules/admin/routes.py b/apps/api/app/modules/admin/routes.py new file mode 100644 index 000000000..f0cb07ae1 --- /dev/null +++ b/apps/api/app/modules/admin/routes.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter, Depends +from app.modules.admin.schemas import VenueApprovalRequest +from app.modules.auth.dependencies import require_role +from app.modules.admin import service + +router = APIRouter() + + +@router.patch("/venues/{venue_id}/approve", status_code=204) +def approve_venue(venue_id: str, body: VenueApprovalRequest, user=Depends(require_role("admin"))): + service.approve_venue(venue_id, body) diff --git a/apps/api/app/modules/admin/schemas.py b/apps/api/app/modules/admin/schemas.py new file mode 100644 index 000000000..3967daec1 --- /dev/null +++ b/apps/api/app/modules/admin/schemas.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from typing import Literal + + +class VenueApprovalRequest(BaseModel): + action: Literal["approve", "reject"] + reason: str = "" diff --git a/apps/api/app/modules/admin/service.py b/apps/api/app/modules/admin/service.py new file mode 100644 index 000000000..acff3fba0 --- /dev/null +++ b/apps/api/app/modules/admin/service.py @@ -0,0 +1,5 @@ +from app.modules.admin.schemas import VenueApprovalRequest + + +def approve_venue(venue_id: str, body: VenueApprovalRequest) -> None: + raise NotImplementedError diff --git a/apps/api/app/modules/auth/__init__.py b/apps/api/app/modules/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/app/modules/auth/dependencies.py b/apps/api/app/modules/auth/dependencies.py new file mode 100644 index 000000000..60a01078b --- /dev/null +++ b/apps/api/app/modules/auth/dependencies.py @@ -0,0 +1,21 @@ +from fastapi import Depends, Header +from app.core.security import decode_token +from app.core.exceptions import UnauthorizedError + + +def get_current_user(authorization: str = Header(...)): + try: + scheme, token = authorization.split() + if scheme.lower() != "bearer": + raise UnauthorizedError() + return decode_token(token) + except Exception: + raise UnauthorizedError() + + +def require_role(*roles: str): + def dependency(user=Depends(get_current_user)): + if user.get("role") not in roles: + raise UnauthorizedError("Insufficient permissions") + return user + return dependency diff --git a/apps/api/app/modules/auth/routes.py b/apps/api/app/modules/auth/routes.py new file mode 100644 index 000000000..4f7ae2d11 --- /dev/null +++ b/apps/api/app/modules/auth/routes.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter +from app.modules.auth.schemas import LoginRequest, TokenResponse, RegisterRequest +from app.modules.auth import service + +router = APIRouter() + + +@router.post("/register", response_model=TokenResponse) +def register(body: RegisterRequest): + return service.register(body) + + +@router.post("/login", response_model=TokenResponse) +def login(body: LoginRequest): + return service.login(body) diff --git a/apps/api/app/modules/auth/schemas.py b/apps/api/app/modules/auth/schemas.py new file mode 100644 index 000000000..e4fb34ffd --- /dev/null +++ b/apps/api/app/modules/auth/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, EmailStr + + +class RegisterRequest(BaseModel): + email: EmailStr + password: str + full_name: str + + +class LoginRequest(BaseModel): + email: EmailStr + password: str + + +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" diff --git a/apps/api/app/modules/auth/service.py b/apps/api/app/modules/auth/service.py new file mode 100644 index 000000000..746752ca6 --- /dev/null +++ b/apps/api/app/modules/auth/service.py @@ -0,0 +1,9 @@ +from app.modules.auth.schemas import LoginRequest, RegisterRequest, TokenResponse + + +def register(body: RegisterRequest) -> TokenResponse: + raise NotImplementedError + + +def login(body: LoginRequest) -> TokenResponse: + raise NotImplementedError diff --git a/apps/api/app/modules/availability/models.py b/apps/api/app/modules/availability/models.py new file mode 100644 index 000000000..f8a7d7123 --- /dev/null +++ b/apps/api/app/modules/availability/models.py @@ -0,0 +1,22 @@ +from sqlalchemy import String, Date, Boolean, ForeignKey, Text +from sqlalchemy.orm import mapped_column, Mapped +from app.core.database import Base +from app.shared.models import TimestampMixin + + +class Slot(Base, TimestampMixin): + __tablename__ = "slots" + + id: Mapped[str] = mapped_column(String, primary_key=True) + venue_id: Mapped[str] = mapped_column(String, ForeignKey("venues.id"), nullable=False) + date: Mapped[str] = mapped_column(Date, nullable=False) + is_available: Mapped[bool] = mapped_column(Boolean, default=True) + + +class BlockedDate(Base, TimestampMixin): + __tablename__ = "blocked_dates" + + id: Mapped[str] = mapped_column(String, primary_key=True) + venue_id: Mapped[str] = mapped_column(String, ForeignKey("venues.id"), nullable=False) + date: Mapped[str] = mapped_column(Date, nullable=False) + reason: Mapped[str] = mapped_column(Text, default="") diff --git a/apps/api/app/modules/availability/routes.py b/apps/api/app/modules/availability/routes.py new file mode 100644 index 000000000..ef1c00b89 --- /dev/null +++ b/apps/api/app/modules/availability/routes.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends +from app.modules.availability.schemas import SlotResponse, BlockDateRequest +from app.modules.auth.dependencies import get_current_user +from app.modules.availability import service + +router = APIRouter() + + +@router.get("/{venue_id}/slots", response_model=list[SlotResponse]) +def get_slots(venue_id: str): + return service.get_slots(venue_id) + + +@router.post("/{venue_id}/block", status_code=204) +def block_date(venue_id: str, body: BlockDateRequest, user=Depends(get_current_user)): + service.block_date(venue_id, user["sub"], body) diff --git a/apps/api/app/modules/availability/schemas.py b/apps/api/app/modules/availability/schemas.py new file mode 100644 index 000000000..7b5fb3d39 --- /dev/null +++ b/apps/api/app/modules/availability/schemas.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel +from datetime import date + + +class SlotResponse(BaseModel): + date: date + is_available: bool + + +class BlockDateRequest(BaseModel): + date: date + reason: str = "" diff --git a/apps/api/app/modules/availability/service.py b/apps/api/app/modules/availability/service.py new file mode 100644 index 000000000..2d1bd350a --- /dev/null +++ b/apps/api/app/modules/availability/service.py @@ -0,0 +1,10 @@ +from app.modules.availability.schemas import SlotResponse, BlockDateRequest +from typing import List + + +def get_slots(venue_id: str) -> List[SlotResponse]: + raise NotImplementedError + + +def block_date(venue_id: str, owner_id: str, body: BlockDateRequest) -> None: + raise NotImplementedError diff --git a/apps/api/app/modules/booking/models.py b/apps/api/app/modules/booking/models.py new file mode 100644 index 000000000..fdeaaeb25 --- /dev/null +++ b/apps/api/app/modules/booking/models.py @@ -0,0 +1,36 @@ +from sqlalchemy import String, Date, Enum, ForeignKey, Text +from sqlalchemy.orm import mapped_column, Mapped, relationship +from app.core.database import Base +from app.shared.models import TimestampMixin +import enum + + +class BookingStatus(str, enum.Enum): + requested = "requested" + accepted = "accepted" + confirmed = "confirmed" + cancelled = "cancelled" + completed = "completed" + + +class Booking(Base, TimestampMixin): + __tablename__ = "bookings" + + id: Mapped[str] = mapped_column(String, primary_key=True) + venue_id: Mapped[str] = mapped_column(String, ForeignKey("venues.id"), nullable=False) + user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), nullable=False) + start_date: Mapped[str] = mapped_column(Date, nullable=False) + end_date: Mapped[str] = mapped_column(Date, nullable=False) + status: Mapped[BookingStatus] = mapped_column(Enum(BookingStatus), default=BookingStatus.requested) + notes: Mapped[str] = mapped_column(Text, default="") + history: Mapped[list["StatusHistory"]] = relationship(back_populates="booking") + + +class StatusHistory(Base, TimestampMixin): + __tablename__ = "booking_status_history" + + id: Mapped[str] = mapped_column(String, primary_key=True) + booking_id: Mapped[str] = mapped_column(String, ForeignKey("bookings.id"), nullable=False) + from_status: Mapped[str] = mapped_column(String, nullable=False) + to_status: Mapped[str] = mapped_column(String, nullable=False) + booking: Mapped["Booking"] = relationship(back_populates="history") diff --git a/apps/api/app/modules/booking/routes.py b/apps/api/app/modules/booking/routes.py new file mode 100644 index 000000000..3fc2db78f --- /dev/null +++ b/apps/api/app/modules/booking/routes.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends +from app.modules.booking.schemas import BookingResponse, CreateBookingRequest +from app.modules.auth.dependencies import get_current_user +from app.modules.booking import service + +router = APIRouter() + + +@router.get("/{booking_id}", response_model=BookingResponse) +def get_booking(booking_id: str, user=Depends(get_current_user)): + return service.get_booking(booking_id) + + +@router.post("/", response_model=BookingResponse, status_code=201) +def create_booking(body: CreateBookingRequest, user=Depends(get_current_user)): + return service.create_booking(user["sub"], body) diff --git a/apps/api/app/modules/booking/schemas.py b/apps/api/app/modules/booking/schemas.py new file mode 100644 index 000000000..e0979b6b6 --- /dev/null +++ b/apps/api/app/modules/booking/schemas.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from datetime import date + + +class CreateBookingRequest(BaseModel): + venue_id: str + start_date: date + end_date: date + notes: str = "" + + +class BookingResponse(BaseModel): + id: str + venue_id: str + user_id: str + start_date: date + end_date: date + status: str diff --git a/apps/api/app/modules/booking/service.py b/apps/api/app/modules/booking/service.py new file mode 100644 index 000000000..cadf27772 --- /dev/null +++ b/apps/api/app/modules/booking/service.py @@ -0,0 +1,9 @@ +from app.modules.booking.schemas import BookingResponse, CreateBookingRequest + + +def get_booking(booking_id: str) -> BookingResponse: + raise NotImplementedError + + +def create_booking(user_id: str, body: CreateBookingRequest) -> BookingResponse: + raise NotImplementedError diff --git a/apps/api/app/modules/booking/state_machine.py b/apps/api/app/modules/booking/state_machine.py new file mode 100644 index 000000000..15adbc5fd --- /dev/null +++ b/apps/api/app/modules/booking/state_machine.py @@ -0,0 +1,13 @@ +from app.modules.booking.models import BookingStatus + +VALID_TRANSITIONS: dict[BookingStatus, set[BookingStatus]] = { + BookingStatus.requested: {BookingStatus.accepted, BookingStatus.cancelled}, + BookingStatus.accepted: {BookingStatus.confirmed, BookingStatus.cancelled}, + BookingStatus.confirmed: {BookingStatus.completed, BookingStatus.cancelled}, + BookingStatus.cancelled: set(), + BookingStatus.completed: set(), +} + + +def can_transition(current: BookingStatus, next_status: BookingStatus) -> bool: + return next_status in VALID_TRANSITIONS.get(current, set()) diff --git a/apps/api/app/modules/notification/models.py b/apps/api/app/modules/notification/models.py new file mode 100644 index 000000000..faf230b5c --- /dev/null +++ b/apps/api/app/modules/notification/models.py @@ -0,0 +1,13 @@ +from sqlalchemy import String, Boolean, Text, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped +from app.core.database import Base +from app.shared.models import TimestampMixin + + +class InAppNotification(Base, TimestampMixin): + __tablename__ = "notifications" + + id: Mapped[str] = mapped_column(String, primary_key=True) + user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), nullable=False) + message: Mapped[str] = mapped_column(Text, nullable=False) + is_read: Mapped[bool] = mapped_column(Boolean, default=False) diff --git a/apps/api/app/modules/notification/routes.py b/apps/api/app/modules/notification/routes.py new file mode 100644 index 000000000..6d3435137 --- /dev/null +++ b/apps/api/app/modules/notification/routes.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends +from app.modules.notification.schemas import NotificationResponse +from app.modules.auth.dependencies import get_current_user +from app.modules.notification import service + +router = APIRouter() + + +@router.get("/", response_model=list[NotificationResponse]) +def list_notifications(user=Depends(get_current_user)): + return service.list_notifications(user["sub"]) + + +@router.patch("/{notification_id}/read", status_code=204) +def mark_read(notification_id: str, user=Depends(get_current_user)): + service.mark_read(notification_id, user["sub"]) diff --git a/apps/api/app/modules/notification/schemas.py b/apps/api/app/modules/notification/schemas.py new file mode 100644 index 000000000..305d07948 --- /dev/null +++ b/apps/api/app/modules/notification/schemas.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from datetime import datetime + + +class NotificationResponse(BaseModel): + id: str + user_id: str + message: str + is_read: bool + created_at: datetime diff --git a/apps/api/app/modules/notification/service.py b/apps/api/app/modules/notification/service.py new file mode 100644 index 000000000..cecea67a0 --- /dev/null +++ b/apps/api/app/modules/notification/service.py @@ -0,0 +1,10 @@ +from app.modules.notification.schemas import NotificationResponse +from typing import List + + +def list_notifications(user_id: str) -> List[NotificationResponse]: + raise NotImplementedError + + +def mark_read(notification_id: str, user_id: str) -> None: + raise NotImplementedError diff --git a/apps/api/app/modules/notification/templates/.gitkeep b/apps/api/app/modules/notification/templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/app/modules/payment/models.py b/apps/api/app/modules/payment/models.py new file mode 100644 index 000000000..1cf8b9c69 --- /dev/null +++ b/apps/api/app/modules/payment/models.py @@ -0,0 +1,30 @@ +from sqlalchemy import String, Float, Enum, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped +from app.core.database import Base +from app.shared.models import TimestampMixin +import enum + + +class PaymentStatus(str, enum.Enum): + pending = "pending" + succeeded = "succeeded" + failed = "failed" + refunded = "refunded" + + +class Payment(Base, TimestampMixin): + __tablename__ = "payments" + + id: Mapped[str] = mapped_column(String, primary_key=True) + booking_id: Mapped[str] = mapped_column(String, ForeignKey("bookings.id"), nullable=False) + amount: Mapped[float] = mapped_column(Float, nullable=False) + status: Mapped[PaymentStatus] = mapped_column(Enum(PaymentStatus), default=PaymentStatus.pending) + stripe_payment_intent_id: Mapped[str] = mapped_column(String, nullable=False) + + +class Refund(Base, TimestampMixin): + __tablename__ = "refunds" + + id: Mapped[str] = mapped_column(String, primary_key=True) + payment_id: Mapped[str] = mapped_column(String, ForeignKey("payments.id"), nullable=False) + amount: Mapped[float] = mapped_column(Float, nullable=False) diff --git a/apps/api/app/modules/payment/routes.py b/apps/api/app/modules/payment/routes.py new file mode 100644 index 000000000..f57bfbf52 --- /dev/null +++ b/apps/api/app/modules/payment/routes.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends, Request +from app.modules.payment.schemas import PaymentResponse, CreatePaymentRequest +from app.modules.auth.dependencies import get_current_user +from app.modules.payment import service, webhooks + +router = APIRouter() + + +@router.post("/", response_model=PaymentResponse, status_code=201) +def create_payment(body: CreatePaymentRequest, user=Depends(get_current_user)): + return service.create_payment(user["sub"], body) + + +@router.post("/webhook") +async def stripe_webhook(request: Request): + return await webhooks.handle(request) diff --git a/apps/api/app/modules/payment/schemas.py b/apps/api/app/modules/payment/schemas.py new file mode 100644 index 000000000..6e83c03a6 --- /dev/null +++ b/apps/api/app/modules/payment/schemas.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + + +class CreatePaymentRequest(BaseModel): + booking_id: str + amount: float + + +class PaymentResponse(BaseModel): + id: str + booking_id: str + amount: float + status: str + stripe_payment_intent_id: str diff --git a/apps/api/app/modules/payment/service.py b/apps/api/app/modules/payment/service.py new file mode 100644 index 000000000..64b40cd63 --- /dev/null +++ b/apps/api/app/modules/payment/service.py @@ -0,0 +1,5 @@ +from app.modules.payment.schemas import PaymentResponse, CreatePaymentRequest + + +def create_payment(user_id: str, body: CreatePaymentRequest) -> PaymentResponse: + raise NotImplementedError diff --git a/apps/api/app/modules/payment/webhooks.py b/apps/api/app/modules/payment/webhooks.py new file mode 100644 index 000000000..45939e06b --- /dev/null +++ b/apps/api/app/modules/payment/webhooks.py @@ -0,0 +1,5 @@ +from fastapi import Request + + +async def handle(request: Request): + raise NotImplementedError diff --git a/apps/api/app/modules/profile/models.py b/apps/api/app/modules/profile/models.py new file mode 100644 index 000000000..57a806012 --- /dev/null +++ b/apps/api/app/modules/profile/models.py @@ -0,0 +1,21 @@ +from sqlalchemy import String, Enum +from sqlalchemy.orm import mapped_column, Mapped +from app.core.database import Base +from app.shared.models import TimestampMixin +import enum + + +class UserRole(str, enum.Enum): + user = "user" + owner = "owner" + admin = "admin" + + +class User(Base, TimestampMixin): + __tablename__ = "users" + + id: Mapped[str] = mapped_column(String, primary_key=True) + email: Mapped[str] = mapped_column(String, unique=True, nullable=False) + hashed_password: Mapped[str] = mapped_column(String, nullable=False) + full_name: Mapped[str] = mapped_column(String, nullable=False) + role: Mapped[UserRole] = mapped_column(Enum(UserRole), default=UserRole.user) diff --git a/apps/api/app/modules/profile/routes.py b/apps/api/app/modules/profile/routes.py new file mode 100644 index 000000000..c70240e46 --- /dev/null +++ b/apps/api/app/modules/profile/routes.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter +from app.modules.profile.schemas import ProfileResponse, UpdateProfileRequest +from app.modules.auth.dependencies import get_current_user +from fastapi import Depends +from app.modules.profile import service + +router = APIRouter() + + +@router.get("/me", response_model=ProfileResponse) +def get_profile(user=Depends(get_current_user)): + return service.get_profile(user["sub"]) + + +@router.patch("/me", response_model=ProfileResponse) +def update_profile(body: UpdateProfileRequest, user=Depends(get_current_user)): + return service.update_profile(user["sub"], body) diff --git a/apps/api/app/modules/profile/schemas.py b/apps/api/app/modules/profile/schemas.py new file mode 100644 index 000000000..ed1868e16 --- /dev/null +++ b/apps/api/app/modules/profile/schemas.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional + + +class ProfileResponse(BaseModel): + id: str + email: EmailStr + full_name: str + role: str + + +class UpdateProfileRequest(BaseModel): + full_name: Optional[str] = None diff --git a/apps/api/app/modules/profile/service.py b/apps/api/app/modules/profile/service.py new file mode 100644 index 000000000..be131d0fc --- /dev/null +++ b/apps/api/app/modules/profile/service.py @@ -0,0 +1,9 @@ +from app.modules.profile.schemas import ProfileResponse, UpdateProfileRequest + + +def get_profile(user_id: str) -> ProfileResponse: + raise NotImplementedError + + +def update_profile(user_id: str, body: UpdateProfileRequest) -> ProfileResponse: + raise NotImplementedError diff --git a/apps/api/app/modules/search/routes.py b/apps/api/app/modules/search/routes.py new file mode 100644 index 000000000..be8418b11 --- /dev/null +++ b/apps/api/app/modules/search/routes.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Query +from app.modules.search.schemas import SearchParams, SearchResult +from app.modules.search import service + +router = APIRouter() + + +@router.get("/", response_model=list[SearchResult]) +def search_venues( + q: str = Query(default=""), + city: str = Query(default=""), + capacity: int = Query(default=0), + page: int = Query(default=1), +): + return service.search(SearchParams(q=q, city=city, capacity=capacity, page=page)) diff --git a/apps/api/app/modules/search/schemas.py b/apps/api/app/modules/search/schemas.py new file mode 100644 index 000000000..b22dcc76c --- /dev/null +++ b/apps/api/app/modules/search/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel +from typing import Optional + + +class SearchParams(BaseModel): + q: str = "" + city: str = "" + capacity: int = 0 + page: int = 1 + + +class SearchResult(BaseModel): + id: str + name: str + address: str + capacity: int + price_per_day: float diff --git a/apps/api/app/modules/search/service.py b/apps/api/app/modules/search/service.py new file mode 100644 index 000000000..bd78d3bc5 --- /dev/null +++ b/apps/api/app/modules/search/service.py @@ -0,0 +1,6 @@ +from app.modules.search.schemas import SearchParams, SearchResult +from typing import List + + +def search(params: SearchParams) -> List[SearchResult]: + raise NotImplementedError diff --git a/apps/api/app/modules/venue/models.py b/apps/api/app/modules/venue/models.py new file mode 100644 index 000000000..adcc7c34f --- /dev/null +++ b/apps/api/app/modules/venue/models.py @@ -0,0 +1,36 @@ +from sqlalchemy import String, Integer, Float, Enum, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship +from app.core.database import Base +from app.shared.models import TimestampMixin +import enum + + +class ApprovalStatus(str, enum.Enum): + pending = "pending" + approved = "approved" + rejected = "rejected" + + +class Venue(Base, TimestampMixin): + __tablename__ = "venues" + + id: Mapped[str] = mapped_column(String, primary_key=True) + owner_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + description: Mapped[str] = mapped_column(String) + address: Mapped[str] = mapped_column(String, nullable=False) + capacity: Mapped[int] = mapped_column(Integer, nullable=False) + price_per_day: Mapped[float] = mapped_column(Float, nullable=False) + approval_status: Mapped[ApprovalStatus] = mapped_column( + Enum(ApprovalStatus), default=ApprovalStatus.pending + ) + photos: Mapped[list["VenuePhoto"]] = relationship(back_populates="venue") + + +class VenuePhoto(Base, TimestampMixin): + __tablename__ = "venue_photos" + + id: Mapped[str] = mapped_column(String, primary_key=True) + venue_id: Mapped[str] = mapped_column(String, ForeignKey("venues.id"), nullable=False) + url: Mapped[str] = mapped_column(String, nullable=False) + venue: Mapped["Venue"] = relationship(back_populates="photos") diff --git a/apps/api/app/modules/venue/routes.py b/apps/api/app/modules/venue/routes.py new file mode 100644 index 000000000..04f9481aa --- /dev/null +++ b/apps/api/app/modules/venue/routes.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends +from app.modules.venue.schemas import VenueResponse, CreateVenueRequest +from app.modules.auth.dependencies import get_current_user +from app.modules.venue import service + +router = APIRouter() + + +@router.get("/{venue_id}", response_model=VenueResponse) +def get_venue(venue_id: str): + return service.get_venue(venue_id) + + +@router.post("/", response_model=VenueResponse, status_code=201) +def create_venue(body: CreateVenueRequest, user=Depends(get_current_user)): + return service.create_venue(user["sub"], body) diff --git a/apps/api/app/modules/venue/schemas.py b/apps/api/app/modules/venue/schemas.py new file mode 100644 index 000000000..3b7a363b1 --- /dev/null +++ b/apps/api/app/modules/venue/schemas.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from typing import Optional, List + + +class CreateVenueRequest(BaseModel): + name: str + description: str + address: str + capacity: int + price_per_day: float + + +class VenueResponse(BaseModel): + id: str + name: str + description: str + address: str + capacity: int + price_per_day: float + approval_status: str + owner_id: str diff --git a/apps/api/app/modules/venue/service.py b/apps/api/app/modules/venue/service.py new file mode 100644 index 000000000..4dd23090f --- /dev/null +++ b/apps/api/app/modules/venue/service.py @@ -0,0 +1,9 @@ +from app.modules.venue.schemas import VenueResponse, CreateVenueRequest + + +def get_venue(venue_id: str) -> VenueResponse: + raise NotImplementedError + + +def create_venue(owner_id: str, body: CreateVenueRequest) -> VenueResponse: + raise NotImplementedError diff --git a/apps/api/app/shared/models.py b/apps/api/app/shared/models.py new file mode 100644 index 000000000..ec6121c33 --- /dev/null +++ b/apps/api/app/shared/models.py @@ -0,0 +1,11 @@ +from datetime import datetime +from sqlalchemy import DateTime, func +from sqlalchemy.orm import mapped_column, MappedColumn +from app.core.database import Base + + +class TimestampMixin: + created_at: MappedColumn[datetime] = mapped_column(DateTime, default=func.now(), nullable=False) + updated_at: MappedColumn[datetime] = mapped_column( + DateTime, default=func.now(), onupdate=func.now(), nullable=False + ) diff --git a/apps/api/app/shared/pagination.py b/apps/api/app/shared/pagination.py new file mode 100644 index 000000000..681413202 --- /dev/null +++ b/apps/api/app/shared/pagination.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from typing import Generic, TypeVar, List + +T = TypeVar("T") + + +class PaginationParams(BaseModel): + page: int = 1 + page_size: int = 20 + + +class Page(BaseModel, Generic[T]): + items: List[T] + total: int + page: int + page_size: int diff --git a/apps/api/app/shared/utils.py b/apps/api/app/shared/utils.py new file mode 100644 index 000000000..f368f1325 --- /dev/null +++ b/apps/api/app/shared/utils.py @@ -0,0 +1,5 @@ +import uuid + + +def generate_id() -> str: + return str(uuid.uuid4()) diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml new file mode 100644 index 000000000..93f93b044 --- /dev/null +++ b/apps/api/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "venue404-api" +version = "0.0.1" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.111.0", + "uvicorn[standard]>=0.30.0", + "sqlalchemy>=2.0.0", + "alembic>=1.13.0", + "psycopg2-binary>=2.9.0", + "pydantic-settings>=2.3.0", + "passlib[bcrypt]>=1.7.4", + "python-jose[cryptography]>=3.3.0", + "stripe>=9.0.0", + "apscheduler>=3.10.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.27.0", + "pytest-asyncio>=0.23.0", +] diff --git a/apps/api/tests/conftest.py b/apps/api/tests/conftest.py new file mode 100644 index 000000000..555264af3 --- /dev/null +++ b/apps/api/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + + +@pytest.fixture +def client(): + return TestClient(app) diff --git a/apps/api/tests/test_booking.py b/apps/api/tests/test_booking.py new file mode 100644 index 000000000..201975fcc --- /dev/null +++ b/apps/api/tests/test_booking.py @@ -0,0 +1,2 @@ +def test_placeholder(): + pass diff --git a/apps/api/tests/test_venue.py b/apps/api/tests/test_venue.py new file mode 100644 index 000000000..201975fcc --- /dev/null +++ b/apps/api/tests/test_venue.py @@ -0,0 +1,2 @@ +def test_placeholder(): + pass diff --git a/apps/owner-portal/.env.example b/apps/owner-portal/.env.example new file mode 100644 index 000000000..c2058b5ea --- /dev/null +++ b/apps/owner-portal/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/apps/owner-portal/index.html b/apps/owner-portal/index.html new file mode 100644 index 000000000..b084ac4bb --- /dev/null +++ b/apps/owner-portal/index.html @@ -0,0 +1,12 @@ + + + + + + BookMyVenue — Owner Portal + + +
+ + + diff --git a/apps/owner-portal/package.json b/apps/owner-portal/package.json new file mode 100644 index 000000000..4077cf1b7 --- /dev/null +++ b/apps/owner-portal/package.json @@ -0,0 +1,25 @@ +{ + "name": "@venue404/owner-portal", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src", + "preview": "vite preview" + }, + "dependencies": { + "@venue404/api-client": "workspace:*", + "@venue404/ui": "workspace:*", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.3.0" + } +} \ No newline at end of file diff --git a/apps/owner-portal/src/App.tsx b/apps/owner-portal/src/App.tsx new file mode 100644 index 000000000..4e4b9ecb7 --- /dev/null +++ b/apps/owner-portal/src/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from 'react-router-dom' +import { router } from './routes' + +export default function App() { + return +} diff --git a/apps/owner-portal/src/main.tsx b/apps/owner-portal/src/main.tsx new file mode 100644 index 000000000..fda61b388 --- /dev/null +++ b/apps/owner-portal/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/apps/owner-portal/src/pages/Bookings.tsx b/apps/owner-portal/src/pages/Bookings.tsx new file mode 100644 index 000000000..5792f6d9d --- /dev/null +++ b/apps/owner-portal/src/pages/Bookings.tsx @@ -0,0 +1,3 @@ +export default function Bookings() { + return
Bookings
+} diff --git a/apps/owner-portal/src/pages/Dashboard.tsx b/apps/owner-portal/src/pages/Dashboard.tsx new file mode 100644 index 000000000..761e6dbf6 --- /dev/null +++ b/apps/owner-portal/src/pages/Dashboard.tsx @@ -0,0 +1,3 @@ +export default function Dashboard() { + return
Dashboard
+} diff --git a/apps/owner-portal/src/pages/ManageVenues.tsx b/apps/owner-portal/src/pages/ManageVenues.tsx new file mode 100644 index 000000000..a0a119f3f --- /dev/null +++ b/apps/owner-portal/src/pages/ManageVenues.tsx @@ -0,0 +1,3 @@ +export default function ManageVenues() { + return
ManageVenues
+} diff --git a/apps/owner-portal/src/routes.tsx b/apps/owner-portal/src/routes.tsx new file mode 100644 index 000000000..ab6b7c48a --- /dev/null +++ b/apps/owner-portal/src/routes.tsx @@ -0,0 +1,10 @@ +import { createBrowserRouter } from 'react-router-dom' +import Dashboard from './pages/Dashboard' +import ManageVenues from './pages/ManageVenues' +import Bookings from './pages/Bookings' + +export const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/venues', element: }, + { path: '/bookings', element: }, +]) diff --git a/apps/owner-portal/src/styles/index.css b/apps/owner-portal/src/styles/index.css new file mode 100644 index 000000000..09a262f28 --- /dev/null +++ b/apps/owner-portal/src/styles/index.css @@ -0,0 +1,9 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: sans-serif; +} diff --git a/apps/owner-portal/tsconfig.json b/apps/owner-portal/tsconfig.json new file mode 100644 index 000000000..351ec2e37 --- /dev/null +++ b/apps/owner-portal/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/apps/owner-portal/vite.config.ts b/apps/owner-portal/vite.config.ts new file mode 100644 index 000000000..25455554a --- /dev/null +++ b/apps/owner-portal/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3001, + proxy: { + '/api': 'http://localhost:8000', + }, + }, +}) diff --git a/apps/user-web/.env.example b/apps/user-web/.env.example new file mode 100644 index 000000000..c2058b5ea --- /dev/null +++ b/apps/user-web/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/apps/user-web/index.html b/apps/user-web/index.html new file mode 100644 index 000000000..23f20a99a --- /dev/null +++ b/apps/user-web/index.html @@ -0,0 +1,12 @@ + + + + + + BookMyVenue + + +
+ + + diff --git a/apps/user-web/package.json b/apps/user-web/package.json new file mode 100644 index 000000000..b31a9a183 --- /dev/null +++ b/apps/user-web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@bookmyvenue/user-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src", + "preview": "vite preview" + }, + "dependencies": { + "@bookmyvenue/api-client": "workspace:*", + "@bookmyvenue/ui": "workspace:*", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.3.0" + } +} diff --git a/apps/user-web/src/App.tsx b/apps/user-web/src/App.tsx new file mode 100644 index 000000000..4e4b9ecb7 --- /dev/null +++ b/apps/user-web/src/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from 'react-router-dom' +import { router } from './routes' + +export default function App() { + return +} diff --git a/apps/user-web/src/main.tsx b/apps/user-web/src/main.tsx new file mode 100644 index 000000000..fda61b388 --- /dev/null +++ b/apps/user-web/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/apps/user-web/src/pages/Home.tsx b/apps/user-web/src/pages/Home.tsx new file mode 100644 index 000000000..95eaf1c49 --- /dev/null +++ b/apps/user-web/src/pages/Home.tsx @@ -0,0 +1,3 @@ +export default function Home() { + return
Home
+} diff --git a/apps/user-web/src/pages/MyBookings.tsx b/apps/user-web/src/pages/MyBookings.tsx new file mode 100644 index 000000000..23ec50d82 --- /dev/null +++ b/apps/user-web/src/pages/MyBookings.tsx @@ -0,0 +1,3 @@ +export default function MyBookings() { + return
MyBookings
+} diff --git a/apps/user-web/src/pages/VenueDetails.tsx b/apps/user-web/src/pages/VenueDetails.tsx new file mode 100644 index 000000000..474dc8cad --- /dev/null +++ b/apps/user-web/src/pages/VenueDetails.tsx @@ -0,0 +1,3 @@ +export default function VenueDetails() { + return
VenueDetails
+} diff --git a/apps/user-web/src/routes.tsx b/apps/user-web/src/routes.tsx new file mode 100644 index 000000000..7d5491673 --- /dev/null +++ b/apps/user-web/src/routes.tsx @@ -0,0 +1,10 @@ +import { createBrowserRouter } from 'react-router-dom' +import Home from './pages/Home' +import VenueDetails from './pages/VenueDetails' +import MyBookings from './pages/MyBookings' + +export const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/venues/:id', element: }, + { path: '/my-bookings', element: }, +]) diff --git a/apps/user-web/src/styles/index.css b/apps/user-web/src/styles/index.css new file mode 100644 index 000000000..09a262f28 --- /dev/null +++ b/apps/user-web/src/styles/index.css @@ -0,0 +1,9 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: sans-serif; +} diff --git a/apps/user-web/tsconfig.json b/apps/user-web/tsconfig.json new file mode 100644 index 000000000..351ec2e37 --- /dev/null +++ b/apps/user-web/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/apps/user-web/vite.config.ts b/apps/user-web/vite.config.ts new file mode 100644 index 000000000..bb6580811 --- /dev/null +++ b/apps/user-web/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': 'http://localhost:8000', + }, + }, +}) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..bc38d99e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.9" + +services: + db: + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: bookmyvenue + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + api: + build: ./apps/api + restart: unless-stopped + depends_on: + - db + env_file: + - .env + ports: + - "8000:8000" + volumes: + - ./apps/api:/app + +volumes: + pgdata: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..31d711873 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import tsParser from '@typescript-eslint/parser' +import tsPlugin from '@typescript-eslint/eslint-plugin' + +export default [ + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsParser, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + }, + }, +] diff --git a/package.json b/package.json new file mode 100644 index 000000000..43ef5ffe0 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "venue404", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "pnpm --parallel -r dev", + "build": "pnpm -r build", + "lint": "pnpm -r lint", + "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^9.0.0", + "prettier": "^3.0.0", + "typescript": "^5.4.0" + } +} \ No newline at end of file diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 000000000..b7400d391 --- /dev/null +++ b/packages/api-client/package.json @@ -0,0 +1,14 @@ +{ + "name": "@bookmyvenue/api-client", + "version": "0.0.0", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "generate": "tsx scripts/generate.ts", + "lint": "eslint src" + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.4.0" + } +} diff --git a/packages/api-client/scripts/generate.ts b/packages/api-client/scripts/generate.ts new file mode 100644 index 000000000..48adf90d5 --- /dev/null +++ b/packages/api-client/scripts/generate.ts @@ -0,0 +1,10 @@ +const OPENAPI_URL = process.env.OPENAPI_URL ?? 'http://localhost:8000/openapi.json' + +async function generate() { + const res = await fetch(OPENAPI_URL) + const schema = await res.json() + console.log('Fetched OpenAPI schema, generation not yet implemented') + console.log(JSON.stringify(schema, null, 2)) +} + +generate().catch(console.error) diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts new file mode 100644 index 000000000..b62876ed4 --- /dev/null +++ b/packages/api-client/src/client.ts @@ -0,0 +1,23 @@ +const BASE_URL = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_BASE_URL) ?? 'http://localhost:8000' + +export function createClient(token?: string) { + const headers: Record = { 'Content-Type': 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + + async function request(method: string, path: string, body?: unknown): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body != null ? JSON.stringify(body) : undefined, + }) + if (!res.ok) throw new Error(`${method} ${path} → ${res.status}`) + return res.json() as Promise + } + + return { + get: (path: string) => request('GET', path), + post: (path: string, body: unknown) => request('POST', path, body), + patch: (path: string, body: unknown) => request('PATCH', path, body), + delete: (path: string) => request('DELETE', path), + } +} diff --git a/packages/api-client/src/endpoints/auth.ts b/packages/api-client/src/endpoints/auth.ts new file mode 100644 index 000000000..0ac3fd0e6 --- /dev/null +++ b/packages/api-client/src/endpoints/auth.ts @@ -0,0 +1,8 @@ +import { createClient } from '../client' + +export const authEndpoints = (client: ReturnType) => ({ + register: (body: { email: string; password: string; full_name: string }) => + client.post<{ access_token: string; token_type: string }>('/api/auth/register', body), + login: (body: { email: string; password: string }) => + client.post<{ access_token: string; token_type: string }>('/api/auth/login', body), +}) diff --git a/packages/api-client/src/endpoints/bookings.ts b/packages/api-client/src/endpoints/bookings.ts new file mode 100644 index 000000000..292c64cab --- /dev/null +++ b/packages/api-client/src/endpoints/bookings.ts @@ -0,0 +1,6 @@ +import { createClient } from '../client' + +export const bookingEndpoints = (client: ReturnType) => ({ + getBooking: (id: string) => client.get(`/api/bookings/${id}`), + createBooking: (body: unknown) => client.post('/api/bookings/', body), +}) diff --git a/packages/api-client/src/endpoints/payments.ts b/packages/api-client/src/endpoints/payments.ts new file mode 100644 index 000000000..76156c0c6 --- /dev/null +++ b/packages/api-client/src/endpoints/payments.ts @@ -0,0 +1,5 @@ +import { createClient } from '../client' + +export const paymentEndpoints = (client: ReturnType) => ({ + createPayment: (body: unknown) => client.post('/api/payments/', body), +}) diff --git a/packages/api-client/src/endpoints/venues.ts b/packages/api-client/src/endpoints/venues.ts new file mode 100644 index 000000000..16189cf88 --- /dev/null +++ b/packages/api-client/src/endpoints/venues.ts @@ -0,0 +1,10 @@ +import { createClient } from '../client' + +export const venueEndpoints = (client: ReturnType) => ({ + getVenue: (id: string) => client.get(`/api/venues/${id}`), + createVenue: (body: unknown) => client.post('/api/venues/', body), + search: (params: Record) => { + const qs = new URLSearchParams(params as Record).toString() + return client.get(`/api/search/?${qs}`) + }, +}) diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts new file mode 100644 index 000000000..38cc55711 --- /dev/null +++ b/packages/api-client/src/index.ts @@ -0,0 +1,6 @@ +export { createClient } from './client' +export * from './types' +export * from './endpoints/auth' +export * from './endpoints/venues' +export * from './endpoints/bookings' +export * from './endpoints/payments' diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts new file mode 100644 index 000000000..1f302b5e2 --- /dev/null +++ b/packages/api-client/src/types.ts @@ -0,0 +1 @@ +// AUTO-GENERATED — do not edit. Run `pnpm generate` in packages/api-client to refresh. diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json new file mode 100644 index 000000000..06bb44d78 --- /dev/null +++ b/packages/api-client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "dist" + }, + "include": ["src", "scripts"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 000000000..b19fc7830 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,16 @@ +{ + "name": "@venue404/ui", + "version": "0.0.0", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "lint": "eslint src" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "typescript": "^5.4.0" + }, + "peerDependencies": { + "react": "^18.3.0" + } +} \ No newline at end of file diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx new file mode 100644 index 000000000..a185f2412 --- /dev/null +++ b/packages/ui/src/Button.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' +} + +export default function Button({ variant = 'primary', children, ...props }: ButtonProps) { + return ( + + ) +} diff --git a/packages/ui/src/Card.tsx b/packages/ui/src/Card.tsx new file mode 100644 index 000000000..fa9dc974d --- /dev/null +++ b/packages/ui/src/Card.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export default function Card({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/packages/ui/src/DatePicker.tsx b/packages/ui/src/DatePicker.tsx new file mode 100644 index 000000000..225ee2f23 --- /dev/null +++ b/packages/ui/src/DatePicker.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +interface DatePickerProps { + value?: string + onChange?: (value: string) => void + label?: string +} + +export default function DatePicker({ value, onChange, label }: DatePickerProps) { + return ( +
+ {label && } + onChange?.(e.target.value)} /> +
+ ) +} diff --git a/packages/ui/src/Input.tsx b/packages/ui/src/Input.tsx new file mode 100644 index 000000000..3d3a9fc2c --- /dev/null +++ b/packages/ui/src/Input.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +interface InputProps extends React.InputHTMLAttributes { + label?: string +} + +export default function Input({ label, ...props }: InputProps) { + return ( +
+ {label && } + +
+ ) +} diff --git a/packages/ui/src/Modal.tsx b/packages/ui/src/Modal.tsx new file mode 100644 index 000000000..abed4059e --- /dev/null +++ b/packages/ui/src/Modal.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +interface ModalProps { + open: boolean + onClose: () => void + children: React.ReactNode +} + +export default function Modal({ open, onClose, children }: ModalProps) { + if (!open) return null + return ( +
+
e.stopPropagation()}>{children}
+
+ ) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 000000000..7e0785248 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,5 @@ +export { default as Button } from './Button' +export { default as Input } from './Input' +export { default as Card } from './Card' +export { default as Modal } from './Modal' +export { default as DatePicker } from './DatePicker' diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 000000000..92e3512a0 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "declaration": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..3ff5faaaf --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/prd.txt b/prd.txt new file mode 100644 index 000000000..738db1533 --- /dev/null +++ b/prd.txt @@ -0,0 +1,161 @@ +bookmyvenue/ +│ +├── package.json # Workspace config + shared dev deps +├── pnpm-workspace.yaml # Declares apps/* and packages/* as workspaces +├── pnpm-lock.yaml # ONE lockfile for the whole monorepo +├── tsconfig.base.json # Shared TS config (apps extend this) +├── .prettierrc # Shared code formatting +├── eslint.config.js # Shared linting rules +├── .editorconfig # Cross-editor indentation/whitespace +├── .gitignore +├── .dockerignore +├── .env.example # Template for env vars (committed) +├── docker-compose.yml # Postgres + FastAPI for local dev +├── README.md # Setup + run instructions +│ +├── apps/ +│ │ +│ ├── user-web/ # React SPA — end users browse + book +│ │ ├── package.json +│ │ ├── vite.config.ts +│ │ ├── tsconfig.json +│ │ ├── index.html +│ │ ├── .env.example +│ │ ├── public/ +│ │ └── src/ +│ │ ├── main.tsx +│ │ ├── App.tsx +│ │ ├── routes.tsx +│ │ ├── pages/ # Home, VenueDetails, MyBookings, etc. +│ │ ├── components/ # App-specific (not shared) +│ │ ├── hooks/ +│ │ ├── lib/ +│ │ └── styles/ +│ │ +│ ├── owner-portal/ # Same shape as user-web +│ │ └── ... +│ │ +│ ├── admin-panel/ # Same shape as user-web +│ │ └── ... +│ │ +│ └── api/ # FastAPI modular monolith +│ ├── Dockerfile +│ ├── pyproject.toml # Python deps + project config +│ ├── .python-version # Locks Python version (like .nvmrc) +│ ├── .env.example +│ ├── alembic.ini # DB migrations config +│ ├── alembic/ +│ │ └── versions/ # Migration files +│ ├── tests/ +│ │ ├── conftest.py +│ │ ├── test_booking.py +│ │ ├── test_venue.py +│ │ └── ... +│ └── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI entry — wires routers, middleware +│ │ +│ ├── core/ # Cross-cutting infrastructure +│ │ ├── config.py # Settings loaded from env vars +│ │ ├── database.py # SQLAlchemy engine + session +│ │ ├── security.py # JWT encode/decode, password hashing +│ │ ├── exceptions.py # Custom exception classes +│ │ └── middleware.py # CORS, request ID, error handlers +│ │ +│ ├── shared/ # Reusable across modules (NOT business logic) +│ │ ├── models.py # Base SQLAlchemy model, timestamp mixin +│ │ ├── pagination.py +│ │ └── utils.py +│ │ +│ ├── modules/ # ← THE MODULAR MONOLITH LIVES HERE +│ │ ├── auth/ +│ │ │ ├── __init__.py +│ │ │ ├── routes.py # FastAPI router (endpoints) +│ │ │ ├── schemas.py # Pydantic request/response models +│ │ │ ├── service.py # Business logic +│ │ │ └── dependencies.py # get_current_user, require_role, etc. +│ │ │ +│ │ ├── profile/ # users, owners, admins +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ ├── models.py # SQLAlchemy: User, Profile +│ │ │ └── service.py +│ │ │ +│ │ ├── venue/ # venue CRUD, photos, approval state +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ ├── models.py # Venue, VenuePhoto +│ │ │ └── service.py +│ │ │ +│ │ ├── search/ # filters, discovery +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ └── service.py # No own models — reads from venue/ +│ │ │ +│ │ ├── booking/ # request, accept, confirm, cancel +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ ├── models.py # Booking, StatusHistory +│ │ │ ├── service.py # The big state machine we designed +│ │ │ └── state_machine.py # Status transitions +│ │ │ +│ │ ├── availability/ # slots, blocked dates, overlap checks +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ ├── models.py # Slot, BlockedDate +│ │ │ └── service.py +│ │ │ +│ │ ├── notification/ # email + in-app +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ ├── models.py # InAppNotification +│ │ │ ├── service.py +│ │ │ └── templates/ # Email templates +│ │ │ +│ │ ├── admin/ # venue approval, suspend, audit +│ │ │ ├── routes.py +│ │ │ ├── schemas.py +│ │ │ └── service.py +│ │ │ +│ │ └── payment/ # Stripe integration +│ │ ├── routes.py +│ │ ├── schemas.py +│ │ ├── models.py # Payment, Refund +│ │ ├── service.py +│ │ └── webhooks.py # Stripe webhook handlers +│ │ +│ └── jobs/ # APScheduler background jobs +│ ├── scheduler.py # Sets up APScheduler +│ ├── hold_expiry.py # 24-hr advance payment expiry +│ ├── stale_requests.py # 7-day request auto-expiry +│ ├── payment_reminders.py # T-7, T-3, T-1 reminders +│ └── booking_completion.py# Mark as completed after event date +│ +└── packages/ + │ + ├── ui/ # Shared React components + │ ├── package.json # name: "@bookmyvenue/ui" + │ ├── tsconfig.json + │ └── src/ + │ ├── index.ts # Re-exports everything + │ ├── Button.tsx + │ ├── Input.tsx + │ ├── Card.tsx + │ ├── Modal.tsx + │ ├── DatePicker.tsx + │ └── ... + │ + └── api-client/ # Typed FastAPI client + ├── package.json # name: "@bookmyvenue/api-client" + ├── tsconfig.json + ├── scripts/ + │ └── generate.ts # Regenerates types from /openapi.json + └── src/ + ├── index.ts + ├── client.ts # fetch wrapper with auth, base URL + ├── types.ts # AUTO-GENERATED — do not edit + └── endpoints/ + ├── auth.ts + ├── venues.ts + ├── bookings.ts + └── payments.ts \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..8eecaed09 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true + } +} From ac55db21b68fdf5c6f3d44f5bc94b0ff06e9ca33 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 21:46:54 +0100 Subject: [PATCH 002/243] fix: Root name change in package files --- .dockerignore | 2 +- apps/user-web/package.json | 8 ++++---- packages/api-client/package.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.dockerignore b/.dockerignore index 62e979706..52240b1e8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -node_modules/ +node_modules dist/ .env __pycache__/ diff --git a/apps/user-web/package.json b/apps/user-web/package.json index b31a9a183..26dcc97c8 100644 --- a/apps/user-web/package.json +++ b/apps/user-web/package.json @@ -1,5 +1,5 @@ { - "name": "@bookmyvenue/user-web", + "name": "@venue404/user-web", "private": true, "version": "0.0.0", "type": "module", @@ -10,8 +10,8 @@ "preview": "vite preview" }, "dependencies": { - "@bookmyvenue/api-client": "workspace:*", - "@bookmyvenue/ui": "workspace:*", + "@venue404/api-client": "workspace:*", + "@venue404/ui": "workspace:*", "react": "^18.3.0", "react-dom": "^18.3.0", "react-router-dom": "^6.23.0" @@ -22,4 +22,4 @@ "@vitejs/plugin-react": "^4.3.0", "vite": "^5.3.0" } -} +} \ No newline at end of file diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b7400d391..d3519b9cc 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,5 +1,5 @@ { - "name": "@bookmyvenue/api-client", + "name": "@venue404/api-client", "version": "0.0.0", "type": "module", "main": "./src/index.ts", @@ -11,4 +11,4 @@ "tsx": "^4.0.0", "typescript": "^5.4.0" } -} +} \ No newline at end of file From dbf5363621d6ac5a30ef5789e8a3090f75307923 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 21:57:22 +0100 Subject: [PATCH 003/243] fix: basic typo fixes in API structure, frontend and configure project gitignore --- .env.example | 2 +- .gitignore | 33 ++++++- apps/api/alembic.ini | 2 +- apps/api/app/main.py | 2 +- apps/owner-portal/index.html | 23 ++--- apps/user-web/index.html | 23 ++--- docker-compose.yml | 2 +- prd.txt | 161 ----------------------------------- 8 files changed, 60 insertions(+), 188 deletions(-) delete mode 100644 prd.txt diff --git a/.env.example b/.env.example index c475e8ec4..8c1733748 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Database -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bookmyvenue +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/venue404 # Auth JWT_SECRET=your-secret-here diff --git a/.gitignore b/.gitignore index f684534cd..01c2eb0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,37 @@ +# ---- Dependencies ---- node_modules/ +.venv/ +*.egg-info/ + +# ---- Build output ---- dist/ +build/ +*.tsbuildinfo + +# ---- Environment secrets ---- .env .env.local +.env.*.local + +# ---- Python runtime ---- __pycache__/ -*.pyc -.venv/ +*.py[cod] +*.pyo + +# ---- Test & coverage ---- .pytest_cache/ .mypy_cache/ -*.egg-info/ +.ruff_cache/ +htmlcov/ +.coverage +coverage.xml + +# ---- Editor & OS ---- +.vscode/ +.idea/ +*.swp +.DS_Store +Thumbs.db + +# ---- Misc ---- +prd.txt diff --git a/apps/api/alembic.ini b/apps/api/alembic.ini index 1d20bcd15..9ab3e9d0d 100644 --- a/apps/api/alembic.ini +++ b/apps/api/alembic.ini @@ -1,7 +1,7 @@ [alembic] script_location = alembic prepend_sys_path = . -sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/bookmyvenue +sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/venue404 [loggers] keys = root,sqlalchemy,alembic diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 727fedccb..b1d2b746b 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -10,7 +10,7 @@ from app.modules.admin.routes import router as admin_router from app.modules.payment.routes import router as payment_router -app = FastAPI(title="BookMyVenue API") +app = FastAPI(title="Venue404 API") register_middleware(app) diff --git a/apps/owner-portal/index.html b/apps/owner-portal/index.html index b084ac4bb..bca2be6ac 100644 --- a/apps/owner-portal/index.html +++ b/apps/owner-portal/index.html @@ -1,12 +1,15 @@ - - - - BookMyVenue — Owner Portal - - -
- - - + + + + + Venue404 — Owner Portal + + + +
+ + + + \ No newline at end of file diff --git a/apps/user-web/index.html b/apps/user-web/index.html index 23f20a99a..c697eea4d 100644 --- a/apps/user-web/index.html +++ b/apps/user-web/index.html @@ -1,12 +1,15 @@ - - - - BookMyVenue - - -
- - - + + + + + Venue404 + + + +
+ + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bc38d99e8..c38cff5c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: bookmyvenue + POSTGRES_DB: venue404 ports: - "5432:5432" volumes: diff --git a/prd.txt b/prd.txt deleted file mode 100644 index 738db1533..000000000 --- a/prd.txt +++ /dev/null @@ -1,161 +0,0 @@ -bookmyvenue/ -│ -├── package.json # Workspace config + shared dev deps -├── pnpm-workspace.yaml # Declares apps/* and packages/* as workspaces -├── pnpm-lock.yaml # ONE lockfile for the whole monorepo -├── tsconfig.base.json # Shared TS config (apps extend this) -├── .prettierrc # Shared code formatting -├── eslint.config.js # Shared linting rules -├── .editorconfig # Cross-editor indentation/whitespace -├── .gitignore -├── .dockerignore -├── .env.example # Template for env vars (committed) -├── docker-compose.yml # Postgres + FastAPI for local dev -├── README.md # Setup + run instructions -│ -├── apps/ -│ │ -│ ├── user-web/ # React SPA — end users browse + book -│ │ ├── package.json -│ │ ├── vite.config.ts -│ │ ├── tsconfig.json -│ │ ├── index.html -│ │ ├── .env.example -│ │ ├── public/ -│ │ └── src/ -│ │ ├── main.tsx -│ │ ├── App.tsx -│ │ ├── routes.tsx -│ │ ├── pages/ # Home, VenueDetails, MyBookings, etc. -│ │ ├── components/ # App-specific (not shared) -│ │ ├── hooks/ -│ │ ├── lib/ -│ │ └── styles/ -│ │ -│ ├── owner-portal/ # Same shape as user-web -│ │ └── ... -│ │ -│ ├── admin-panel/ # Same shape as user-web -│ │ └── ... -│ │ -│ └── api/ # FastAPI modular monolith -│ ├── Dockerfile -│ ├── pyproject.toml # Python deps + project config -│ ├── .python-version # Locks Python version (like .nvmrc) -│ ├── .env.example -│ ├── alembic.ini # DB migrations config -│ ├── alembic/ -│ │ └── versions/ # Migration files -│ ├── tests/ -│ │ ├── conftest.py -│ │ ├── test_booking.py -│ │ ├── test_venue.py -│ │ └── ... -│ └── app/ -│ ├── __init__.py -│ ├── main.py # FastAPI entry — wires routers, middleware -│ │ -│ ├── core/ # Cross-cutting infrastructure -│ │ ├── config.py # Settings loaded from env vars -│ │ ├── database.py # SQLAlchemy engine + session -│ │ ├── security.py # JWT encode/decode, password hashing -│ │ ├── exceptions.py # Custom exception classes -│ │ └── middleware.py # CORS, request ID, error handlers -│ │ -│ ├── shared/ # Reusable across modules (NOT business logic) -│ │ ├── models.py # Base SQLAlchemy model, timestamp mixin -│ │ ├── pagination.py -│ │ └── utils.py -│ │ -│ ├── modules/ # ← THE MODULAR MONOLITH LIVES HERE -│ │ ├── auth/ -│ │ │ ├── __init__.py -│ │ │ ├── routes.py # FastAPI router (endpoints) -│ │ │ ├── schemas.py # Pydantic request/response models -│ │ │ ├── service.py # Business logic -│ │ │ └── dependencies.py # get_current_user, require_role, etc. -│ │ │ -│ │ ├── profile/ # users, owners, admins -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ ├── models.py # SQLAlchemy: User, Profile -│ │ │ └── service.py -│ │ │ -│ │ ├── venue/ # venue CRUD, photos, approval state -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ ├── models.py # Venue, VenuePhoto -│ │ │ └── service.py -│ │ │ -│ │ ├── search/ # filters, discovery -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ └── service.py # No own models — reads from venue/ -│ │ │ -│ │ ├── booking/ # request, accept, confirm, cancel -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ ├── models.py # Booking, StatusHistory -│ │ │ ├── service.py # The big state machine we designed -│ │ │ └── state_machine.py # Status transitions -│ │ │ -│ │ ├── availability/ # slots, blocked dates, overlap checks -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ ├── models.py # Slot, BlockedDate -│ │ │ └── service.py -│ │ │ -│ │ ├── notification/ # email + in-app -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ ├── models.py # InAppNotification -│ │ │ ├── service.py -│ │ │ └── templates/ # Email templates -│ │ │ -│ │ ├── admin/ # venue approval, suspend, audit -│ │ │ ├── routes.py -│ │ │ ├── schemas.py -│ │ │ └── service.py -│ │ │ -│ │ └── payment/ # Stripe integration -│ │ ├── routes.py -│ │ ├── schemas.py -│ │ ├── models.py # Payment, Refund -│ │ ├── service.py -│ │ └── webhooks.py # Stripe webhook handlers -│ │ -│ └── jobs/ # APScheduler background jobs -│ ├── scheduler.py # Sets up APScheduler -│ ├── hold_expiry.py # 24-hr advance payment expiry -│ ├── stale_requests.py # 7-day request auto-expiry -│ ├── payment_reminders.py # T-7, T-3, T-1 reminders -│ └── booking_completion.py# Mark as completed after event date -│ -└── packages/ - │ - ├── ui/ # Shared React components - │ ├── package.json # name: "@bookmyvenue/ui" - │ ├── tsconfig.json - │ └── src/ - │ ├── index.ts # Re-exports everything - │ ├── Button.tsx - │ ├── Input.tsx - │ ├── Card.tsx - │ ├── Modal.tsx - │ ├── DatePicker.tsx - │ └── ... - │ - └── api-client/ # Typed FastAPI client - ├── package.json # name: "@bookmyvenue/api-client" - ├── tsconfig.json - ├── scripts/ - │ └── generate.ts # Regenerates types from /openapi.json - └── src/ - ├── index.ts - ├── client.ts # fetch wrapper with auth, base URL - ├── types.ts # AUTO-GENERATED — do not edit - └── endpoints/ - ├── auth.ts - ├── venues.ts - ├── bookings.ts - └── payments.ts \ No newline at end of file From 36fdd5562ef8822b8e03cc7b72ce125f3f1e67e0 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 22:01:23 +0100 Subject: [PATCH 004/243] docs: add database and migration workflow documentation for Alembic --- apps/admin-panel/index.html | 23 ++-- docs/commands/commands.db.txt | 211 ++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 docs/commands/commands.db.txt diff --git a/apps/admin-panel/index.html b/apps/admin-panel/index.html index e176e6af2..0d0dd2e03 100644 --- a/apps/admin-panel/index.html +++ b/apps/admin-panel/index.html @@ -1,12 +1,15 @@ - - - - BookMyVenue — Admin - - -
- - - + + + + + Venue404 — Admin + + + +
+ + + + \ No newline at end of file diff --git a/docs/commands/commands.db.txt b/docs/commands/commands.db.txt new file mode 100644 index 000000000..592263b38 --- /dev/null +++ b/docs/commands/commands.db.txt @@ -0,0 +1,211 @@ +============================================================ + VENUE404 — DATABASE & MIGRATION COMMANDS + Location: docs/commands.db.txt + Tool: Alembic (schema versioning) + SQLAlchemy (ORM) +============================================================ + +MENTAL MODEL +------------ +SQLAlchemy models = your "desired" schema (in Python code) +Alembic = git for the DB (tracks/applies/rolls back changes) +Migration files = versioned commits in alembic/versions/ +Database = the deployed state (local Postgres OR Supabase) + +Workflow in one line: + Edit model -> generate migration -> review -> apply -> commit + + +============================================================ + ONE-TIME SETUP (do this once per developer machine) +============================================================ + +# 1. Install dependencies (already in pyproject.toml) +cd apps/api +pip install -e . + +# 2. Confirm DATABASE_URL is set +cat .env | grep DATABASE_URL +# For local dev, it should point to docker-compose Postgres: +# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/venue404 + +# 3. Start local Postgres +cd ../.. # back to repo root +docker compose up -d postgres + +# 4. Apply all existing migrations to your fresh local DB +cd apps/api +alembic upgrade head + + +============================================================ + DAILY WORKFLOW (when you change a SQLAlchemy model) +============================================================ + +# 1. Edit the model in app/modules//models.py +# e.g. add a new column, new table, change a type + +# 2. Generate a migration file from the diff +alembic revision --autogenerate -m "short description of the change" +# Output: alembic/versions/abc123_short_description_of_the_change.py + +# 3. *** ALWAYS REVIEW THE GENERATED FILE *** +# Autogenerate is not perfect. Check for: +# - Dropped columns you didn't mean to drop +# - Renamed columns shown as drop+add (loses data!) +# - Missing custom constraints (exclusion, check, partial indexes) +code alembic/versions/abc123_*.py + +# 4. Apply it to your local DB +alembic upgrade head + +# 5. Test your code against the new schema + +# 6. Commit BOTH the model change AND the migration file together +git add app/modules//models.py alembic/versions/abc123_*.py +git commit -m "feat(): short description" + + +============================================================ + ROLLING BACK +============================================================ + +# Roll back the most recent migration +alembic downgrade -1 + +# Roll back to a specific revision +alembic downgrade abc123 + +# Roll back EVERYTHING (nuke schema — local only, never prod) +alembic downgrade base + + +============================================================ + INSPECTION COMMANDS +============================================================ + +# Show the current applied migration +alembic current + +# Show full history +alembic history --verbose + +# Show a specific migration's SQL without applying +alembic show abc123 + +# Show pending migrations (not yet applied) +alembic history | head -20 # then compare with `alembic current` + + +============================================================ + APPLYING TO SUPABASE (staging / production) +============================================================ + +# *** NEVER autogenerate against Supabase. Generate locally first. *** + +# 1. Make sure migrations work locally first +alembic upgrade head # against local DB +# ... test thoroughly ... + +# 2. Switch DATABASE_URL to point at Supabase (direct connection, port 5432) +# export DATABASE_URL=postgresql://postgres.xxx:pass@aws-0-region.pooler.supabase.com:5432/postgres + +# 3. Apply +alembic upgrade head + +# 4. Switch DATABASE_URL back to local for further dev +# (or use separate .env.local and .env.supabase files) + + +============================================================ + CREATING A MIGRATION MANUALLY (no autogenerate) +============================================================ + +# Useful for: +# - Data migrations (e.g. backfill values) +# - Custom Postgres features Alembic can't detect: +# * EXCLUDE constraints (overlap prevention) +# * Partial indexes, GIN/GiST indexes +# * Triggers, functions + +alembic revision -m "add exclusion constraint to bookings" +# Then edit the generated file and write op.execute("...") with raw SQL + + +============================================================ + COMMON GOTCHAS +============================================================ + +[1] RENAMING A COLUMN + Autogenerate sees: drop old + add new -> LOSES DATA + Fix: hand-edit migration to use op.alter_column(..., new_column_name=...) + +[2] CHANGING A COLUMN TYPE + May fail on existing data. Use USING clause: + op.execute("ALTER TABLE x ALTER COLUMN y TYPE int USING y::int") + +[3] DROPPING A COLUMN WITH DATA + Confirm no code still reads it. Once dropped, data is gone. + +[4] EXCLUSION CONSTRAINTS (slot overlap prevention) + SQLAlchemy doesn't model these. Add manually: + op.execute(""" + ALTER TABLE bookings + ADD CONSTRAINT no_overlap + EXCLUDE USING gist ( + venue_id WITH =, + tstzrange(start_time, end_time) WITH && + ) WHERE (status IN ('confirmed', 'accepted')) + """) + +[5] FORGETTING TO COMMIT THE MIGRATION FILE + Models updated but migration not pushed to git + = teammate's local DB doesn't match yours + = bugs that "only happen on my machine" + ALWAYS commit model change + migration file together. + +[6] RUNNING AUTOGENERATE AGAINST AN OUT-OF-DATE DB + First run `alembic upgrade head` to catch up, + THEN autogenerate your new change. + Otherwise you'll regenerate already-applied changes. + + +============================================================ + RAW DB ACCESS (when you need to poke around) +============================================================ + +# Connect to local Postgres via psql +docker compose exec postgres psql -U postgres -d bookmyvenue + +# Useful psql commands once connected: +\dt # list tables +\d bookings # describe a table +\q # quit + +# For Supabase: use the SQL Editor in the Supabase dashboard +# OR connect via psql with the connection string: +psql "$DATABASE_URL" + + +============================================================ + RESET LOCAL DB (when things get weird in dev) +============================================================ + +# Nuclear option — drops everything and re-applies all migrations +docker compose down -v # removes the postgres volume +docker compose up -d postgres # fresh DB +alembic upgrade head # re-apply all migrations + + +============================================================ + QUICK REFERENCE +============================================================ + +alembic upgrade head # apply all pending migrations +alembic downgrade -1 # roll back one +alembic revision --autogenerate -m "msg" # generate from model diff +alembic revision -m "msg" # empty migration (manual SQL) +alembic current # what's applied now +alembic history # full timeline +alembic show # inspect a specific migration + +============================================================ From d8bdb708a1837b2ed5a9fa3a5ffbb273962a9e25 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 22:47:50 +0100 Subject: [PATCH 005/243] feat: initialize vite port config in admin, owner, and user-web applications with concurrent dev scripts --- apps/admin-panel/package.json | 2 +- apps/admin-panel/vite.config.ts | 2 +- apps/owner-portal/package.json | 2 +- apps/owner-portal/vite.config.ts | 2 +- apps/user-web/package.json | 2 +- apps/user-web/vite.config.ts | 2 +- docs/commands/commands.db.txt | 211 --- package.json | 4 +- pnpm-lock.yaml | 2633 ++++++++++++++++++++++++++++++ 9 files changed, 2642 insertions(+), 218 deletions(-) delete mode 100644 docs/commands/commands.db.txt create mode 100644 pnpm-lock.yaml diff --git a/apps/admin-panel/package.json b/apps/admin-panel/package.json index 77efbcdd9..c3183e646 100644 --- a/apps/admin-panel/package.json +++ b/apps/admin-panel/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --port 5175", "build": "tsc && vite build", "lint": "eslint src", "preview": "vite preview" diff --git a/apps/admin-panel/vite.config.ts b/apps/admin-panel/vite.config.ts index aad496d54..d635e361d 100644 --- a/apps/admin-panel/vite.config.ts +++ b/apps/admin-panel/vite.config.ts @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { - port: 3002, + port: 5175, proxy: { '/api': 'http://localhost:8000', }, diff --git a/apps/owner-portal/package.json b/apps/owner-portal/package.json index 4077cf1b7..6555a0f54 100644 --- a/apps/owner-portal/package.json +++ b/apps/owner-portal/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --port 5174", "build": "tsc && vite build", "lint": "eslint src", "preview": "vite preview" diff --git a/apps/owner-portal/vite.config.ts b/apps/owner-portal/vite.config.ts index 25455554a..4b5cb12f6 100644 --- a/apps/owner-portal/vite.config.ts +++ b/apps/owner-portal/vite.config.ts @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { - port: 3001, + port: 5174, proxy: { '/api': 'http://localhost:8000', }, diff --git a/apps/user-web/package.json b/apps/user-web/package.json index 26dcc97c8..36a47fc0f 100644 --- a/apps/user-web/package.json +++ b/apps/user-web/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --port 5173", "build": "tsc && vite build", "lint": "eslint src", "preview": "vite preview" diff --git a/apps/user-web/vite.config.ts b/apps/user-web/vite.config.ts index bb6580811..52ad95ac5 100644 --- a/apps/user-web/vite.config.ts +++ b/apps/user-web/vite.config.ts @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { - port: 3000, + port: 5173, proxy: { '/api': 'http://localhost:8000', }, diff --git a/docs/commands/commands.db.txt b/docs/commands/commands.db.txt deleted file mode 100644 index 592263b38..000000000 --- a/docs/commands/commands.db.txt +++ /dev/null @@ -1,211 +0,0 @@ -============================================================ - VENUE404 — DATABASE & MIGRATION COMMANDS - Location: docs/commands.db.txt - Tool: Alembic (schema versioning) + SQLAlchemy (ORM) -============================================================ - -MENTAL MODEL ------------- -SQLAlchemy models = your "desired" schema (in Python code) -Alembic = git for the DB (tracks/applies/rolls back changes) -Migration files = versioned commits in alembic/versions/ -Database = the deployed state (local Postgres OR Supabase) - -Workflow in one line: - Edit model -> generate migration -> review -> apply -> commit - - -============================================================ - ONE-TIME SETUP (do this once per developer machine) -============================================================ - -# 1. Install dependencies (already in pyproject.toml) -cd apps/api -pip install -e . - -# 2. Confirm DATABASE_URL is set -cat .env | grep DATABASE_URL -# For local dev, it should point to docker-compose Postgres: -# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/venue404 - -# 3. Start local Postgres -cd ../.. # back to repo root -docker compose up -d postgres - -# 4. Apply all existing migrations to your fresh local DB -cd apps/api -alembic upgrade head - - -============================================================ - DAILY WORKFLOW (when you change a SQLAlchemy model) -============================================================ - -# 1. Edit the model in app/modules//models.py -# e.g. add a new column, new table, change a type - -# 2. Generate a migration file from the diff -alembic revision --autogenerate -m "short description of the change" -# Output: alembic/versions/abc123_short_description_of_the_change.py - -# 3. *** ALWAYS REVIEW THE GENERATED FILE *** -# Autogenerate is not perfect. Check for: -# - Dropped columns you didn't mean to drop -# - Renamed columns shown as drop+add (loses data!) -# - Missing custom constraints (exclusion, check, partial indexes) -code alembic/versions/abc123_*.py - -# 4. Apply it to your local DB -alembic upgrade head - -# 5. Test your code against the new schema - -# 6. Commit BOTH the model change AND the migration file together -git add app/modules//models.py alembic/versions/abc123_*.py -git commit -m "feat(): short description" - - -============================================================ - ROLLING BACK -============================================================ - -# Roll back the most recent migration -alembic downgrade -1 - -# Roll back to a specific revision -alembic downgrade abc123 - -# Roll back EVERYTHING (nuke schema — local only, never prod) -alembic downgrade base - - -============================================================ - INSPECTION COMMANDS -============================================================ - -# Show the current applied migration -alembic current - -# Show full history -alembic history --verbose - -# Show a specific migration's SQL without applying -alembic show abc123 - -# Show pending migrations (not yet applied) -alembic history | head -20 # then compare with `alembic current` - - -============================================================ - APPLYING TO SUPABASE (staging / production) -============================================================ - -# *** NEVER autogenerate against Supabase. Generate locally first. *** - -# 1. Make sure migrations work locally first -alembic upgrade head # against local DB -# ... test thoroughly ... - -# 2. Switch DATABASE_URL to point at Supabase (direct connection, port 5432) -# export DATABASE_URL=postgresql://postgres.xxx:pass@aws-0-region.pooler.supabase.com:5432/postgres - -# 3. Apply -alembic upgrade head - -# 4. Switch DATABASE_URL back to local for further dev -# (or use separate .env.local and .env.supabase files) - - -============================================================ - CREATING A MIGRATION MANUALLY (no autogenerate) -============================================================ - -# Useful for: -# - Data migrations (e.g. backfill values) -# - Custom Postgres features Alembic can't detect: -# * EXCLUDE constraints (overlap prevention) -# * Partial indexes, GIN/GiST indexes -# * Triggers, functions - -alembic revision -m "add exclusion constraint to bookings" -# Then edit the generated file and write op.execute("...") with raw SQL - - -============================================================ - COMMON GOTCHAS -============================================================ - -[1] RENAMING A COLUMN - Autogenerate sees: drop old + add new -> LOSES DATA - Fix: hand-edit migration to use op.alter_column(..., new_column_name=...) - -[2] CHANGING A COLUMN TYPE - May fail on existing data. Use USING clause: - op.execute("ALTER TABLE x ALTER COLUMN y TYPE int USING y::int") - -[3] DROPPING A COLUMN WITH DATA - Confirm no code still reads it. Once dropped, data is gone. - -[4] EXCLUSION CONSTRAINTS (slot overlap prevention) - SQLAlchemy doesn't model these. Add manually: - op.execute(""" - ALTER TABLE bookings - ADD CONSTRAINT no_overlap - EXCLUDE USING gist ( - venue_id WITH =, - tstzrange(start_time, end_time) WITH && - ) WHERE (status IN ('confirmed', 'accepted')) - """) - -[5] FORGETTING TO COMMIT THE MIGRATION FILE - Models updated but migration not pushed to git - = teammate's local DB doesn't match yours - = bugs that "only happen on my machine" - ALWAYS commit model change + migration file together. - -[6] RUNNING AUTOGENERATE AGAINST AN OUT-OF-DATE DB - First run `alembic upgrade head` to catch up, - THEN autogenerate your new change. - Otherwise you'll regenerate already-applied changes. - - -============================================================ - RAW DB ACCESS (when you need to poke around) -============================================================ - -# Connect to local Postgres via psql -docker compose exec postgres psql -U postgres -d bookmyvenue - -# Useful psql commands once connected: -\dt # list tables -\d bookings # describe a table -\q # quit - -# For Supabase: use the SQL Editor in the Supabase dashboard -# OR connect via psql with the connection string: -psql "$DATABASE_URL" - - -============================================================ - RESET LOCAL DB (when things get weird in dev) -============================================================ - -# Nuclear option — drops everything and re-applies all migrations -docker compose down -v # removes the postgres volume -docker compose up -d postgres # fresh DB -alembic upgrade head # re-apply all migrations - - -============================================================ - QUICK REFERENCE -============================================================ - -alembic upgrade head # apply all pending migrations -alembic downgrade -1 # roll back one -alembic revision --autogenerate -m "msg" # generate from model diff -alembic revision -m "msg" # empty migration (manual SQL) -alembic current # what's applied now -alembic history # full timeline -alembic show # inspect a specific migration - -============================================================ diff --git a/package.json b/package.json index 43ef5ffe0..c0153b9ce 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "private": true, "version": "0.0.0", "scripts": { - "dev": "pnpm --parallel -r dev", + "dev": "concurrently -n user,owner,admin -c blue,green,magenta \"pnpm --filter user-web dev\" \"pnpm --filter owner-portal dev\" \"pnpm --filter admin-panel dev\"", + "dev:all": "concurrently -n docker,user,owner,admin -c yellow,blue,green,magenta \"docker compose up\" \"pnpm --filter user-web dev\" \"pnpm --filter owner-portal dev\" \"pnpm --filter admin-panel dev\"", "build": "pnpm -r build", "lint": "pnpm -r lint", "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"" @@ -11,6 +12,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", + "concurrently": "^10.0.0", "eslint": "^9.0.0", "prettier": "^3.0.0", "typescript": "^5.4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..bccc29827 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2633 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^7.0.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^7.0.0 + version: 7.18.0(eslint@9.39.4)(typescript@5.9.3) + concurrently: + specifier: ^10.0.0 + version: 10.0.0 + eslint: + specifier: ^9.0.0 + version: 9.39.4 + prettier: + specifier: ^3.0.0 + version: 3.8.3 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + + apps/admin-panel: + dependencies: + '@venue404/api-client': + specifier: workspace:* + version: link:../../packages/api-client + '@venue404/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.23.0 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21) + vite: + specifier: ^5.3.0 + version: 5.4.21 + + apps/owner-portal: + dependencies: + '@venue404/api-client': + specifier: workspace:* + version: link:../../packages/api-client + '@venue404/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.23.0 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21) + vite: + specifier: ^5.3.0 + version: 5.4.21 + + apps/user-web: + dependencies: + '@venue404/api-client': + specifier: workspace:* + version: link:../../packages/api-client + '@venue404/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.23.0 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21) + vite: + specifier: ^5.3.0 + version: 5.4.21 + + packages/api-client: + devDependencies: + tsx: + specifier: ^4.0.0 + version: 4.22.4 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + + packages/ui: + dependencies: + react: + specifier: ^18.3.0 + version: 18.3.1 + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.29 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@remix-run/router@1.23.3': + resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.29': + resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@10.0.0: + resolution: {integrity: sha512-DRrk10z3sVPpguNe8od2cGNqZGqbT15rwAnxD4dG3b78mdNNb/gJyr8T834Oj518WcBmTktrt4FhdwZn09ZWSg==} + engines: {node: '>=22'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.4: + resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.4: + resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + 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/core@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.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + 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.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@remix-run/router@1.23.3': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.29)': + dependencies: + '@types/react': 18.3.29 + + '@types/react@18.3.29': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 9.39.4 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 9.39.4 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.8.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + eslint: 9.39.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.33: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001793: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + concurrently@10.0.0: + dependencies: + chalk: 5.6.2 + rxjs: 7.8.2 + shell-quote: 1.8.4 + supports-color: 10.2.2 + tree-kill: 1.2.2 + yargs: 18.0.0 + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + electron-to-chromium@1.5.364: {} + + emoji-regex@10.6.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.46: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.4(react@18.3.1) + + react-router@6.30.4(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.8.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.4: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + tsx@4.22.4: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.4 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} From c4c4a08fa04307e3920f8f61bdd9d2a68e673232 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 23:18:20 +0100 Subject: [PATCH 006/243] feat: initialize docker healthchecks, and updated migration scripts --- docker-compose.yml | 17 +++++++++++------ package.json | 8 ++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c38cff5c9..e814024ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: db: image: postgres:16 @@ -12,18 +10,25 @@ services: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 5s + timeout: 5s + retries: 5 api: build: ./apps/api restart: unless-stopped depends_on: - - db + db: + condition: service_healthy env_file: - - .env + - ./apps/api/.env ports: - "8000:8000" volumes: - ./apps/api:/app - + - /app/.venv + volumes: - pgdata: + pgdata: \ No newline at end of file diff --git a/package.json b/package.json index c0153b9ce..43d20f0c8 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,12 @@ "scripts": { "dev": "concurrently -n user,owner,admin -c blue,green,magenta \"pnpm --filter user-web dev\" \"pnpm --filter owner-portal dev\" \"pnpm --filter admin-panel dev\"", "dev:all": "concurrently -n docker,user,owner,admin -c yellow,blue,green,magenta \"docker compose up\" \"pnpm --filter user-web dev\" \"pnpm --filter owner-portal dev\" \"pnpm --filter admin-panel dev\"", - "build": "pnpm -r build", - "lint": "pnpm -r lint", + "api:migrate": "docker compose exec api alembic upgrade head", + "api:migrate:new": "docker compose exec api alembic revision --autogenerate -m", + "api:migrate:down": "docker compose exec api alembic downgrade -1", + "build": "pnpm -r --filter \"./apps/*\" --filter \"!./apps/api\" build", + "lint": "pnpm -r --filter \"./apps/*\" --filter \"!./apps/api\" lint", + "typecheck": "pnpm -r --filter \"./apps/*\" --filter \"!./apps/api\" typecheck", "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"" }, "devDependencies": { From b42e4b5495b50adda018183f9970be4c03ffa7cb Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 23:18:31 +0100 Subject: [PATCH 007/243] feat: configure CORS middleware for local development environments --- apps/api/app/core/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/app/core/middleware.py b/apps/api/app/core/middleware.py index fa5980cdd..b55ce9353 100644 --- a/apps/api/app/core/middleware.py +++ b/apps/api/app/core/middleware.py @@ -5,7 +5,7 @@ def register_middleware(app: FastAPI) -> None: app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000", "http://localhost:3001", "http://localhost:3002"], + allow_origins=["http://localhost:3000", "http://localhost:3001", "http://localhost:3002", "http://localhost:5173", "http://localhost:5174", "http://localhost:5175"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], From b189616691182f41c3211699401b00d99cd52af2 Mon Sep 17 00:00:00 2001 From: sankar Date: Sun, 31 May 2026 23:18:41 +0100 Subject: [PATCH 008/243] feat: initialize api application with pyproject.toml configuration --- apps/api/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index 93f93b044..baa45be5a 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "alembic>=1.13.0", "psycopg2-binary>=2.9.0", "pydantic-settings>=2.3.0", + "email-validator>=2.2.0", "passlib[bcrypt]>=1.7.4", "python-jose[cryptography]>=3.3.0", "stripe>=9.0.0", From 9ad5e83b5d19f3fafa586185ad3fb464a3e8864c Mon Sep 17 00:00:00 2001 From: sankar Date: Mon, 1 Jun 2026 00:11:15 +0100 Subject: [PATCH 009/243] docs: add architecture documentation and project README --- docs/README.md | 152 ++++++++++++++++++++ docs/architecture.md | 326 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 478 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/architecture.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..9847df193 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,152 @@ +# venue404 + +A venue booking platform, Owners list venues, users browse and book, admins approve. + +Built as a modular monolith MVP with a focus on shipping fast on free-tier infrastructure. + +## Stack + +- **Frontend:** React 18 + Vite + TypeScript (3 SPAs: user, owner, admin) +- **Backend:** FastAPI (modular monolith) + SQLAlchemy + Alembic +- **Database:** PostgreSQL 16 (local via Docker; Supabase in production) +- **Images:** Cloudinary (Free Plan) +- **Payments:** Stripe (test mode) +- **Email:** Resend (Free Plan) +- **Maps:** OpenStreetMap + Leaflet.js (Free) +- **Logs:** Better Stack / Logtail (Free Plan) +- **Monorepo:** pnpm workspaces +## Project structure + +``` +venue404/ +├── apps/ +│ ├── user-web/ React SPA — browse & book venues +│ ├── owner-portal/ React SPA — manage venues & bookings +│ ├── admin-panel/ React SPA — approve venues, audit +│ └── api/ FastAPI modular monolith +│ └── app/modules/ → auth, profile, venue, search, booking, +│ availability, notification, admin, payment ... +├── packages/ +│ ├── ui/ Shared React component library +│ └── api-client/ Typed FastAPI client (auto-generated from OpenAPI) +├── docs/ Architecture, commands, decisions +├── docker-compose.yml Local Postgres + API +└── pnpm-workspace.yaml +``` + +Each app/package has its own `package.json`. One `pnpm-lock.yaml` at the root locks versions across the whole repo. + +## Prerequisites + +- **Node.js 20+** (use [nvm](https://github.com/nvm-sh/nvm) to manage versions) +- **Docker Desktop** running +- **pnpm** (enable via `corepack enable` — comes with Node) +No host Python needed — the API runs entirely inside Docker. + +## Quick start + +```bash +# 1. Clone and enter the repo +git clone venue404 +cd venue404 + +# 2. Install all JS/TS dependencies (one command, all workspaces) +pnpm install + +# 3. Set up env files +cp apps/api/.env.example apps/api/.env +# Edit apps/api/.env and fill in any test keys (Stripe, Resend, Cloudinary) + +# 4. Start the full stack +pnpm dev:all +``` + +That's it. After `pnpm dev:all` runs you should have: + +| Service | URL | +|---------------|------------------------------| +| User app | http://localhost:5173 | +| Owner portal | http://localhost:5174 | +| Admin panel | http://localhost:5175 | +| FastAPI | http://localhost:8000 | +| FastAPI docs | http://localhost:8000/docs | +| PostgreSQL | localhost:5432 | + +Apply initial database migrations (first time only): + +```bash +pnpm api:migrate +``` + +## Common commands + +```bash +# Run everything (Postgres + API + 3 React apps) +pnpm dev:all + +# Run only the 3 React apps (backend already running) +pnpm dev + +# Run a single app +pnpm dev:user +pnpm dev:owner +pnpm dev:admin + +# Database migrations +pnpm api:migrate # apply pending migrations +pnpm api:migrate:new "describe change" # create a new migration +pnpm api:migrate:down # roll back the last migration + +# Quality checks +pnpm typecheck # TypeScript across all apps +pnpm lint # ESLint +pnpm format # Prettier +pnpm build # production build (rarely needed in dev) +``` + +For ad-hoc Docker access: + +```bash +docker compose exec api bash # shell into API container +docker compose exec db psql -U postgres -d venue404 # psql into Postgres +docker compose logs -f api # tail API logs +``` + +## Workflow basics + +**After `git pull`:** + +```bash +pnpm api:migrate # if teammate added migrations +pnpm dev:all # start working +``` + +**After changing a SQLAlchemy model:** + +```bash +pnpm api:migrate:new "add payment_method to bookings" +# review the generated file in apps/api/alembic/versions/ +pnpm api:migrate +git add . && git commit +``` + +**Before committing** (ideally automated via pre-commit hooks): + +```bash +pnpm typecheck +pnpm lint +``` + +## Documentation + +- [`docs/architecture.md`](./docs/architecture.md) — system architecture, modular monolith rules + +## Environment + +| Environment | Database | Hosting | +|-------------|---------------------------------------|---------------------------| +| Local | Docker Postgres | Your machine | +| Staging | Supabase (project #1) | Vercel + Fly.io (staging) | +| Production | Supabase (project #2) | Vercel + Fly.io (prod) | + +Same migration files apply across all three, only `DATABASE_URL` changes. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..8e0abc4b6 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,326 @@ +# Architecture + +This document captures the system design and the rationale behind key decisions. Read this after the [README](./README.md) — it's the deeper "why and how" of how venue404 is built. + +For the visual overview, see the [HLD diagram](https://drive.google.com/file/d/1dygoeJaWBjRntRdI4wqXOW7DEp6SJnQz/view?usp=sharing). + +--- + +## Overview + +venue404 is a venue booking platform with three user roles: **users** (browse and book), **owners** (list and manage venues), and **admins** (approve venues and audit activity). The system is built as a **modular monolith**: one deployable backend with internally separated modules, three independent React frontends, and one shared PostgreSQL database. + +The architecture optimizes for two things: + +1. **Shipping speed.** One backend deployable, free-tier infrastructure, no premature distributed-systems complexity. +2. **A clean extraction path.** If any module ever needs to scale separately (likely Payment or Search), it can be lifted into its own service without rewriting the system. + +--- + +## Architectural pattern: Modular monolith + +The backend is a single FastAPI application organized into self-contained modules under `apps/api/app/modules/`. Each module owns its routes, schemas, models, and business logic. Cross-module communication happens through **service interfaces**, never through direct model or database access. + +``` +apps/api/app/ +├── core/ ← infrastructure: DB session, JWT, config +├── shared/ ← reusable utilities (base models, pagination) +├── modules/ ← business modules (the 9 modules below) +└── jobs/ ← background jobs (APScheduler) +``` + +### Why modular monolith, not microservices? + +For an MVP with one team, microservices add operational overhead (multiple deploys, service discovery, distributed tracing, eventual consistency) without proportional benefit. A modular monolith gives the **logical separation** of microservices without the **physical complexity**. + +The boundaries are enforced through convention — not the network — but the cost of crossing them improperly is a code review failure, not a production outage. + +### When to consider extracting a module + +A module is a candidate for extraction into its own service when: + +- It has dramatically different scaling needs than the rest (e.g. Payment hits during peak booking hours; Search hits constantly). +- It needs a different language or runtime. +- It needs independent deploy cadence for safety or compliance reasons. +- Multiple teams own different modules and step on each other during deploys. + +None of these apply yet. Stay monolithic until they do. + +--- + +## Modules + +| Module | Responsibility | +|------------------|---------------------------------------------------------------------------| +| **auth** | JWT issuance/validation, login, signup, role claims | +| **profile** | User, owner, admin profiles | +| **venue** | Venue CRUD, photo management, approval state | +| **search** | Discovery: filtering by city, type, capacity, price, date range | +| **booking** | Booking lifecycle: request → accept → confirm → complete (state machine) | +| **availability** | Time slots, blocked dates, overlap detection | +| **notification** | In-app notifications + transactional email (via Resend) | +| **admin** | Venue approval workflow, suspensions, audit log | +| **payment** | Stripe checkout, refunds, webhook handling | + +Each module folder looks like: + +``` +modules/booking/ +├── routes.py FastAPI router +├── schemas.py Pydantic request/response models +├── models.py SQLAlchemy ORM models +├── service.py Business logic +└── dependencies.py FastAPI dependencies (e.g. require_owner_role) +``` + +### Module rules (enforced by code review) + +These rules exist to keep modules genuinely modular. Breaking them is what turns a "modular monolith" into a regular monolith. + +1. **Modules do not import other modules' models or query each other's tables.** + If `booking` needs venue data, it calls `venue.service.get_venue_by_id(id)`. It does not `from app.modules.venue.models import Venue`. + +2. **Service functions are the only public API of a module.** + Models, schemas, and internal helpers are private. Other modules don't know they exist. + +3. **Shared utilities go in `shared/`, not in modules.** + Resist the urge to put business logic in `shared/`. If two modules need similar logic, it usually means one of them should own it and the other should call its service. + +4. **Cross-cutting infrastructure goes in `core/`.** + Database session, auth middleware, config, exception handlers. These are imported everywhere; they're not modules. + +5. **A module that doesn't own data still has a `service.py`.** + Example: `search` has no models of its own — it queries via `venue.service`. But it still has a `search/service.py` that orchestrates the search logic, so callers depend on `search`, not on `venue` internals. + +--- + +## Data model + +The core entities and how they relate: + +``` +User ──< Booking >── Venue + │ │ + │ └── VenuePhoto + │ └── BlockedDate + ├── Payment + └── StatusHistory +``` + +Key entities: + +- **User** — accounts for all three roles (user/owner/admin), distinguished by a `role` field. +- **Venue** — owned by a user (owner), has photos and blocked dates. Has an `approval_state` (pending/approved/rejected/suspended). +- **Booking** — links a user to a venue for a specific time range. Carries the booking status (see state machine below). +- **StatusHistory** — append-only log of status transitions for a booking. Used for audit and dispute resolution. +- **Payment** — Stripe payment intent records, linked to bookings. + +### Critical DB-level constraint + +The booking system relies on a **PostgreSQL exclusion constraint** to prevent overlapping bookings at the database layer: + +```sql +ALTER TABLE bookings +ADD CONSTRAINT no_overlap +EXCLUDE USING gist ( + venue_id WITH =, + tstzrange(start_time, end_time) WITH && +) WHERE (status IN ('confirmed', 'accepted')); +``` + +This means: even if two race-condition requests get past application-level checks, the database itself rejects the second insert. **Do not rely solely on application code to prevent double-booking.** The exclusion constraint is the source of truth. + +This constraint is added in a hand-edited migration (Alembic's autogenerate does not detect it). + +--- + +## The booking state machine + +This is the most important business flow in the system. Get it wrong and you have angry users, missed payments, or double-bookings. + +### Status definitions + +| Status | Meaning | +|---------------------|----------------------------------------------------------------| +| `requested` | User submitted a booking, awaiting owner decision | +| `accepted` | Owner picked this user; 24-hour payment hold is active | +| `confirmed` | Token advance paid; slot is locked | +| `completed` | Event date has passed | +| `hold_expired` | Accepted user did not pay within 24 hours | +| `request_expired` | Request sat with no movement for 7+ days | +| `conflict_canceled` | Another user got this slot | +| `canceled_by_user` | User cancelled a confirmed booking | +| `canceled_by_owner` | Owner cancelled (any stage) | +| `rejected` | Owner rejected the initial request | + +### Happy path + +``` +1. User submits booking request → status = requested +2. Owner reviews all requesters, + picks one (exclusive selection) → that user: accepted + others: stay requested +3. Selected user pays token advance → that user: confirmed + within 24 hours others: conflict_canceled +4. User pays remaining amount before + deadline (N days before event) +5. Event date passes → status = completed +``` + +### Key rules + +- **Owner acceptance is exclusive.** Picking one user does not auto-reject others; they stay `requested` so the owner can fall back to them if the picked user doesn't pay. +- **Auto-confirmation on payment.** No manual "confirm" step by the owner — Stripe webhook → status update. +- **24-hour payment hold.** If the accepted user doesn't pay in time, status → `hold_expired`, owner is notified to pick the next requester from the still-`requested` pool. +- **7-day request expiry.** Requests with no movement for a week auto-expire to `request_expired` to keep the owner's queue clean. +- **Payment deadline (remaining amount).** Reminders sent at T-7, T-3, T-1 days. On deadline day, owner is notified with options (extend / cancel-forfeit / cancel-with-refund). If owner doesn't act within 48-72 hours, system auto-cancels with advance forfeited. + +### Cancellation policy + +| Initiator | Token advance | Remaining (if paid) | Effect on others | +|-------------------|---------------|---------------------|-----------------------------------------------| +| Owner cancels | Refunded | Refunded | Owner may reactivate `conflict_canceled` users | +| User cancels | **Forfeited** | Refunded | `conflict_canceled` users auto-reactivate | + +Payment gateway fees on refunds: **TBD** — needs decision before launch. Current default: platform absorbs for owner cancellations, deducted from refund for user cancellations. + +--- + +## Background jobs + +Time-based logic runs via **APScheduler** running in-process within the FastAPI app: + +- **Hold expiry** — every 5 minutes, find `accepted` bookings where the 24-hour hold has passed without payment; move to `hold_expired`, notify owner. +- **Stale request cleanup** — daily, expire `requested` bookings older than 7 days. +- **Payment reminders** — daily, send T-7, T-3, T-1 reminders for confirmed bookings approaching the remaining-payment deadline. +- **Booking completion** — daily, move `confirmed` bookings past their event date to `completed`. + +### Caveat for future scaling + +APScheduler runs in-process, so jobs fire on every FastAPI instance. **The system currently assumes a single FastAPI instance.** When scaling to multiple instances: + +- Add Redis + a distributed lock (e.g. via `redis-py-lock`) so each job runs exactly once across the fleet. +- Or migrate to a proper job runner (Celery, RQ, Dramatiq) with a dedicated worker process. + +Not needed for MVP. Tag this in code so it's visible when revisited. + +--- + +## Authentication & authorization + +- **JWT-based**, stateless. Issued by the `auth` module on login/signup, validated on every request via FastAPI middleware in `core/security.py`. +- **Tokens carry the user ID and role claim** (`user` / `owner` / `admin`). +- **Role enforcement** happens in module dependencies — e.g. `Depends(require_owner)` on a route. +- **No external auth provider.** Plain bcrypt password hashing + JWT. If/when social login is needed, plug in Auth0, Supabase Auth, or Clerk later. + +--- + +## External services + +| Service | Purpose | Why this one | +|------------------|--------------------------|---------------------------------------------------------------| +| **Stripe** | Payments | Industry standard, test mode is generous, webhook model is solid | +| **Cloudinary** | Venue photos + image CDN | Free tier covers MVP; transformations on the fly | +| **Resend** | Transactional email | Modern API, 3k emails/month free, simpler than SendGrid | +| **OpenStreetMap** + Leaflet | Venue map previews | Free, no API key, no quota anxiety | +| **Better Stack** | Log aggregation | Free tier sufficient for MVP traffic | + +All external services are accessed only from their respective modules — Stripe from `payment`, Cloudinary from `venue`, Resend from `notification`. No module should call an external service that isn't "theirs." + +--- + +## Frontend architecture + +Three independent React SPAs in `apps/`: + +- **user-web** — public-facing, browse and book venues +- **owner-portal** — venue management, accept/reject requests, view bookings +- **admin-panel** — venue approval, user suspension, audit log + +All three are Vite + React 18 + TypeScript. They share code through two workspace packages: + +- **`@venue404/ui`** — design system: buttons, inputs, cards, modals, date pickers. React is declared as a `peerDependency` to avoid multiple React instances. +- **`@venue404/api-client`** — typed FastAPI client. TypeScript types are **auto-generated from FastAPI's `/openapi.json`** schema, so backend changes surface as compile-time errors in the frontend. + +### Why three apps instead of one with role-based routing? + +Three apps means: +- Smaller bundle sizes — users don't download admin code. +- Clearer separation of concerns; teams (or your future self) can iterate on one without risking the others. +- Independent deploys — a bug in the admin panel doesn't block a user-facing fix. + +The tradeoff is some shared infra duplication (auth context, layout shells). This is mitigated by the shared `@venue404/ui` package. + +--- + +## Hosting & environments + +| Environment | Database | Backend | Frontends | +|--------------|---------------------------|-------------------|----------------------------| +| Local | Postgres 16 (Docker) | FastAPI (Docker) | Vite dev server (native) | +| Staging | Supabase (project #1) | Fly.io (Mumbai) | Vercel preview deploys | +| Production | Supabase (project #2) | Fly.io (Mumbai) | Vercel production deploys | + +- **Same Docker image** runs in local and production (just different env vars). +- **Same migration files** apply across all three environments — only `DATABASE_URL` differs. +- **Fly.io Mumbai region** is chosen for proximity to the target user base (India). +- **Vercel hosts each React app as its own project** with its own domain (`app.`, `owner.`, `admin.`). + +--- + +## Things deliberately NOT in the MVP + +These are tracked but out of scope for v1: + +- **Redis** — caching, distributed locks, session storage. Not needed at MVP scale. +- **Microservices** — see "modular monolith" rationale. +- **Real-time features** (live booking updates, chat). WebSockets / Server-Sent Events can come later. +- **Multi-region** — single Mumbai deploy is enough for India-first launch. +- **Mobile apps** — web-only initially. Native apps wrap or rewrite later if traction warrants. +- **Advanced search** (Elasticsearch, vector search). Postgres full-text + filters is enough until it isn't. +- **Multi-tenant venue chains** — assumed one venue per owner record for now. + +--- + +## Future scaling path (in rough order of likelihood) + +If/when you hit limits, attack in this order: + +1. **Vertical scale Fly.io** instance — bigger machine, cheap and immediate. +2. **Read replicas on Supabase Pro** — separate read/write paths for Search and listing endpoints. +3. **Move APScheduler to a dedicated worker** with Redis-backed job queue (Celery or Dramatiq). +4. **Extract Search module** into its own service with Elasticsearch / Meilisearch / vector search. +5. **Extract Payment module** for compliance isolation and independent deploys. +6. **Add CDN caching** for venue listing pages (Cloudflare or Vercel Edge). +7. **Multi-region** — replicate Postgres, deploy Fly.io to additional regions. + +Each step is a real engineering investment. Don't pre-build. Wait for the metric that justifies it. + +--- + +## References + +- [HLD diagram](./hld.drawio) — visual system overview +- [Booking flow](./booking-flow.md) — full state machine with branch diagrams +- [DB commands](./commands.db.md) — Alembic + migration workflow +- [pnpm commands](./commands/pnpm.md) — monorepo + workspace workflow + +--- + +## Decision log + +Track significant architectural decisions here as they're made. + +| Date | Decision | Rationale | +|------------|-----------------------------------------------------------------------|----------------------------------------------------------------------| +| 2026-05 | Modular monolith over microservices | Single-team MVP, optimize for shipping speed | +| 2026-05 | Three React SPAs over one role-routed app | Bundle size, deploy independence, clearer ownership | +| 2026-05 | pnpm workspaces over npm/yarn | Disk savings, strict dependency resolution, monorepo-first design | +| 2026-05 | APScheduler in-process over Celery/Redis | Simpler for MVP; single instance assumed | +| 2026-05 | Cloudinary over S3 | Free tier covers MVP; image transformations included | +| 2026-05 | Fly.io Mumbai over Render/Railway | Proximity to India-based user base | +| 2026-05 | DB exclusion constraint as source of truth for slot overlaps | Prevents race conditions even if app code has bugs | +| 2026-05 | Owner-picks-one acceptance model over first-come-first-served | Gives owners control; standard for venue/event bookings | +| 2026-05 | User-cancels = advance forfeited; owner-cancels = full refund | Standard industry policy; reflects who broke the commitment | + +When making a new significant decision (new external service, new module, new pattern), add a row here so future contributors understand *why* the system looks the way it does. \ No newline at end of file From af0e897380eb3cc4c0a67bdc9442b99efc676c7b Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 12:51:19 +0100 Subject: [PATCH 010/243] chor: removed local database container --- docker-compose.yml | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e814024ed..89e91439d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,11 @@ services: - db: - image: postgres:16 - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: venue404 - ports: - - "5432:5432" - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - interval: 5s - timeout: 5s - retries: 5 - api: build: ./apps/api restart: unless-stopped - depends_on: - db: - condition: service_healthy env_file: - ./apps/api/.env ports: - "8000:8000" volumes: - ./apps/api:/app - - /app/.venv - -volumes: - pgdata: \ No newline at end of file + - /app/.venv \ No newline at end of file From 17f665602e732e75a18ee9b6dc44173c2a0b6aeb Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 12:51:39 +0100 Subject: [PATCH 011/243] feat: configure backend environment variables and add .env.example templates for all applications --- .gitignore | 3 +++ apps/admin-panel/.env.example | 5 +++++ apps/api/.env.example | 25 ++++++++++++++++--------- apps/api/app/core/config.py | 6 +++--- apps/owner-portal/.env.example | 5 +++++ apps/user-web/.env.example | 5 +++++ 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 01c2eb0e9..134d48248 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ Thumbs.db # ---- Misc ---- prd.txt +auth-prd.md +.claude/ +claude.md \ No newline at end of file diff --git a/apps/admin-panel/.env.example b/apps/admin-panel/.env.example index c2058b5ea..8ccaca929 100644 --- a/apps/admin-panel/.env.example +++ b/apps/admin-panel/.env.example @@ -1 +1,6 @@ +# Supabase (browser client - anon key only) +VITE_SUPABASE_URL=https://your-project-ref.supabase.co +VITE_SUPABASE_ANON_KEY=your-supabase-anon-key + +# FastAPI backend VITE_API_BASE_URL=http://localhost:8000 diff --git a/apps/api/.env.example b/apps/api/.env.example index 9f9b69cbb..8c624bc7c 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,10 +1,17 @@ -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/venue404 -JWT_SECRET=change-me -JWT_ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=30 -STRIPE_SECRET_KEY=sk_test_... -STRIPE_WEBHOOK_SECRET=whsec_... -SMTP_HOST=smtp.example.com +# Database (Supabase direct connection - use this for Alembic migrations) +DATABASE_URL=postgresql+asyncpg://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres + +# Supabase +SUPABASE_URL=https://your-project-ref.supabase.co +SUPABASE_JWT_SECRET=your-supabase-jwt-secret +SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key + +# Stripe +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= + +# Email (SMTP) +SMTP_HOST= SMTP_PORT=587 -SMTP_USER=noreply@example.com -SMTP_PASSWORD=secret +SMTP_USER= +SMTP_PASSWORD= diff --git a/apps/api/app/core/config.py b/apps/api/app/core/config.py index 13f8df454..5e30571af 100644 --- a/apps/api/app/core/config.py +++ b/apps/api/app/core/config.py @@ -3,9 +3,9 @@ class Settings(BaseSettings): database_url: str - jwt_secret: str - jwt_algorithm: str = "HS256" - access_token_expire_minutes: int = 30 + supabase_url: str + supabase_jwt_secret: str + supabase_service_role_key: str stripe_secret_key: str = "" stripe_webhook_secret: str = "" smtp_host: str = "" diff --git a/apps/owner-portal/.env.example b/apps/owner-portal/.env.example index c2058b5ea..8ccaca929 100644 --- a/apps/owner-portal/.env.example +++ b/apps/owner-portal/.env.example @@ -1 +1,6 @@ +# Supabase (browser client - anon key only) +VITE_SUPABASE_URL=https://your-project-ref.supabase.co +VITE_SUPABASE_ANON_KEY=your-supabase-anon-key + +# FastAPI backend VITE_API_BASE_URL=http://localhost:8000 diff --git a/apps/user-web/.env.example b/apps/user-web/.env.example index c2058b5ea..8ccaca929 100644 --- a/apps/user-web/.env.example +++ b/apps/user-web/.env.example @@ -1 +1,6 @@ +# Supabase (browser client - anon key only) +VITE_SUPABASE_URL=https://your-project-ref.supabase.co +VITE_SUPABASE_ANON_KEY=your-supabase-anon-key + +# FastAPI backend VITE_API_BASE_URL=http://localhost:8000 From 2cd10de023b1a23c2501364ed041a4e3155290e8 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 20:40:33 +0100 Subject: [PATCH 012/243] feat: initialize database schema with profiles, user roles, and admin audit logs using Alembic --- apps/api/.env.example | 3 +- apps/api/alembic.ini | 2 +- apps/api/alembic/env.py | 52 ++++++++ apps/api/alembic/script.py.mako | 24 ++++ apps/api/alembic/versions/0001_auth_schema.py | 115 ++++++++++++++++++ apps/api/app/modules/profile/models.py | 66 ++++++++-- 6 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 apps/api/alembic/env.py create mode 100644 apps/api/alembic/script.py.mako create mode 100644 apps/api/alembic/versions/0001_auth_schema.py diff --git a/apps/api/.env.example b/apps/api/.env.example index 8c624bc7c..40b4dfe3d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,5 +1,6 @@ # Database (Supabase direct connection - use this for Alembic migrations) -DATABASE_URL=postgresql+asyncpg://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres +# Find in: Supabase Dashboard -> Project Settings -> Database -> Connection string -> URI +DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres # Supabase SUPABASE_URL=https://your-project-ref.supabase.co diff --git a/apps/api/alembic.ini b/apps/api/alembic.ini index 9ab3e9d0d..581cac9a7 100644 --- a/apps/api/alembic.ini +++ b/apps/api/alembic.ini @@ -1,7 +1,7 @@ [alembic] script_location = alembic prepend_sys_path = . -sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/venue404 +sqlalchemy.url = placeholder_overridden_by_env_py [loggers] keys = root,sqlalchemy,alembic diff --git a/apps/api/alembic/env.py b/apps/api/alembic/env.py new file mode 100644 index 000000000..b03b3de08 --- /dev/null +++ b/apps/api/alembic/env.py @@ -0,0 +1,52 @@ +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context +from app.core.config import settings +from app.core.database import Base + +# import all models so Base.metadata is populated for autogenerate +import app.modules.profile.models # noqa: F401 + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def get_url() -> str: + url = settings.database_url + # Alembic needs a sync driver — strip +asyncpg if present + return url.replace("postgresql+asyncpg://", "postgresql://") + + +def run_migrations_offline() -> None: + context.configure( + url=get_url(), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + configuration = config.get_section(config.config_ini_section, {}) + configuration["sqlalchemy.url"] = get_url() + connectable = engine_from_config( + configuration, + 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/apps/api/alembic/script.py.mako b/apps/api/alembic/script.py.mako new file mode 100644 index 000000000..4940cfda6 --- /dev/null +++ b/apps/api/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${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: 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/apps/api/alembic/versions/0001_auth_schema.py b/apps/api/alembic/versions/0001_auth_schema.py new file mode 100644 index 000000000..1de260172 --- /dev/null +++ b/apps/api/alembic/versions/0001_auth_schema.py @@ -0,0 +1,115 @@ +"""auth schema - profiles, user_roles, admin_actions + +Revision ID: 0001 +Revises: +Create Date: 2026-06-02 + +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +revision: str = "0001" +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: + # enums + profile_status = sa.Enum("active", "suspended", name="profile_status") + user_role = sa.Enum("customer", "venue_owner", "super_admin", name="user_role") + profile_status.create(op.get_bind(), checkfirst=True) + user_role.create(op.get_bind(), checkfirst=True) + + # profiles — id mirrors auth.users.id (Supabase manages auth.users) + op.create_table( + "profiles", + sa.Column("id", UUID(as_uuid=True), primary_key=True), + sa.Column("full_name", sa.String, nullable=True), + sa.Column("phone", sa.String, nullable=True), + sa.Column("avatar_url", sa.String, nullable=True), + sa.Column( + "status", + sa.Enum("active", "suspended", name="profile_status", create_type=False), + nullable=False, + server_default="active", + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.ForeignKeyConstraint( + ["id"], + ["auth.users.id"], + name="fk_profiles_auth_users", + ondelete="CASCADE", + ), + ) + + # user_roles + op.create_table( + "user_roles", + sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("user_id", UUID(as_uuid=True), nullable=False), + sa.Column( + "role", + sa.Enum("customer", "venue_owner", "super_admin", name="user_role", create_type=False), + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["profiles.id"], + name="fk_user_roles_profile", + ondelete="CASCADE", + ), + sa.UniqueConstraint("user_id", "role", name="uq_user_roles_user_role"), + ) + + # admin_actions — append-only audit log + op.create_table( + "admin_actions", + sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("admin_id", UUID(as_uuid=True), nullable=False), + sa.Column("action_type", sa.String, nullable=False), + sa.Column("target_type", sa.String, nullable=False), + sa.Column("target_id", UUID(as_uuid=True), nullable=False), + sa.Column("reason", sa.Text, nullable=True), + sa.Column("metadata", sa.JSON, nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.ForeignKeyConstraint( + ["admin_id"], + ["profiles.id"], + name="fk_admin_actions_profile", + ondelete="RESTRICT", + ), + ) + + +def downgrade() -> None: + op.drop_table("admin_actions") + op.drop_table("user_roles") + op.drop_table("profiles") + sa.Enum(name="user_role").drop(op.get_bind(), checkfirst=True) + sa.Enum(name="profile_status").drop(op.get_bind(), checkfirst=True) diff --git a/apps/api/app/modules/profile/models.py b/apps/api/app/modules/profile/models.py index 57a806012..0e58ff49d 100644 --- a/apps/api/app/modules/profile/models.py +++ b/apps/api/app/modules/profile/models.py @@ -1,21 +1,61 @@ -from sqlalchemy import String, Enum +import enum +import uuid +from datetime import datetime +from sqlalchemy import String, Enum, DateTime, Text, ForeignKey, JSON, UniqueConstraint, func from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base -from app.shared.models import TimestampMixin -import enum + + +class ProfileStatus(str, enum.Enum): + active = "active" + suspended = "suspended" class UserRole(str, enum.Enum): - user = "user" - owner = "owner" - admin = "admin" + customer = "customer" + venue_owner = "venue_owner" + super_admin = "super_admin" + + +class Profile(Base): + __tablename__ = "profiles" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True) + full_name: Mapped[str | None] = mapped_column(String, nullable=True) + phone: Mapped[str | None] = mapped_column(String, nullable=True) + avatar_url: Mapped[str | None] = mapped_column(String, nullable=True) + status: Mapped[ProfileStatus] = mapped_column( + Enum(ProfileStatus, name="profile_status", create_constraint=False), + nullable=False, + default=ProfileStatus.active, + ) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), onupdate=func.now()) + + +class UserRoleAssignment(Base): + __tablename__ = "user_roles" + __table_args__ = (UniqueConstraint("user_id", "role", name="uq_user_roles_user_role"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False) + role: Mapped[UserRole] = mapped_column( + Enum(UserRole, name="user_role", create_constraint=False), + nullable=False, + ) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) -class User(Base, TimestampMixin): - __tablename__ = "users" +class AdminAction(Base): + __tablename__ = "admin_actions" - id: Mapped[str] = mapped_column(String, primary_key=True) - email: Mapped[str] = mapped_column(String, unique=True, nullable=False) - hashed_password: Mapped[str] = mapped_column(String, nullable=False) - full_name: Mapped[str] = mapped_column(String, nullable=False) - role: Mapped[UserRole] = mapped_column(Enum(UserRole), default=UserRole.user) + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + admin_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="RESTRICT"), nullable=False) + action_type: Mapped[str] = mapped_column(String, nullable=False) + target_type: Mapped[str] = mapped_column(String, nullable=False) + target_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + reason: Mapped[str | None] = mapped_column(Text, nullable=True) + action_metadata: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) From 9c30f8002ac9d4abfec7f5d51e0893a4a3c5c9b9 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 21:19:39 +0100 Subject: [PATCH 013/243] fix: fixed database auth schema --- apps/api/.env.example | 2 +- apps/api/alembic/versions/0001_auth_schema.py | 24 +++++--- .../alembic/versions/0002_signup_trigger.py | 59 +++++++++++++++++++ 3 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 apps/api/alembic/versions/0002_signup_trigger.py diff --git a/apps/api/.env.example b/apps/api/.env.example index 40b4dfe3d..67daf53ac 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,6 +1,6 @@ # Database (Supabase direct connection - use this for Alembic migrations) # Find in: Supabase Dashboard -> Project Settings -> Database -> Connection string -> URI -DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres +DATABASE_URL=postgresql://postgres.qcctiopmoscndyyitzpg:[YOUR-PASSWORD]@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres # Supabase SUPABASE_URL=https://your-project-ref.supabase.co diff --git a/apps/api/alembic/versions/0001_auth_schema.py b/apps/api/alembic/versions/0001_auth_schema.py index 1de260172..a75ad1419 100644 --- a/apps/api/alembic/versions/0001_auth_schema.py +++ b/apps/api/alembic/versions/0001_auth_schema.py @@ -8,7 +8,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import UUID, ENUM as PgEnum revision: str = "0001" down_revision: Union[str, None] = None @@ -17,11 +17,19 @@ def upgrade() -> None: - # enums - profile_status = sa.Enum("active", "suspended", name="profile_status") - user_role = sa.Enum("customer", "venue_owner", "super_admin", name="user_role") - profile_status.create(op.get_bind(), checkfirst=True) - user_role.create(op.get_bind(), checkfirst=True) + # create enums via raw SQL — DO block silently skips if already exists + op.execute(""" + DO $$ BEGIN + CREATE TYPE profile_status AS ENUM ('active', 'suspended'); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + """) + op.execute(""" + DO $$ BEGIN + CREATE TYPE user_role AS ENUM ('customer', 'venue_owner', 'super_admin'); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + """) # profiles — id mirrors auth.users.id (Supabase manages auth.users) op.create_table( @@ -32,7 +40,7 @@ def upgrade() -> None: sa.Column("avatar_url", sa.String, nullable=True), sa.Column( "status", - sa.Enum("active", "suspended", name="profile_status", create_type=False), + PgEnum("active", "suspended", name="profile_status", create_type=False), nullable=False, server_default="active", ), @@ -64,7 +72,7 @@ def upgrade() -> None: sa.Column("user_id", UUID(as_uuid=True), nullable=False), sa.Column( "role", - sa.Enum("customer", "venue_owner", "super_admin", name="user_role", create_type=False), + PgEnum("customer", "venue_owner", "super_admin", name="user_role", create_type=False), nullable=False, ), sa.Column( diff --git a/apps/api/alembic/versions/0002_signup_trigger.py b/apps/api/alembic/versions/0002_signup_trigger.py new file mode 100644 index 000000000..62fe12c63 --- /dev/null +++ b/apps/api/alembic/versions/0002_signup_trigger.py @@ -0,0 +1,59 @@ +"""signup trigger - auto-create profile and customer role on auth.users insert + +Revision ID: 0002 +Revises: 0001 +Create Date: 2026-06-02 + +""" +from typing import Sequence, Union +from alembic import op + +revision: str = "0002" +down_revision: Union[str, None] = "0001" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute(""" + CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, full_name, phone, status, created_at, updated_at) + VALUES ( + NEW.id, + NEW.raw_user_meta_data ->> 'full_name', + NEW.raw_user_meta_data ->> 'phone', + 'active', + now(), + now() + ) + ON CONFLICT (id) DO NOTHING; + + INSERT INTO public.user_roles (id, user_id, role, created_at) + VALUES ( + gen_random_uuid(), + NEW.id, + 'customer', + now() + ) + ON CONFLICT (user_id, role) DO NOTHING; + + RETURN NEW; + END; + $$; + """) + + op.execute(""" + CREATE OR REPLACE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + """) + + +def downgrade() -> None: + op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;") + op.execute("DROP FUNCTION IF EXISTS public.handle_new_user();") From 4a9eb2146313b1ce82ff0a22aa35a72347ff5413 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 21:35:33 +0100 Subject: [PATCH 014/243] fix: fixed authentication auth login and register routes --- apps/api/app/modules/auth/routes.py | 16 ++++++---------- apps/api/app/modules/auth/schemas.py | 25 ++++++++++++------------- apps/api/app/modules/auth/service.py | 24 +++++++++++++++--------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/apps/api/app/modules/auth/routes.py b/apps/api/app/modules/auth/routes.py index 4f7ae2d11..cbb18b33f 100644 --- a/apps/api/app/modules/auth/routes.py +++ b/apps/api/app/modules/auth/routes.py @@ -1,15 +1,11 @@ -from fastapi import APIRouter -from app.modules.auth.schemas import LoginRequest, TokenResponse, RegisterRequest +from fastapi import APIRouter, Depends +from app.modules.auth.dependencies import get_current_user +from app.modules.auth.schemas import AuthMeResponse from app.modules.auth import service router = APIRouter() -@router.post("/register", response_model=TokenResponse) -def register(body: RegisterRequest): - return service.register(body) - - -@router.post("/login", response_model=TokenResponse) -def login(body: LoginRequest): - return service.login(body) +@router.get("/me", response_model=AuthMeResponse) +async def me(current_user=Depends(get_current_user)): + return await service.get_me(current_user) diff --git a/apps/api/app/modules/auth/schemas.py b/apps/api/app/modules/auth/schemas.py index e4fb34ffd..23013a3d8 100644 --- a/apps/api/app/modules/auth/schemas.py +++ b/apps/api/app/modules/auth/schemas.py @@ -1,17 +1,16 @@ -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel +from uuid import UUID -class RegisterRequest(BaseModel): - email: EmailStr - password: str - full_name: str +class ProfileResponse(BaseModel): + full_name: str | None + phone: str | None + avatar_url: str | None + status: str -class LoginRequest(BaseModel): - email: EmailStr - password: str - - -class TokenResponse(BaseModel): - access_token: str - token_type: str = "bearer" +class AuthMeResponse(BaseModel): + id: UUID + email: str | None + profile: ProfileResponse + roles: list[str] diff --git a/apps/api/app/modules/auth/service.py b/apps/api/app/modules/auth/service.py index 746752ca6..66c4199fe 100644 --- a/apps/api/app/modules/auth/service.py +++ b/apps/api/app/modules/auth/service.py @@ -1,9 +1,15 @@ -from app.modules.auth.schemas import LoginRequest, RegisterRequest, TokenResponse - - -def register(body: RegisterRequest) -> TokenResponse: - raise NotImplementedError - - -def login(body: LoginRequest) -> TokenResponse: - raise NotImplementedError +from app.modules.auth.schemas import AuthMeResponse, ProfileResponse + + +async def get_me(current_user) -> AuthMeResponse: + return AuthMeResponse( + id=current_user.user_id, + email=current_user.email, + profile=ProfileResponse( + full_name=current_user.full_name, + phone=current_user.phone, + avatar_url=current_user.avatar_url, + status=current_user.status, + ), + roles=current_user.roles, + ) From 38179373c3be36ee04c4145773d084c8d29d89b3 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 21:35:54 +0100 Subject: [PATCH 015/243] feat: initialize api-client package with Supabase integration and authentication helpers --- packages/api-client/package.json | 3 ++ packages/api-client/src/auth.ts | 59 +++++++++++++++++++++++ packages/api-client/src/endpoints/auth.ts | 17 +++++-- packages/api-client/src/index.ts | 2 + packages/api-client/src/supabase.ts | 10 ++++ 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 packages/api-client/src/auth.ts create mode 100644 packages/api-client/src/supabase.ts diff --git a/packages/api-client/package.json b/packages/api-client/package.json index d3519b9cc..669940dfa 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -7,6 +7,9 @@ "generate": "tsx scripts/generate.ts", "lint": "eslint src" }, + "dependencies": { + "@supabase/supabase-js": "^2.45.0" + }, "devDependencies": { "tsx": "^4.0.0", "typescript": "^5.4.0" diff --git a/packages/api-client/src/auth.ts b/packages/api-client/src/auth.ts new file mode 100644 index 000000000..b0423c466 --- /dev/null +++ b/packages/api-client/src/auth.ts @@ -0,0 +1,59 @@ +import { supabase } from './supabase' +import type { AuthChangeEvent, Session } from '@supabase/supabase-js' + +export type SignUpInput = { + email: string + password: string + fullName: string + phone?: string +} + +export type SignInInput = { + email: string + password: string +} + +export async function signUpWithEmail(input: SignUpInput) { + const { data, error } = await supabase.auth.signUp({ + email: input.email, + password: input.password, + options: { + data: { + full_name: input.fullName, + phone: input.phone ?? null, + }, + }, + }) + if (error) throw error + return data +} + +export async function signInWithEmail(input: SignInInput) { + const { data, error } = await supabase.auth.signInWithPassword({ + email: input.email, + password: input.password, + }) + if (error) throw error + return data +} + +export async function signOut() { + const { error } = await supabase.auth.signOut() + if (error) throw error +} + +export async function getSession() { + const { data, error } = await supabase.auth.getSession() + if (error) throw error + return data.session +} + +export async function getAccessToken(): Promise { + const session = await getSession() + return session?.access_token ?? null +} + +export function onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => void) { + const { data } = supabase.auth.onAuthStateChange(callback) + return data.subscription +} diff --git a/packages/api-client/src/endpoints/auth.ts b/packages/api-client/src/endpoints/auth.ts index 0ac3fd0e6..e9b960cf2 100644 --- a/packages/api-client/src/endpoints/auth.ts +++ b/packages/api-client/src/endpoints/auth.ts @@ -1,8 +1,17 @@ import { createClient } from '../client' +export type AuthUser = { + id: string + email: string | null + profile: { + full_name: string | null + phone: string | null + avatar_url: string | null + status: 'active' | 'suspended' + } + roles: string[] +} + export const authEndpoints = (client: ReturnType) => ({ - register: (body: { email: string; password: string; full_name: string }) => - client.post<{ access_token: string; token_type: string }>('/api/auth/register', body), - login: (body: { email: string; password: string }) => - client.post<{ access_token: string; token_type: string }>('/api/auth/login', body), + me: () => client.get('/api/auth/me'), }) diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 38cc55711..0fe85ea7f 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -1,4 +1,6 @@ export { createClient } from './client' +export { supabase } from './supabase' +export * from './auth' export * from './types' export * from './endpoints/auth' export * from './endpoints/venues' diff --git a/packages/api-client/src/supabase.ts b/packages/api-client/src/supabase.ts new file mode 100644 index 000000000..79c4e2c5a --- /dev/null +++ b/packages/api-client/src/supabase.ts @@ -0,0 +1,10 @@ +import { createClient } from '@supabase/supabase-js' + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY in environment') +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey) From 1b299506da66ea41514a68a77a875dda4bdde4f3 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 21:47:27 +0100 Subject: [PATCH 016/243] feat: add specific error messages for authentication --- apps/api/app/modules/auth/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/app/modules/auth/dependencies.py b/apps/api/app/modules/auth/dependencies.py index 60a01078b..6446e3d7f 100644 --- a/apps/api/app/modules/auth/dependencies.py +++ b/apps/api/app/modules/auth/dependencies.py @@ -7,10 +7,10 @@ def get_current_user(authorization: str = Header(...)): try: scheme, token = authorization.split() if scheme.lower() != "bearer": - raise UnauthorizedError() + raise UnauthorizedError("Invalid authentication scheme") return decode_token(token) - except Exception: - raise UnauthorizedError() + except Exception as e: + raise UnauthorizedError(f"Authentication failed: {e}") def require_role(*roles: str): From bf0f7b8d09ab9538b241890744db4abbd4b69062 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 22:00:41 +0100 Subject: [PATCH 017/243] feat: Shared API Client with Token Injection to every frontend request --- packages/api-client/src/client.ts | 29 +++++++++++++++++++++++++---- packages/api-client/src/index.ts | 2 +- packages/api-client/src/types.ts | 2 ++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts index b62876ed4..8c73baa82 100644 --- a/packages/api-client/src/client.ts +++ b/packages/api-client/src/client.ts @@ -1,16 +1,37 @@ +import { getAccessToken, signOut } from './auth' + const BASE_URL = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_BASE_URL) ?? 'http://localhost:8000' -export function createClient(token?: string) { - const headers: Record = { 'Content-Type': 'application/json' } - if (token) headers['Authorization'] = `Bearer ${token}` +export class ApiError extends Error { + constructor(public status: number, message: string) { + super(message) + this.name = 'ApiError' + } +} +export function createClient() { async function request(method: string, path: string, body?: unknown): Promise { + const token = await getAccessToken() + + const headers: Record = { 'Content-Type': 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + const res = await fetch(`${BASE_URL}${path}`, { method, headers, body: body != null ? JSON.stringify(body) : undefined, }) - if (!res.ok) throw new Error(`${method} ${path} → ${res.status}`) + + if (res.status === 401) { + await signOut() + throw new ApiError(401, 'Session expired — signed out') + } + + if (!res.ok) { + const detail = await res.json().catch(() => ({ detail: res.statusText })) + throw new ApiError(res.status, detail?.detail ?? `${method} ${path} → ${res.status}`) + } + return res.json() as Promise } diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 0fe85ea7f..5ad42a327 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -1,4 +1,4 @@ -export { createClient } from './client' +export { createClient, ApiError } from './client' export { supabase } from './supabase' export * from './auth' export * from './types' diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts index 1f302b5e2..b5f2763bd 100644 --- a/packages/api-client/src/types.ts +++ b/packages/api-client/src/types.ts @@ -1 +1,3 @@ // AUTO-GENERATED — do not edit. Run `pnpm generate` in packages/api-client to refresh. + +export {} From f2dad914c68370979ec5906d341dce842a0140f9 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 23:04:20 +0100 Subject: [PATCH 018/243] feat: implement AuthProvider base class and Supabase authentication provider --- .../app/modules/auth/providers/__init__.py | 0 apps/api/app/modules/auth/providers/base.py | 18 +++++++++++++ .../app/modules/auth/providers/supabase.py | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 apps/api/app/modules/auth/providers/__init__.py create mode 100644 apps/api/app/modules/auth/providers/base.py create mode 100644 apps/api/app/modules/auth/providers/supabase.py diff --git a/apps/api/app/modules/auth/providers/__init__.py b/apps/api/app/modules/auth/providers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/api/app/modules/auth/providers/base.py b/apps/api/app/modules/auth/providers/base.py new file mode 100644 index 000000000..7559edcc7 --- /dev/null +++ b/apps/api/app/modules/auth/providers/base.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from uuid import UUID + + +@dataclass +class ProviderUser: + id: UUID + email: str | None + + +class AuthProvider(ABC): + @abstractmethod + async def verify_token(self, token: str) -> ProviderUser: + """Verify the token and return a normalized ProviderUser. + Raise UnauthorizedError if the token is invalid or expired. + """ + ... diff --git a/apps/api/app/modules/auth/providers/supabase.py b/apps/api/app/modules/auth/providers/supabase.py new file mode 100644 index 000000000..470031a7f --- /dev/null +++ b/apps/api/app/modules/auth/providers/supabase.py @@ -0,0 +1,27 @@ +from uuid import UUID +from jose import jwt, JWTError +from app.core.config import settings +from app.core.exceptions import UnauthorizedError +from app.modules.auth.providers.base import AuthProvider, ProviderUser + + +class SupabaseAuthProvider(AuthProvider): + async def verify_token(self, token: str) -> ProviderUser: + try: + payload = jwt.decode( + token, + settings.supabase_jwt_secret, + algorithms=["HS256"], + audience="authenticated", + ) + except JWTError: + raise UnauthorizedError("Invalid or expired token") + + sub = payload.get("sub") + if not sub: + raise UnauthorizedError("Token missing subject claim") + + return ProviderUser( + id=UUID(sub), + email=payload.get("email"), + ) From ce8a4e6a2a36f3eeffeac79a25c5d698bbeb8ca2 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 2 Jun 2026 23:20:53 +0100 Subject: [PATCH 019/243] feat: implement authorization dependency layer and Supabase JWT verification provider --- apps/api/app/modules/auth/dependencies.py | 120 ++++++++++++++++-- apps/api/app/modules/auth/providers/base.py | 2 +- .../app/modules/auth/providers/supabase.py | 2 +- 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/apps/api/app/modules/auth/dependencies.py b/apps/api/app/modules/auth/dependencies.py index 6446e3d7f..8b7c40fc1 100644 --- a/apps/api/app/modules/auth/dependencies.py +++ b/apps/api/app/modules/auth/dependencies.py @@ -1,21 +1,113 @@ +from dataclasses import dataclass +from uuid import UUID from fastapi import Depends, Header -from app.core.security import decode_token -from app.core.exceptions import UnauthorizedError +from sqlalchemy.orm import Session +from app.core.database import get_db +from app.core.exceptions import UnauthorizedError, ForbiddenError +from app.modules.auth.providers.supabase import SupabaseAuthProvider +from app.modules.profile.models import Profile, UserRoleAssignment -def get_current_user(authorization: str = Header(...)): - try: - scheme, token = authorization.split() - if scheme.lower() != "bearer": - raise UnauthorizedError("Invalid authentication scheme") - return decode_token(token) - except Exception as e: - raise UnauthorizedError(f"Authentication failed: {e}") +_auth_provider = SupabaseAuthProvider() + + +@dataclass +class AuthContext: + user_id: UUID + email: str | None + full_name: str | None + phone: str | None + avatar_url: str | None + status: str + roles: list[str] + + def has_role(self, role: str) -> bool: + return role in self.roles + + def is_admin(self) -> bool: + return "super_admin" in self.roles + + def is_owner(self) -> bool: + return "venue_owner" in self.roles or self.is_admin() + + +def _extract_bearer_token(authorization: str) -> str: + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != "bearer": + raise UnauthorizedError("Invalid authorization header") + return parts[1] + + +def get_current_user( + authorization: str = Header(...), + db: Session = Depends(get_db), +) -> AuthContext: + token = _extract_bearer_token(authorization) + provider_user = _auth_provider.verify_token(token) + + profile = db.query(Profile).filter( + Profile.id == provider_user.id, + Profile.deleted_at.is_(None), + ).first() + + if not profile: + raise ForbiddenError("Account not found") + + if profile.status.value == "suspended": + raise ForbiddenError("Account suspended") + + role_rows = db.query(UserRoleAssignment).filter( + UserRoleAssignment.user_id == provider_user.id + ).all() + + return AuthContext( + user_id=profile.id, + email=provider_user.email, + full_name=profile.full_name, + phone=profile.phone, + avatar_url=profile.avatar_url, + status=profile.status.value, + roles=[r.role.value for r in role_rows], + ) + + +def require_auth( + current_user: AuthContext = Depends(get_current_user), +) -> AuthContext: + return current_user def require_role(*roles: str): - def dependency(user=Depends(get_current_user)): - if user.get("role") not in roles: - raise UnauthorizedError("Insufficient permissions") - return user + def dependency( + current_user: AuthContext = Depends(get_current_user), + ) -> AuthContext: + if not any(r in current_user.roles for r in roles): + raise ForbiddenError("Insufficient permissions") + return current_user return dependency + + +def require_any_role(roles: list[str]): + def dependency( + current_user: AuthContext = Depends(get_current_user), + ) -> AuthContext: + if not any(r in current_user.roles for r in roles): + raise ForbiddenError("Insufficient permissions") + return current_user + return dependency + + +def require_admin( + current_user: AuthContext = Depends(get_current_user), +) -> AuthContext: + if not current_user.is_admin(): + raise ForbiddenError("Admin access required") + return current_user + + +def require_owner( + current_user: AuthContext = Depends(get_current_user), +) -> AuthContext: + if not current_user.is_owner(): + raise ForbiddenError("Venue owner access required") + return current_user diff --git a/apps/api/app/modules/auth/providers/base.py b/apps/api/app/modules/auth/providers/base.py index 7559edcc7..cb5f35924 100644 --- a/apps/api/app/modules/auth/providers/base.py +++ b/apps/api/app/modules/auth/providers/base.py @@ -11,7 +11,7 @@ class ProviderUser: class AuthProvider(ABC): @abstractmethod - async def verify_token(self, token: str) -> ProviderUser: + def verify_token(self, token: str) -> ProviderUser: """Verify the token and return a normalized ProviderUser. Raise UnauthorizedError if the token is invalid or expired. """ diff --git a/apps/api/app/modules/auth/providers/supabase.py b/apps/api/app/modules/auth/providers/supabase.py index 470031a7f..e6d892072 100644 --- a/apps/api/app/modules/auth/providers/supabase.py +++ b/apps/api/app/modules/auth/providers/supabase.py @@ -6,7 +6,7 @@ class SupabaseAuthProvider(AuthProvider): - async def verify_token(self, token: str) -> ProviderUser: + def verify_token(self, token: str) -> ProviderUser: try: payload = jwt.decode( token, From 506857fb79ff0a2da3f0b9473d7f7642915571ac Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 01:00:04 +0100 Subject: [PATCH 020/243] feat: add /me endpoint to retrieve current user profile and authentication details --- apps/api/app/modules/auth/routes.py | 6 +++--- apps/api/app/modules/auth/service.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/app/modules/auth/routes.py b/apps/api/app/modules/auth/routes.py index cbb18b33f..f9ae79990 100644 --- a/apps/api/app/modules/auth/routes.py +++ b/apps/api/app/modules/auth/routes.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends -from app.modules.auth.dependencies import get_current_user +from app.modules.auth.dependencies import get_current_user, AuthContext from app.modules.auth.schemas import AuthMeResponse from app.modules.auth import service @@ -7,5 +7,5 @@ @router.get("/me", response_model=AuthMeResponse) -async def me(current_user=Depends(get_current_user)): - return await service.get_me(current_user) +def me(current_user: AuthContext = Depends(get_current_user)): + return service.get_me(current_user) diff --git a/apps/api/app/modules/auth/service.py b/apps/api/app/modules/auth/service.py index 66c4199fe..7eb490dd0 100644 --- a/apps/api/app/modules/auth/service.py +++ b/apps/api/app/modules/auth/service.py @@ -1,7 +1,7 @@ from app.modules.auth.schemas import AuthMeResponse, ProfileResponse -async def get_me(current_user) -> AuthMeResponse: +def get_me(current_user) -> AuthMeResponse: return AuthMeResponse( id=current_user.user_id, email=current_user.email, From 647a16d365ceb83f7ee3c400c603843f6afd239e Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 01:20:35 +0100 Subject: [PATCH 021/243] feat: implement cross-app authentication, session management, and role-based access control across all portals --- .../src/components/ProtectedRoute.tsx | 16 ++++ apps/admin-panel/src/lib/AuthContext.tsx | 80 ++++++++++++++++++ apps/admin-panel/src/main.tsx | 5 +- apps/admin-panel/src/pages/Login.tsx | 64 ++++++++++++++ apps/admin-panel/src/routes.tsx | 20 ++++- apps/owner-portal/.env.example | 4 + .../src/components/ProtectedRoute.tsx | 18 ++++ apps/owner-portal/src/lib/AuthContext.tsx | 80 ++++++++++++++++++ apps/owner-portal/src/main.tsx | 5 +- apps/owner-portal/src/pages/Login.tsx | 69 +++++++++++++++ apps/owner-portal/src/routes.tsx | 20 ++++- apps/user-web/.env.example | 4 + .../src/components/ProtectedRoute.tsx | 21 +++++ apps/user-web/src/lib/AuthContext.tsx | 84 +++++++++++++++++++ apps/user-web/src/main.tsx | 5 +- apps/user-web/src/pages/Login.tsx | 78 +++++++++++++++++ apps/user-web/src/routes.tsx | 18 +++- pnpm-lock.yaml | 69 +++++++++++++++ 18 files changed, 650 insertions(+), 10 deletions(-) create mode 100644 apps/admin-panel/src/components/ProtectedRoute.tsx create mode 100644 apps/admin-panel/src/lib/AuthContext.tsx create mode 100644 apps/admin-panel/src/pages/Login.tsx create mode 100644 apps/owner-portal/src/components/ProtectedRoute.tsx create mode 100644 apps/owner-portal/src/lib/AuthContext.tsx create mode 100644 apps/owner-portal/src/pages/Login.tsx create mode 100644 apps/user-web/src/components/ProtectedRoute.tsx create mode 100644 apps/user-web/src/lib/AuthContext.tsx create mode 100644 apps/user-web/src/pages/Login.tsx diff --git a/apps/admin-panel/src/components/ProtectedRoute.tsx b/apps/admin-panel/src/components/ProtectedRoute.tsx new file mode 100644 index 000000000..015436f0d --- /dev/null +++ b/apps/admin-panel/src/components/ProtectedRoute.tsx @@ -0,0 +1,16 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) return null + + if (!user) return + + if (!user.roles.includes('super_admin')) { + return + } + + return <>{children} +} diff --git a/apps/admin-panel/src/lib/AuthContext.tsx b/apps/admin-panel/src/lib/AuthContext.tsx new file mode 100644 index 000000000..dbdabf941 --- /dev/null +++ b/apps/admin-panel/src/lib/AuthContext.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext, useEffect, useState } from 'react' +import { + createClient, + onAuthStateChange, + signInWithEmail, + signUpWithEmail, + signOut as supabaseSignOut, + type SignInInput, + type SignUpInput, +} from '@venue404/api-client' +import { authEndpoints, type AuthUser } from '@venue404/api-client' + +type AuthState = { + user: AuthUser | null + loading: boolean + signIn: (input: SignInInput) => Promise + signUp: (input: SignUpInput) => Promise + signOut: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + async function loadUser() { + try { + const client = createClient() + const authUser = await authEndpoints(client).me() + setUser(authUser) + } catch { + setUser(null) + } + } + + useEffect(() => { + const subscription = onAuthStateChange(async (event, session) => { + if (event === 'SIGNED_OUT') { + setUser(null) + setLoading(false) + return + } + + if ((event === 'INITIAL_SESSION' || event === 'SIGNED_IN') && session) { + await loadUser() + setLoading(false) + return + } + + setLoading(false) + }) + + return () => subscription.unsubscribe() + }, []) + + async function signIn(input: SignInInput) { + await signInWithEmail(input) + } + + async function signUp(input: SignUpInput) { + await signUpWithEmail(input) + } + + async function signOut() { + await supabaseSignOut() + } + + return ( + + {children} + + ) +} + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used inside AuthProvider') + return ctx +} diff --git a/apps/admin-panel/src/main.tsx b/apps/admin-panel/src/main.tsx index fda61b388..9d474e208 100644 --- a/apps/admin-panel/src/main.tsx +++ b/apps/admin-panel/src/main.tsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { AuthProvider } from './lib/AuthContext' import './styles/index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/apps/admin-panel/src/pages/Login.tsx b/apps/admin-panel/src/pages/Login.tsx new file mode 100644 index 000000000..121a69b69 --- /dev/null +++ b/apps/admin-panel/src/pages/Login.tsx @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +export default function Login() { + const { user, loading, signIn } = useAuth() + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (!loading && user) { + if (user.roles.includes('super_admin')) { + navigate('/', { replace: true }) + } else { + // not an admin — show 403 + navigate('/403', { replace: true }) + } + } + }, [user, loading]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setSubmitting(true) + try { + await signIn({ email, password }) + } catch (err: any) { + setError(err.message ?? 'Login failed') + } finally { + setSubmitting(false) + } + } + + if (loading) return null + + return ( +
+

Admin Panel — Sign in

+
+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + {error &&

{error}

} + +
+
+ ) +} diff --git a/apps/admin-panel/src/routes.tsx b/apps/admin-panel/src/routes.tsx index 4d6cb038e..397732dbf 100644 --- a/apps/admin-panel/src/routes.tsx +++ b/apps/admin-panel/src/routes.tsx @@ -1,10 +1,24 @@ import { createBrowserRouter } from 'react-router-dom' +import { ProtectedRoute } from './components/ProtectedRoute' import Dashboard from './pages/Dashboard' import VenueApprovals from './pages/VenueApprovals' import Users from './pages/Users' +import Login from './pages/Login' export const router = createBrowserRouter([ - { path: '/', element: }, - { path: '/approvals', element: }, - { path: '/users', element: }, + { path: '/login', element: }, + { path: '/403', element:
Access denied. Super admin only.
}, + + { + path: '/', + element: , + }, + { + path: '/approvals', + element: , + }, + { + path: '/users', + element: , + }, ]) diff --git a/apps/owner-portal/.env.example b/apps/owner-portal/.env.example index 8ccaca929..04ed7197d 100644 --- a/apps/owner-portal/.env.example +++ b/apps/owner-portal/.env.example @@ -4,3 +4,7 @@ VITE_SUPABASE_ANON_KEY=your-supabase-anon-key # FastAPI backend VITE_API_BASE_URL=http://localhost:8000 + +# Cross-app redirect URLs +VITE_USER_WEB_URL=http://localhost:5173 +VITE_ADMIN_PANEL_URL=http://localhost:5175 diff --git a/apps/owner-portal/src/components/ProtectedRoute.tsx b/apps/owner-portal/src/components/ProtectedRoute.tsx new file mode 100644 index 000000000..a06ec8c8a --- /dev/null +++ b/apps/owner-portal/src/components/ProtectedRoute.tsx @@ -0,0 +1,18 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +const ALLOWED_ROLES = ['venue_owner', 'super_admin'] + +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) return null + + if (!user) return + + if (!ALLOWED_ROLES.some(r => user.roles.includes(r))) { + return + } + + return <>{children} +} diff --git a/apps/owner-portal/src/lib/AuthContext.tsx b/apps/owner-portal/src/lib/AuthContext.tsx new file mode 100644 index 000000000..dbdabf941 --- /dev/null +++ b/apps/owner-portal/src/lib/AuthContext.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext, useEffect, useState } from 'react' +import { + createClient, + onAuthStateChange, + signInWithEmail, + signUpWithEmail, + signOut as supabaseSignOut, + type SignInInput, + type SignUpInput, +} from '@venue404/api-client' +import { authEndpoints, type AuthUser } from '@venue404/api-client' + +type AuthState = { + user: AuthUser | null + loading: boolean + signIn: (input: SignInInput) => Promise + signUp: (input: SignUpInput) => Promise + signOut: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + async function loadUser() { + try { + const client = createClient() + const authUser = await authEndpoints(client).me() + setUser(authUser) + } catch { + setUser(null) + } + } + + useEffect(() => { + const subscription = onAuthStateChange(async (event, session) => { + if (event === 'SIGNED_OUT') { + setUser(null) + setLoading(false) + return + } + + if ((event === 'INITIAL_SESSION' || event === 'SIGNED_IN') && session) { + await loadUser() + setLoading(false) + return + } + + setLoading(false) + }) + + return () => subscription.unsubscribe() + }, []) + + async function signIn(input: SignInInput) { + await signInWithEmail(input) + } + + async function signUp(input: SignUpInput) { + await signUpWithEmail(input) + } + + async function signOut() { + await supabaseSignOut() + } + + return ( + + {children} + + ) +} + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used inside AuthProvider') + return ctx +} diff --git a/apps/owner-portal/src/main.tsx b/apps/owner-portal/src/main.tsx index fda61b388..9d474e208 100644 --- a/apps/owner-portal/src/main.tsx +++ b/apps/owner-portal/src/main.tsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { AuthProvider } from './lib/AuthContext' import './styles/index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/apps/owner-portal/src/pages/Login.tsx b/apps/owner-portal/src/pages/Login.tsx new file mode 100644 index 000000000..60656db6a --- /dev/null +++ b/apps/owner-portal/src/pages/Login.tsx @@ -0,0 +1,69 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +const USER_WEB_URL = import.meta.env.VITE_USER_WEB_URL ?? 'http://localhost:5173' +const ADMIN_PANEL_URL = import.meta.env.VITE_ADMIN_PANEL_URL ?? 'http://localhost:5175' + +export default function Login() { + const { user, loading, signIn } = useAuth() + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (!loading && user) { + if (user.roles.includes('super_admin')) { + window.location.href = ADMIN_PANEL_URL + } else if (user.roles.includes('venue_owner')) { + navigate('/', { replace: true }) + } else { + // customer — send them to user-web + window.location.href = USER_WEB_URL + } + } + }, [user, loading]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setSubmitting(true) + try { + await signIn({ email, password }) + } catch (err: any) { + setError(err.message ?? 'Login failed') + } finally { + setSubmitting(false) + } + } + + if (loading) return null + + return ( +
+

Owner Portal — Sign in

+
+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + {error &&

{error}

} + +
+
+ ) +} diff --git a/apps/owner-portal/src/routes.tsx b/apps/owner-portal/src/routes.tsx index ab6b7c48a..47b7d7318 100644 --- a/apps/owner-portal/src/routes.tsx +++ b/apps/owner-portal/src/routes.tsx @@ -1,10 +1,24 @@ import { createBrowserRouter } from 'react-router-dom' +import { ProtectedRoute } from './components/ProtectedRoute' import Dashboard from './pages/Dashboard' import ManageVenues from './pages/ManageVenues' import Bookings from './pages/Bookings' +import Login from './pages/Login' export const router = createBrowserRouter([ - { path: '/', element: }, - { path: '/venues', element: }, - { path: '/bookings', element: }, + { path: '/login', element: }, + { path: '/403', element:
Access denied. You need a venue owner account.
}, + + { + path: '/', + element: , + }, + { + path: '/venues', + element: , + }, + { + path: '/bookings', + element: , + }, ]) diff --git a/apps/user-web/.env.example b/apps/user-web/.env.example index 8ccaca929..1d5bf7f99 100644 --- a/apps/user-web/.env.example +++ b/apps/user-web/.env.example @@ -4,3 +4,7 @@ VITE_SUPABASE_ANON_KEY=your-supabase-anon-key # FastAPI backend VITE_API_BASE_URL=http://localhost:8000 + +# Cross-app redirect URLs (after login based on role) +VITE_OWNER_PORTAL_URL=http://localhost:5174 +VITE_ADMIN_PANEL_URL=http://localhost:5175 diff --git a/apps/user-web/src/components/ProtectedRoute.tsx b/apps/user-web/src/components/ProtectedRoute.tsx new file mode 100644 index 000000000..ff17cab0f --- /dev/null +++ b/apps/user-web/src/components/ProtectedRoute.tsx @@ -0,0 +1,21 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +type Props = { + children: React.ReactNode + requiredRoles?: string[] +} + +export function ProtectedRoute({ children, requiredRoles }: Props) { + const { user, loading } = useAuth() + + if (loading) return null + + if (!user) return + + if (requiredRoles && !requiredRoles.some(r => user.roles.includes(r))) { + return + } + + return <>{children} +} diff --git a/apps/user-web/src/lib/AuthContext.tsx b/apps/user-web/src/lib/AuthContext.tsx new file mode 100644 index 000000000..31f152063 --- /dev/null +++ b/apps/user-web/src/lib/AuthContext.tsx @@ -0,0 +1,84 @@ +import { createContext, useContext, useEffect, useState } from 'react' +import { + createClient, + onAuthStateChange, + signInWithEmail, + signUpWithEmail, + signOut as supabaseSignOut, + type SignInInput, + type SignUpInput, +} from '@venue404/api-client' +import { authEndpoints, type AuthUser } from '@venue404/api-client' + +type AuthState = { + user: AuthUser | null + loading: boolean + signIn: (input: SignInInput) => Promise + signUp: (input: SignUpInput) => Promise + signOut: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + async function loadUser() { + try { + const client = createClient() + const authUser = await authEndpoints(client).me() + setUser(authUser) + } catch { + setUser(null) + } + } + + useEffect(() => { + const subscription = onAuthStateChange(async (event, session) => { + if (event === 'SIGNED_OUT') { + setUser(null) + setLoading(false) + return + } + + if ((event === 'INITIAL_SESSION' || event === 'SIGNED_IN') && session) { + await loadUser() + setLoading(false) + return + } + + // no session on initial load — user is not logged in + setLoading(false) + }) + + return () => subscription.unsubscribe() + }, []) + + async function signIn(input: SignInInput) { + await signInWithEmail(input) + // onAuthStateChange SIGNED_IN fires automatically and calls loadUser + } + + async function signUp(input: SignUpInput) { + await signUpWithEmail(input) + // onAuthStateChange SIGNED_IN fires automatically and calls loadUser + } + + async function signOut() { + await supabaseSignOut() + // onAuthStateChange SIGNED_OUT fires automatically and clears user + } + + return ( + + {children} + + ) +} + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used inside AuthProvider') + return ctx +} diff --git a/apps/user-web/src/main.tsx b/apps/user-web/src/main.tsx index fda61b388..9d474e208 100644 --- a/apps/user-web/src/main.tsx +++ b/apps/user-web/src/main.tsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { AuthProvider } from './lib/AuthContext' import './styles/index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/apps/user-web/src/pages/Login.tsx b/apps/user-web/src/pages/Login.tsx new file mode 100644 index 000000000..087bbe7eb --- /dev/null +++ b/apps/user-web/src/pages/Login.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' + +const OWNER_PORTAL_URL = import.meta.env.VITE_OWNER_PORTAL_URL ?? 'http://localhost:5174' +const ADMIN_PANEL_URL = import.meta.env.VITE_ADMIN_PANEL_URL ?? 'http://localhost:5175' + +function redirectByRole(roles: string[]) { + if (roles.includes('super_admin')) { + window.location.href = ADMIN_PANEL_URL + } else if (roles.includes('venue_owner')) { + window.location.href = OWNER_PORTAL_URL + } + // customer stays on user-web — handled by navigate below +} + +export default function Login() { + const { user, loading, signIn } = useAuth() + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + // if already logged in, redirect immediately + useEffect(() => { + if (!loading && user) { + if (user.roles.includes('super_admin') || user.roles.includes('venue_owner')) { + redirectByRole(user.roles) + } else { + navigate('/', { replace: true }) + } + } + }, [user, loading]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + setSubmitting(true) + + try { + await signIn({ email, password }) + // onAuthStateChange fires → loadUser → user state updates → useEffect above redirects + } catch (err: any) { + setError(err.message ?? 'Login failed') + } finally { + setSubmitting(false) + } + } + + if (loading) return null + + return ( +
+

Sign in

+
+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + {error &&

{error}

} + +
+
+ ) +} diff --git a/apps/user-web/src/routes.tsx b/apps/user-web/src/routes.tsx index 7d5491673..1f1eca401 100644 --- a/apps/user-web/src/routes.tsx +++ b/apps/user-web/src/routes.tsx @@ -1,10 +1,26 @@ import { createBrowserRouter } from 'react-router-dom' +import { ProtectedRoute } from './components/ProtectedRoute' import Home from './pages/Home' import VenueDetails from './pages/VenueDetails' import MyBookings from './pages/MyBookings' +import Login from './pages/Login' export const router = createBrowserRouter([ + // public routes { path: '/', element: }, { path: '/venues/:id', element: }, - { path: '/my-bookings', element: }, + { path: '/login', element: }, + + // protected routes — require any authenticated user + { + path: '/my-bookings', + element: ( + + + + ), + }, + + // 403 fallback + { path: '/403', element:
Access denied.
}, ]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bccc29827..732644176 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,10 @@ importers: version: 5.4.21 packages/api-client: + dependencies: + '@supabase/supabase-js': + specifier: ^2.45.0 + version: 2.107.0 devDependencies: tsx: specifier: ^4.0.0 @@ -752,6 +756,33 @@ packages: cpu: [x64] os: [win32] + '@supabase/auth-js@2.107.0': + resolution: {integrity: sha512-XA7x+WIeIvuC3GTZ2ey67QcBbGw4n+o5B7M+dMm9KT1lL3wX1B52DfEWW00WuPt/LnniJLLIn1WIm9YPtuxzKQ==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.107.0': + resolution: {integrity: sha512-iMtRUmEj1KOgQd/a3MR4hnBlPnZc62DW8+z8aPpnzbxWkexEZUVL2fSgvvp15gqFg1V55e2yMGqgK+yhSQxp5w==} + engines: {node: '>=20.0.0'} + + '@supabase/phoenix@0.4.2': + resolution: {integrity: sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==} + + '@supabase/postgrest-js@2.107.0': + resolution: {integrity: sha512-7ARs47/tyIjX7T0Ive20d4NY8zQYXsP5/P07jJWxffSIM2gpnSnGRnL/Fe15GPbdjsW2sTYeckHcyaoKbM6yWQ==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.107.0': + resolution: {integrity: sha512-cF2KYdR3JIn9YlWGeluY9S0G+otqTdL6hB8GzpatlEIY6fZudCcyFo6Dc3+X9tjeb+x9XcIyNAk9qhNAknjH1A==} + engines: {node: '>=20.0.0'} + + '@supabase/storage-js@2.107.0': + resolution: {integrity: sha512-/X8OOVwKBn8aVKuHAGOz2yLA0d2OauqhVuy4mNtN+o7wttHOgx1/j+pqOzlsjmhOHrYykF6AJNZhs3gKZzcMUw==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.107.0': + resolution: {integrity: sha512-ChKzdlWVweMUUhr0U79JhMmgm1haS/C5JquaiCDr70JaGARRtjjoY9rkIheXWybXxTSNzRiQs3Sk8IAg1HS3ZA==} + engines: {node: '>=20.0.0'} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1104,6 +1135,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1896,6 +1931,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true + '@supabase/auth-js@2.107.0': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.107.0': + dependencies: + tslib: 2.8.1 + + '@supabase/phoenix@0.4.2': {} + + '@supabase/postgrest-js@2.107.0': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.107.0': + dependencies: + '@supabase/phoenix': 0.4.2 + tslib: 2.8.1 + + '@supabase/storage-js@2.107.0': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.107.0': + dependencies: + '@supabase/auth-js': 2.107.0 + '@supabase/functions-js': 2.107.0 + '@supabase/postgrest-js': 2.107.0 + '@supabase/realtime-js': 2.107.0 + '@supabase/storage-js': 2.107.0 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -2330,6 +2397,8 @@ snapshots: has-flag@4.0.0: {} + iceberg-js@0.8.1: {} + ignore@5.3.2: {} import-fresh@3.3.1: From 10716328a6ed9922402d3f0e05b8b37af7beefac Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 01:47:35 +0100 Subject: [PATCH 022/243] fix: Change Auth algorithm HS256 to RS256 --- .../app/modules/auth/providers/supabase.py | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/apps/api/app/modules/auth/providers/supabase.py b/apps/api/app/modules/auth/providers/supabase.py index e6d892072..67d0903d5 100644 --- a/apps/api/app/modules/auth/providers/supabase.py +++ b/apps/api/app/modules/auth/providers/supabase.py @@ -1,27 +1,55 @@ +import json +import urllib.request from uuid import UUID + from jose import jwt, JWTError + from app.core.config import settings from app.core.exceptions import UnauthorizedError from app.modules.auth.providers.base import AuthProvider, ProviderUser class SupabaseAuthProvider(AuthProvider): + def __init__(self) -> None: + self._jwks_cache: list | None = None + + def _get_jwks(self) -> list: + if self._jwks_cache is None: + url = f"{settings.supabase_url}/auth/v1/.well-known/jwks.json" + with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310 + self._jwks_cache = json.loads(resp.read()).get("keys", []) + return self._jwks_cache + def verify_token(self, token: str) -> ProviderUser: try: - payload = jwt.decode( - token, - settings.supabase_jwt_secret, - algorithms=["HS256"], - audience="authenticated", - ) - except JWTError: - raise UnauthorizedError("Invalid or expired token") + header = jwt.get_unverified_header(token) + alg = header.get("alg", "HS256") + + if alg == "HS256": + payload = jwt.decode( + token, + settings.supabase_jwt_secret, + algorithms=["HS256"], + audience="authenticated", + ) + else: + # RS256 or other asymmetric alg — verify via Supabase JWKS + kid = header.get("kid") + keys = self._get_jwks() + key = next((k for k in keys if k.get("kid") == kid), None) or (keys[0] if keys else None) + if key is None: + raise UnauthorizedError("No matching JWKS key found") + payload = jwt.decode( + token, + key, + algorithms=[alg], + audience="authenticated", + ) + except JWTError as exc: + raise UnauthorizedError(f"JWT error: {exc}") sub = payload.get("sub") if not sub: raise UnauthorizedError("Token missing subject claim") - return ProviderUser( - id=UUID(sub), - email=payload.get("email"), - ) + return ProviderUser(id=UUID(sub), email=payload.get("email")) From f852ca9198ababbc31130d1a0d19aea8a042b53d Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 09:43:40 +0100 Subject: [PATCH 023/243] chore: add local /prds directory to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 134d48248..2f2b18b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ Thumbs.db prd.txt auth-prd.md .claude/ -claude.md \ No newline at end of file +claude.md +/prds \ No newline at end of file From 983bc0e6c6354d5b9cfe82de8f6e7ec9f777b7ec Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 10:27:52 +0100 Subject: [PATCH 024/243] refactor: move admin action schema from profile to admin --- apps/api/alembic/env.py | 1 + apps/api/app/modules/admin/models.py | 19 +++++++++++++++++++ apps/api/app/modules/admin/routes.py | 2 +- apps/api/app/modules/profile/models.py | 15 +-------------- 4 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 apps/api/app/modules/admin/models.py diff --git a/apps/api/alembic/env.py b/apps/api/alembic/env.py index b03b3de08..92dda9886 100644 --- a/apps/api/alembic/env.py +++ b/apps/api/alembic/env.py @@ -6,6 +6,7 @@ # import all models so Base.metadata is populated for autogenerate import app.modules.profile.models # noqa: F401 +import app.modules.admin.models # noqa: F401 config = context.config diff --git a/apps/api/app/modules/admin/models.py b/apps/api/app/modules/admin/models.py new file mode 100644 index 000000000..746427999 --- /dev/null +++ b/apps/api/app/modules/admin/models.py @@ -0,0 +1,19 @@ +import uuid +from datetime import datetime +from sqlalchemy import String, DateTime, Text, ForeignKey, JSON, func +from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.dialects.postgresql import UUID +from app.core.database import Base + + +class AdminAction(Base): + __tablename__ = "admin_actions" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + admin_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="RESTRICT"), nullable=False) + action_type: Mapped[str] = mapped_column(String, nullable=False) + target_type: Mapped[str] = mapped_column(String, nullable=False) + target_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + reason: Mapped[str | None] = mapped_column(Text, nullable=True) + action_metadata: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) diff --git a/apps/api/app/modules/admin/routes.py b/apps/api/app/modules/admin/routes.py index f0cb07ae1..1a831d075 100644 --- a/apps/api/app/modules/admin/routes.py +++ b/apps/api/app/modules/admin/routes.py @@ -7,5 +7,5 @@ @router.patch("/venues/{venue_id}/approve", status_code=204) -def approve_venue(venue_id: str, body: VenueApprovalRequest, user=Depends(require_role("admin"))): +def approve_venue(venue_id: str, body: VenueApprovalRequest, _=Depends(require_role("super_admin"))): service.approve_venue(venue_id, body) diff --git a/apps/api/app/modules/profile/models.py b/apps/api/app/modules/profile/models.py index 0e58ff49d..f2982c85f 100644 --- a/apps/api/app/modules/profile/models.py +++ b/apps/api/app/modules/profile/models.py @@ -1,7 +1,7 @@ import enum import uuid from datetime import datetime -from sqlalchemy import String, Enum, DateTime, Text, ForeignKey, JSON, UniqueConstraint, func +from sqlalchemy import String, Enum, DateTime, UniqueConstraint, func from sqlalchemy.orm import mapped_column, Mapped from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base @@ -46,16 +46,3 @@ class UserRoleAssignment(Base): nullable=False, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) - - -class AdminAction(Base): - __tablename__ = "admin_actions" - - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - admin_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="RESTRICT"), nullable=False) - action_type: Mapped[str] = mapped_column(String, nullable=False) - target_type: Mapped[str] = mapped_column(String, nullable=False) - target_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) - reason: Mapped[str | None] = mapped_column(Text, nullable=True) - action_metadata: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now()) From 98cd4778308145903c2e0e2fcad1f139353ba970 Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 10:38:58 +0100 Subject: [PATCH 025/243] feat: seed super admin user in Supabase on first run --- apps/api/.env.example | 4 ++ apps/api/app/core/config.py | 2 + apps/api/app/main.py | 11 +++- apps/api/app/modules/admin/service.py | 94 +++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 67daf53ac..b1976c6d5 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -16,3 +16,7 @@ SMTP_HOST= SMTP_PORT=587 SMTP_USER= SMTP_PASSWORD= + +# Super Admin +SUPER_ADMIN_EMAIL= +SUPER_ADMIN_PASSWORD= diff --git a/apps/api/app/core/config.py b/apps/api/app/core/config.py index 5e30571af..ed71301ae 100644 --- a/apps/api/app/core/config.py +++ b/apps/api/app/core/config.py @@ -12,6 +12,8 @@ class Settings(BaseSettings): smtp_port: int = 587 smtp_user: str = "" smtp_password: str = "" + super_admin_email: str = "" + super_admin_password: str = "" class Config: env_file = ".env" diff --git a/apps/api/app/main.py b/apps/api/app/main.py index b1d2b746b..7804e0c40 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -1,3 +1,4 @@ +from contextlib import asynccontextmanager from fastapi import FastAPI from app.core.middleware import register_middleware from app.modules.auth.routes import router as auth_router @@ -9,8 +10,16 @@ from app.modules.notification.routes import router as notification_router from app.modules.admin.routes import router as admin_router from app.modules.payment.routes import router as payment_router +from app.modules.admin.service import seed_super_admin -app = FastAPI(title="Venue404 API") + +@asynccontextmanager +async def lifespan(_: FastAPI): + seed_super_admin() + yield + + +app = FastAPI(title="Venue404 API", lifespan=lifespan) register_middleware(app) diff --git a/apps/api/app/modules/admin/service.py b/apps/api/app/modules/admin/service.py index acff3fba0..3ebb2c724 100644 --- a/apps/api/app/modules/admin/service.py +++ b/apps/api/app/modules/admin/service.py @@ -1,5 +1,99 @@ +import logging +from supabase import create_client +from sqlalchemy.orm import Session + +from app.core.config import settings +from app.core.database import SessionLocal from app.modules.admin.schemas import VenueApprovalRequest +from app.modules.profile.models import Profile, UserRoleAssignment, UserRole, ProfileStatus + +logger = logging.getLogger(__name__) def approve_venue(venue_id: str, body: VenueApprovalRequest) -> None: raise NotImplementedError + + +def seed_super_admin() -> None: + """ + Idempotent. Creates the super admin user in Supabase on first run. + Skipped entirely if SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD + are not set in the environment. + """ + email = settings.super_admin_email + password = settings.super_admin_password + + if not email or not password: + logger.info("SUPER_ADMIN_EMAIL/PASSWORD not set, skipping super admin seed") + return + + db: Session = SessionLocal() + try: + _seed(db, email, password) + finally: + db.close() + + +def _seed(db: Session, email: str, password: str) -> None: + supabase = create_client(settings.supabase_url, settings.supabase_service_role_key) + + # Step 1: resolve or create the Supabase auth user + auth_user_id = _resolve_supabase_user(supabase, email, password) + + # Step 2: ensure profile row exists + profile = db.query(Profile).filter(Profile.id == auth_user_id).first() + if not profile: + profile = Profile( + id=auth_user_id, + full_name="Super Admin", + status=ProfileStatus.active, + ) + db.add(profile) + db.flush() + logger.info("Created profile for super admin %s", email) + + # Step 3: ensure super_admin role row exists + role_exists = db.query(UserRoleAssignment).filter( + UserRoleAssignment.user_id == auth_user_id, + UserRoleAssignment.role == UserRole.super_admin, + ).first() + + if not role_exists: + db.add(UserRoleAssignment(user_id=auth_user_id, role=UserRole.super_admin)) + logger.info("Assigned super_admin role to %s", email) + + db.commit() + logger.info("Super admin seed complete for %s", email) + + +def _resolve_supabase_user(supabase, email: str, password: str): + """ + Returns the UUID of the Supabase auth user, creating them if they don't + exist. Uses admin API so no confirmation email is triggered. + """ + import uuid + + # Try listing users to find by email (admin API) + # Supabase admin.list_users() returns paginated results + page = 1 + per_page = 1000 + while True: + response = supabase.auth.admin.list_users(page=page, per_page=per_page) + users = response if isinstance(response, list) else getattr(response, 'users', []) + for u in users: + if u.email == email: + logger.info("Super admin already exists in Supabase: %s", email) + return uuid.UUID(str(u.id)) + if len(users) < per_page: + break + page += 1 + + # Not found — create via admin API (skips email confirmation) + result = supabase.auth.admin.create_user({ + "email": email, + "password": password, + "email_confirm": True, + }) + created = result.user if hasattr(result, 'user') else result + logger.info("Created Supabase auth user for super admin: %s", email) + return uuid.UUID(str(created.id)) From 36ed98e0684a8f4314f67b6456ea4fc5060bfc3d Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 11:44:47 +0100 Subject: [PATCH 026/243] feat: scaffold admin panel with static dashboard --- apps/admin-panel/package.json | 8 +- .../src/components/AdminLayout.tsx | 53 ++ .../src/components/ProtectedRoute.tsx | 3 +- apps/admin-panel/src/pages/ComingSoon.tsx | 22 + apps/admin-panel/src/pages/Dashboard.tsx | 194 +++++++- apps/admin-panel/src/pages/Forbidden.tsx | 36 ++ apps/admin-panel/src/pages/Login.tsx | 136 ++++-- apps/admin-panel/src/pages/LoginSuccess.tsx | 47 ++ apps/admin-panel/src/pages/NotFound.tsx | 29 ++ apps/admin-panel/src/routes.tsx | 85 +++- apps/admin-panel/src/styles/index.css | 12 +- apps/admin-panel/vite.config.ts | 3 +- pnpm-lock.yaml | 455 ++++++++++++++++-- tsconfig.base.json | 3 +- 14 files changed, 1002 insertions(+), 84 deletions(-) create mode 100644 apps/admin-panel/src/components/AdminLayout.tsx create mode 100644 apps/admin-panel/src/pages/ComingSoon.tsx create mode 100644 apps/admin-panel/src/pages/Forbidden.tsx create mode 100644 apps/admin-panel/src/pages/LoginSuccess.tsx create mode 100644 apps/admin-panel/src/pages/NotFound.tsx diff --git a/apps/admin-panel/package.json b/apps/admin-panel/package.json index c3183e646..2848953f3 100644 --- a/apps/admin-panel/package.json +++ b/apps/admin-panel/package.json @@ -10,11 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.3.0", "@venue404/api-client": "workspace:*", "@venue404/ui": "workspace:*", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.17.0", "react": "^18.3.0", "react-dom": "^18.3.0", - "react-router-dom": "^6.23.0" + "react-router-dom": "^6.23.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0" }, "devDependencies": { "@types/react": "^18.3.0", diff --git a/apps/admin-panel/src/components/AdminLayout.tsx b/apps/admin-panel/src/components/AdminLayout.tsx new file mode 100644 index 000000000..a1c91d0d9 --- /dev/null +++ b/apps/admin-panel/src/components/AdminLayout.tsx @@ -0,0 +1,53 @@ +import { useNavigate, useLocation } from 'react-router-dom' +import { useAuth } from '../lib/AuthContext' +import { AppShell, Logo, type NavItemConfig } from '@venue404/ui' +import { + LayoutDashboard, Building2, Users, UserCheck, + CalendarDays, ClipboardList, Settings, +} from 'lucide-react' + +const NAV: NavItemConfig[] = [ + { label: 'Dashboard', href: '/dashboard', icon: }, + { label: 'Venue Approvals', href: '/venues/pending', icon: }, + { label: 'Users', href: '/users', icon: }, + { label: 'Venue Owners', href: '/owners', icon: }, + { label: 'Bookings', href: '/bookings', icon: }, + { label: 'Audit Log', href: '/audit-log', icon: }, + { label: 'Settings', href: '/settings', icon: }, +] + +type AdminLayoutProps = { + pageTitle: string + pageSubtitle?: string + children: React.ReactNode +} + +export function AdminLayout({ pageTitle, pageSubtitle, children }: AdminLayoutProps) { + const { user, signOut } = useAuth() + const navigate = useNavigate() + const location = useLocation() + + async function handleSignOut() { + await signOut() + navigate('/login', { replace: true }) + } + + return ( + } + user={user ? { + name: user.profile.full_name ?? user.email ?? 'Admin', + email: user.email ?? '', + role: 'Super Admin', + } : undefined} + onSignOut={handleSignOut} + > + {children} + + ) +} diff --git a/apps/admin-panel/src/components/ProtectedRoute.tsx b/apps/admin-panel/src/components/ProtectedRoute.tsx index 015436f0d..5786e9787 100644 --- a/apps/admin-panel/src/components/ProtectedRoute.tsx +++ b/apps/admin-panel/src/components/ProtectedRoute.tsx @@ -1,10 +1,11 @@ import { Navigate } from 'react-router-dom' import { useAuth } from '../lib/AuthContext' +import { LoadingScreen } from '@venue404/ui' export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth() - if (loading) return null + if (loading) return if (!user) return diff --git a/apps/admin-panel/src/pages/ComingSoon.tsx b/apps/admin-panel/src/pages/ComingSoon.tsx new file mode 100644 index 000000000..bf0047adb --- /dev/null +++ b/apps/admin-panel/src/pages/ComingSoon.tsx @@ -0,0 +1,22 @@ +import { EmptyState } from '@venue404/ui' +import { Construction } from 'lucide-react' +import { AdminLayout } from '../components/AdminLayout' + +type ComingSoonProps = { + title: string + description: string +} + +export default function ComingSoon({ title, description }: ComingSoonProps) { + return ( + +
+ } + title={title} + description={description} + /> +
+
+ ) +} diff --git a/apps/admin-panel/src/pages/Dashboard.tsx b/apps/admin-panel/src/pages/Dashboard.tsx index 77a19ea53..e49ba71c3 100644 --- a/apps/admin-panel/src/pages/Dashboard.tsx +++ b/apps/admin-panel/src/pages/Dashboard.tsx @@ -1,3 +1,195 @@ +import { useAuth } from '../lib/AuthContext' +import { + MetricCard, ActivityItem, + SectionHeader, StatusBadge, EmptyState, + type DashboardMetric, +} from '@venue404/ui' +import { + Building2, UserCheck, + CalendarDays, ClipboardList, + CheckCircle2, XCircle, Clock, +} from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { AdminLayout } from '../components/AdminLayout' + +const METRICS: DashboardMetric[] = [ + { + label: 'Pending Approvals', + value: '—', + description: 'Venues awaiting review', + icon: , + accent: 'amber', + }, + { + label: 'Active Bookings', + value: '—', + description: 'Confirmed this month', + icon: , + accent: 'blue', + }, + { + label: 'Venue Owners', + value: '—', + description: 'Registered on platform', + icon: , + accent: 'emerald', + }, + { + label: 'Open Actions', + value: '—', + description: 'Pending admin review', + icon: , + accent: 'violet', + }, +] + +const MOCK_VENUES = [ + { id: '1', name: 'The Grand Ballroom', owner: 'Ravi Kumar', submitted: '2h ago' }, + { id: '2', name: 'Rooftop Events Space', owner: 'Priya Sharma', submitted: '5h ago' }, + { id: '3', name: 'Garden Pavilion', owner: 'Amit Patel', submitted: '1d ago' }, +] + +const MOCK_ACTIONS = [ + { id: '1', action: 'Venue approved', target: 'Sunset Terrace', ok: true, time: '3h ago' }, + { id: '2', action: 'User suspended', target: 'john@example.com', ok: false, time: '1d ago' }, + { id: '3', action: 'Venue rejected', target: 'Old Warehouse', ok: false, time: '2d ago' }, +] + +const today = new Date().toLocaleDateString('en-IN', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', +}) + export default function Dashboard() { - return
Admin Dashboard
+ const { user } = useAuth() + const navigate = useNavigate() + + const firstName = user?.profile.full_name?.split(' ')[0] ?? null + + return ( + + {/* Welcome */} +
+

+ {firstName ? `Good to see you, ${firstName}` : 'Welcome back'} +

+

+ Here is what needs your attention today. +

+
+ + {/* Metrics */} +
+ {METRICS.map((m, i) => ( +
+ +
+ ))} +
+ + {/* Content grid */} +
+ + {/* Pending venue approvals */} +
+
+ navigate('/venues/pending')} + className="press text-xs font-medium text-blue-600 transition-colors hover:text-blue-700" + > + View all + + } + /> +
+
+ {MOCK_VENUES.length > 0 ? ( +
    + {MOCK_VENUES.map((v) => ( +
  • + } + badge={} + /> +
  • + ))} +
+ ) : ( +
+ } + title="No pending venues" + description="All submissions have been reviewed." + /> +
+ )} +
+
+ + {/* Recent audit actions */} +
+
+ navigate('/audit-log')} + className="press text-xs font-medium text-blue-600 transition-colors hover:text-blue-700" + > + Full log + + } + /> +
+
    + {MOCK_ACTIONS.map((a) => ( +
  • + + : + } + /> +
  • + ))} +
+
+
+ + {/* Quick actions */} +
+ +
+ {[ + { label: 'Review pending venues', href: '/venues/pending', icon: }, + { label: 'View all owners', href: '/owners', icon: }, + { label: 'Open audit log', href: '/audit-log', icon: }, + { label: 'Active bookings', href: '/bookings', icon: }, + ].map((a) => ( + + ))} +
+
+
+ ) } diff --git a/apps/admin-panel/src/pages/Forbidden.tsx b/apps/admin-panel/src/pages/Forbidden.tsx new file mode 100644 index 000000000..9568aa967 --- /dev/null +++ b/apps/admin-panel/src/pages/Forbidden.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from 'react-router-dom' +import { ForbiddenState } from '@venue404/ui' +import { useAuth } from '../lib/AuthContext' + +export default function Forbidden() { + const navigate = useNavigate() + const { signOut } = useAuth() + + async function handleSignOut() { + await signOut() + navigate('/login', { replace: true }) + } + + return ( + + + + + } + /> + ) +} diff --git a/apps/admin-panel/src/pages/Login.tsx b/apps/admin-panel/src/pages/Login.tsx index 121a69b69..5ca89661d 100644 --- a/apps/admin-panel/src/pages/Login.tsx +++ b/apps/admin-panel/src/pages/Login.tsx @@ -1,6 +1,15 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useAuth } from '../lib/AuthContext' +import { AuthLayout, AuthCard, AuthStatusPanel, Logo } from '@venue404/ui' +import { ShieldCheck, Building2, Users, BookOpen, ClipboardList } from 'lucide-react' + +const FEATURES = [ + { label: 'Venue Approvals', icon: }, + { label: 'User Management', icon: }, + { label: 'Booking Monitor', icon: }, + { label: 'Audit Trail', icon: }, +] export default function Login() { const { user, loading, signIn } = useAuth() @@ -11,15 +20,8 @@ export default function Login() { const [submitting, setSubmitting] = useState(false) useEffect(() => { - if (!loading && user) { - if (user.roles.includes('super_admin')) { - navigate('/', { replace: true }) - } else { - // not an admin — show 403 - navigate('/403', { replace: true }) - } - } - }, [user, loading]) + if (!loading && user) navigate('/login/success', { replace: true }) + }, [user, loading, navigate]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -27,8 +29,8 @@ export default function Login() { setSubmitting(true) try { await signIn({ email, password }) - } catch (err: any) { - setError(err.message ?? 'Login failed') + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Sign in failed. Check your credentials.') } finally { setSubmitting(false) } @@ -37,28 +39,96 @@ export default function Login() { if (loading) return null return ( -
-

Admin Panel — Sign in

-
- setEmail(e.target.value)} - required - /> - setPassword(e.target.value)} - required + Not an admin? Contact your system administrator. + } + > +
+ +
+ + +
+ + setEmail(e.target.value)} + placeholder="you@venue404.com" + disabled={submitting} + className="block w-full rounded-lg border border-zinc-300 bg-white px-3.5 py-2.5 text-sm text-zinc-900 placeholder-zinc-400 shadow-sm outline-none transition-[border-color,box-shadow] duration-150 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15 disabled:opacity-50" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••••" + disabled={submitting} + className="block w-full rounded-lg border border-zinc-300 bg-white px-3.5 py-2.5 text-sm text-zinc-900 placeholder-zinc-400 shadow-sm outline-none transition-[border-color,box-shadow] duration-150 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15 disabled:opacity-50" + /> +
+ + {error && ( +
+ + {error} +
+ )} + + + + +
+
+ + } + right={ + - {error &&

{error}

} - - -
+ } + /> ) } diff --git a/apps/admin-panel/src/pages/LoginSuccess.tsx b/apps/admin-panel/src/pages/LoginSuccess.tsx new file mode 100644 index 000000000..5980c5c59 --- /dev/null +++ b/apps/admin-panel/src/pages/LoginSuccess.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { createClient, authEndpoints } from '@venue404/api-client' +import { LoadingScreen, ErrorState } from '@venue404/ui' + +export default function LoginSuccess() { + const navigate = useNavigate() + const [error, setError] = useState(null) + + useEffect(() => { + async function verify() { + try { + const client = createClient() + const user = await authEndpoints(client).me() + + if (user.roles.includes('super_admin')) { + navigate('/dashboard', { replace: true }) + } else { + navigate('/403', { replace: true }) + } + } catch { + setError('Session verification failed. Please sign in again.') + } + } + + verify() + }, [navigate]) + + if (error) { + return ( + navigate('/login', { replace: true })} + className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" + > + Back to Login + + } + /> + ) + } + + return +} diff --git a/apps/admin-panel/src/pages/NotFound.tsx b/apps/admin-panel/src/pages/NotFound.tsx new file mode 100644 index 000000000..cdeb971fb --- /dev/null +++ b/apps/admin-panel/src/pages/NotFound.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from 'react-router-dom' +import { NotFoundState } from '@venue404/ui' + +export default function NotFound() { + const navigate = useNavigate() + + return ( + + + + + } + /> + ) +} diff --git a/apps/admin-panel/src/routes.tsx b/apps/admin-panel/src/routes.tsx index 397732dbf..3e910ad39 100644 --- a/apps/admin-panel/src/routes.tsx +++ b/apps/admin-panel/src/routes.tsx @@ -1,24 +1,93 @@ -import { createBrowserRouter } from 'react-router-dom' +import { createBrowserRouter, Navigate } from 'react-router-dom' import { ProtectedRoute } from './components/ProtectedRoute' import Dashboard from './pages/Dashboard' -import VenueApprovals from './pages/VenueApprovals' -import Users from './pages/Users' import Login from './pages/Login' +import LoginSuccess from './pages/LoginSuccess' +import Forbidden from './pages/Forbidden' +import NotFound from './pages/NotFound' +import ComingSoon from './pages/ComingSoon' export const router = createBrowserRouter([ + // Auth { path: '/login', element: }, - { path: '/403', element:
Access denied. Super admin only.
}, + { path: '/login/success', element: }, + { path: '/403', element: }, + // Root → dashboard + { path: '/', element: }, + + // Protected routes { - path: '/', + path: '/dashboard', element: , }, { - path: '/approvals', - element: , + path: '/venues/pending', + element: ( + + + + ), }, { path: '/users', - element: , + element: ( + + + + ), + }, + { + path: '/owners', + element: ( + + + + ), + }, + { + path: '/bookings', + element: ( + + + + ), }, + { + path: '/audit-log', + element: ( + + + + ), + }, + { + path: '/settings', + element: ( + + + + ), + }, + + // Catch-all → 404 + { path: '*', element: }, ]) diff --git a/apps/admin-panel/src/styles/index.css b/apps/admin-panel/src/styles/index.css index 09a262f28..b80916285 100644 --- a/apps/admin-panel/src/styles/index.css +++ b/apps/admin-panel/src/styles/index.css @@ -1,9 +1,3 @@ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: sans-serif; -} +@import "tailwindcss"; +@import "@venue404/ui/theme.css"; +@source "../../../../packages/ui/src"; diff --git a/apps/admin-panel/vite.config.ts b/apps/admin-panel/vite.config.ts index d635e361d..2e9584df7 100644 --- a/apps/admin-panel/vite.config.ts +++ b/apps/admin-panel/vite.config.ts @@ -1,8 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' export default defineConfig({ - plugins: [react()], + plugins: [tailwindcss(), react()], server: { port: 5175, proxy: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 732644176..c3b8bc317 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^7.0.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^7.0.0 - version: 7.18.0(eslint@9.39.4)(typescript@5.9.3) + version: 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) concurrently: specifier: ^10.0.0 version: 10.0.0 eslint: specifier: ^9.0.0 - version: 9.39.4 + version: 9.39.4(jiti@2.7.0) prettier: specifier: ^3.0.0 version: 3.8.3 @@ -29,12 +29,24 @@ importers: apps/admin-panel: dependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@5.4.21(lightningcss@1.32.0)) '@venue404/api-client': specifier: workspace:* version: link:../../packages/api-client '@venue404/ui': specifier: workspace:* version: link:../../packages/ui + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@18.3.1) react: specifier: ^18.3.0 version: 18.3.1 @@ -44,6 +56,12 @@ importers: react-router-dom: specifier: ^6.23.0 version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 devDependencies: '@types/react': specifier: ^18.3.0 @@ -53,10 +71,10 @@ importers: version: 18.3.7(@types/react@18.3.29) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@5.4.21) + version: 4.7.0(vite@5.4.21(lightningcss@1.32.0)) vite: specifier: ^5.3.0 - version: 5.4.21 + version: 5.4.21(lightningcss@1.32.0) apps/owner-portal: dependencies: @@ -84,10 +102,10 @@ importers: version: 18.3.7(@types/react@18.3.29) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@5.4.21) + version: 4.7.0(vite@5.4.21(lightningcss@1.32.0)) vite: specifier: ^5.3.0 - version: 5.4.21 + version: 5.4.21(lightningcss@1.32.0) apps/user-web: dependencies: @@ -115,10 +133,10 @@ importers: version: 18.3.7(@types/react@18.3.29) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@5.4.21) + version: 4.7.0(vite@5.4.21(lightningcss@1.32.0)) vite: specifier: ^5.3.0 - version: 5.4.21 + version: 5.4.21(lightningcss@1.32.0) packages/api-client: dependencies: @@ -135,13 +153,28 @@ importers: packages/ui: dependencies: + '@fontsource-variable/geist': + specifier: ^5.2.9 + version: 5.2.9 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 react: specifier: ^18.3.0 version: 18.3.1 + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 devDependencies: '@types/react': specifier: ^18.3.0 version: 18.3.29 + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@18.3.1) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -563,6 +596,9 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource-variable/geist@5.2.9': + resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -783,6 +819,100 @@ packages: resolution: {integrity: sha512-ChKzdlWVweMUUhr0U79JhMmgm1haS/C5JquaiCDr70JaGARRtjjoY9rkIheXWybXxTSNzRiQs3Sk8IAg1HS3ZA==} engines: {node: '>=20.0.0'} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -949,10 +1079,17 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -990,6 +1127,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1000,6 +1141,10 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1128,6 +1273,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1166,6 +1314,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1199,6 +1351,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1213,6 +1439,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1395,6 +1629,16 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1759,9 +2003,9 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1805,6 +2049,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fontsource-variable/geist@5.2.9': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -1963,6 +2209,74 @@ snapshots: '@supabase/realtime-js': 2.107.0 '@supabase/storage-js': 2.107.0 + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@5.4.21(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 5.4.21(lightningcss@1.32.0) + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -2001,15 +2315,15 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -2019,14 +2333,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -2037,12 +2351,12 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) ts-api-utils: 1.4.3(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 @@ -2066,13 +2380,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) transitivePeerDependencies: - supports-color - typescript @@ -2082,7 +2396,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 - '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + '@vitejs/plugin-react@4.7.0(vite@5.4.21(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -2090,7 +2404,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21 + vite: 5.4.21(lightningcss@1.32.0) transitivePeerDependencies: - supports-color @@ -2155,12 +2469,18 @@ snapshots: chalk@5.6.2: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2194,6 +2514,8 @@ snapshots: deep-is@0.1.4: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2202,6 +2524,11 @@ snapshots: emoji-regex@10.6.0: {} + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2270,9 +2597,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.4: + eslint@9.39.4(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -2306,6 +2633,8 @@ snapshots: minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -2393,6 +2722,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-flag@4.0.0: {} @@ -2418,6 +2749,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.7.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -2443,6 +2776,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2457,6 +2839,14 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@1.17.0(react@18.3.1): + dependencies: + react: 18.3.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} micromatch@4.0.8: @@ -2630,6 +3020,12 @@ snapshots: dependencies: has-flag: 4.0.0 + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -2664,13 +3060,14 @@ snapshots: dependencies: punycode: 2.3.1 - vite@5.4.21: + vite@5.4.21(lightningcss@1.32.0): dependencies: esbuild: 0.21.5 postcss: 8.5.15 rollup: 4.60.4 optionalDependencies: fsevents: 2.3.3 + lightningcss: 1.32.0 which@2.0.2: dependencies: diff --git a/tsconfig.base.json b/tsconfig.base.json index 8eecaed09..6a6413be4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,6 +10,7 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "resolveJsonModule": true, - "isolatedModules": true + "isolatedModules": true, + "noEmit": true } } From 7c308de371512a521be366e261971d656a916dd9 Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 11:46:14 +0100 Subject: [PATCH 027/243] fix: admin seed supabase bug fix --- apps/api/app/core/security.py | 24 ----------- apps/api/app/modules/admin/service.py | 58 ++++++++++++++++---------- apps/api/app/modules/profile/models.py | 2 +- 3 files changed, 36 insertions(+), 48 deletions(-) delete mode 100644 apps/api/app/core/security.py diff --git a/apps/api/app/core/security.py b/apps/api/app/core/security.py deleted file mode 100644 index 5602c1e2e..000000000 --- a/apps/api/app/core/security.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import datetime, timedelta -from jose import jwt -from passlib.context import CryptContext -from app.core.config import settings - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -def hash_password(password: str) -> str: - return pwd_context.hash(password) - - -def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) - - -def create_access_token(data: dict) -> str: - payload = data.copy() - payload["exp"] = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) - return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) - - -def decode_token(token: str) -> dict: - return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) diff --git a/apps/api/app/modules/admin/service.py b/apps/api/app/modules/admin/service.py index 3ebb2c724..ceef73b47 100644 --- a/apps/api/app/modules/admin/service.py +++ b/apps/api/app/modules/admin/service.py @@ -1,5 +1,8 @@ +import json import logging -from supabase import create_client +import urllib.request +import urllib.error +import uuid from sqlalchemy.orm import Session from app.core.config import settings @@ -16,7 +19,7 @@ def approve_venue(venue_id: str, body: VenueApprovalRequest) -> None: def seed_super_admin() -> None: """ - Idempotent. Creates the super admin user in Supabase on first run. + Idempotent. Creates the super admin user in Supabase on first run. Skipped entirely if SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD are not set in the environment. """ @@ -35,12 +38,8 @@ def seed_super_admin() -> None: def _seed(db: Session, email: str, password: str) -> None: - supabase = create_client(settings.supabase_url, settings.supabase_service_role_key) + auth_user_id = _resolve_supabase_user(email, password) - # Step 1: resolve or create the Supabase auth user - auth_user_id = _resolve_supabase_user(supabase, email, password) - - # Step 2: ensure profile row exists profile = db.query(Profile).filter(Profile.id == auth_user_id).first() if not profile: profile = Profile( @@ -52,7 +51,6 @@ def _seed(db: Session, email: str, password: str) -> None: db.flush() logger.info("Created profile for super admin %s", email) - # Step 3: ensure super_admin role row exists role_exists = db.query(UserRoleAssignment).filter( UserRoleAssignment.user_id == auth_user_id, UserRoleAssignment.role == UserRole.super_admin, @@ -66,34 +64,48 @@ def _seed(db: Session, email: str, password: str) -> None: logger.info("Super admin seed complete for %s", email) -def _resolve_supabase_user(supabase, email: str, password: str): +def _supabase_admin_request(method: str, path: str, body: dict | None = None) -> dict: + """Makes a request to the Supabase Auth Admin REST API.""" + url = f"{settings.supabase_url}/auth/v1/admin/{path}" + data = json.dumps(body).encode() if body else None + req = urllib.request.Request( + url, + data=data, + method=method, + headers={ + "Authorization": f"Bearer {settings.supabase_service_role_key}", + "apikey": settings.supabase_service_role_key, + "Content-Type": "application/json", + }, + ) + with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 + return json.loads(resp.read()) + + +def _resolve_supabase_user(email: str, password: str) -> uuid.UUID: """ - Returns the UUID of the Supabase auth user, creating them if they don't - exist. Uses admin API so no confirmation email is triggered. + Returns the UUID of the Supabase auth user, creating them if they + don't exist. Uses the Admin API so no confirmation email is triggered. """ - import uuid - - # Try listing users to find by email (admin API) - # Supabase admin.list_users() returns paginated results + # List users and search by email page = 1 per_page = 1000 while True: - response = supabase.auth.admin.list_users(page=page, per_page=per_page) - users = response if isinstance(response, list) else getattr(response, 'users', []) + data = _supabase_admin_request("GET", f"users?page={page}&per_page={per_page}") + users = data.get("users", []) for u in users: - if u.email == email: + if u.get("email") == email: logger.info("Super admin already exists in Supabase: %s", email) - return uuid.UUID(str(u.id)) + return uuid.UUID(u["id"]) if len(users) < per_page: break page += 1 - # Not found — create via admin API (skips email confirmation) - result = supabase.auth.admin.create_user({ + # Not found — create via Admin API (email_confirm skips verification email) + result = _supabase_admin_request("POST", "users", { "email": email, "password": password, "email_confirm": True, }) - created = result.user if hasattr(result, 'user') else result logger.info("Created Supabase auth user for super admin: %s", email) - return uuid.UUID(str(created.id)) + return uuid.UUID(result["id"]) diff --git a/apps/api/app/modules/profile/models.py b/apps/api/app/modules/profile/models.py index f2982c85f..e52e33dea 100644 --- a/apps/api/app/modules/profile/models.py +++ b/apps/api/app/modules/profile/models.py @@ -1,7 +1,7 @@ import enum import uuid from datetime import datetime -from sqlalchemy import String, Enum, DateTime, UniqueConstraint, func +from sqlalchemy import String, Enum, DateTime, UniqueConstraint, func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base From 5c7ad70ae0a43c576e1a28dfa1ffb54d6eba400e Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 11:46:45 +0100 Subject: [PATCH 028/243] fix: admin app title change --- apps/admin-panel/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin-panel/index.html b/apps/admin-panel/index.html index 0d0dd2e03..9ecd5ea96 100644 --- a/apps/admin-panel/index.html +++ b/apps/admin-panel/index.html @@ -4,7 +4,7 @@ - Venue404 — Admin + Venue404 | Admin From 17be216cfd995f160376434e946b5b95dd11ba61 Mon Sep 17 00:00:00 2001 From: sankar Date: Wed, 3 Jun 2026 11:48:35 +0100 Subject: [PATCH 029/243] feat: integrated shared packages with base components and layout wrappers --- packages/api-client/src/supabase.ts | 6 ++ packages/ui/package.json | 12 +++ packages/ui/src/DatePicker.tsx | 2 - .../ui/src/components/app-shell/AppShell.tsx | 62 ++++++++++++ .../src/components/app-shell/AppSidebar.tsx | 95 ++++++++++++++++++ .../ui/src/components/app-shell/AppTopbar.tsx | 45 +++++++++ .../ui/src/components/app-shell/NavItem.tsx | 54 +++++++++++ .../ui/src/components/app-shell/UserMenu.tsx | 82 ++++++++++++++++ packages/ui/src/components/auth/AuthCard.tsx | 26 +++++ .../ui/src/components/auth/AuthLayout.tsx | 22 +++++ .../src/components/auth/AuthStatusPanel.tsx | 68 +++++++++++++ packages/ui/src/components/brand/Logo.tsx | 55 +++++++++++ .../src/components/dashboard/ActivityItem.tsx | 34 +++++++ .../src/components/dashboard/MetricCard.tsx | 86 ++++++++++++++++ .../components/dashboard/SectionHeader.tsx | 20 ++++ .../src/components/dashboard/StatusBadge.tsx | 34 +++++++ .../src/components/empty-state/EmptyState.tsx | 33 +++++++ .../ui/src/components/feedback/ErrorState.tsx | 42 ++++++++ .../components/feedback/ForbiddenState.tsx | 52 ++++++++++ .../src/components/feedback/LoadingScreen.tsx | 25 +++++ .../src/components/feedback/NotFoundState.tsx | 87 +++++++++++++++++ packages/ui/src/index.ts | 39 ++++++++ packages/ui/src/lib/utils.ts | 6 ++ packages/ui/src/styles/theme.css | 97 +++++++++++++++++++ packages/ui/src/tokens.ts | 47 +++++++++ 25 files changed, 1129 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/components/app-shell/AppShell.tsx create mode 100644 packages/ui/src/components/app-shell/AppSidebar.tsx create mode 100644 packages/ui/src/components/app-shell/AppTopbar.tsx create mode 100644 packages/ui/src/components/app-shell/NavItem.tsx create mode 100644 packages/ui/src/components/app-shell/UserMenu.tsx create mode 100644 packages/ui/src/components/auth/AuthCard.tsx create mode 100644 packages/ui/src/components/auth/AuthLayout.tsx create mode 100644 packages/ui/src/components/auth/AuthStatusPanel.tsx create mode 100644 packages/ui/src/components/brand/Logo.tsx create mode 100644 packages/ui/src/components/dashboard/ActivityItem.tsx create mode 100644 packages/ui/src/components/dashboard/MetricCard.tsx create mode 100644 packages/ui/src/components/dashboard/SectionHeader.tsx create mode 100644 packages/ui/src/components/dashboard/StatusBadge.tsx create mode 100644 packages/ui/src/components/empty-state/EmptyState.tsx create mode 100644 packages/ui/src/components/feedback/ErrorState.tsx create mode 100644 packages/ui/src/components/feedback/ForbiddenState.tsx create mode 100644 packages/ui/src/components/feedback/LoadingScreen.tsx create mode 100644 packages/ui/src/components/feedback/NotFoundState.tsx create mode 100644 packages/ui/src/lib/utils.ts create mode 100644 packages/ui/src/styles/theme.css create mode 100644 packages/ui/src/tokens.ts diff --git a/packages/api-client/src/supabase.ts b/packages/api-client/src/supabase.ts index 79c4e2c5a..b6641c713 100644 --- a/packages/api-client/src/supabase.ts +++ b/packages/api-client/src/supabase.ts @@ -1,5 +1,11 @@ import { createClient } from '@supabase/supabase-js' +declare global { + interface ImportMeta { + readonly env: Record + } +} + const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string diff --git a/packages/ui/package.json b/packages/ui/package.json index b19fc7830..640500681 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,14 +3,26 @@ "version": "0.0.0", "type": "module", "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./theme.css": "./src/styles/theme.css" + }, "scripts": { "lint": "eslint src" }, "devDependencies": { "@types/react": "^18.3.0", + "lucide-react": "^1.17.0", "typescript": "^5.4.0" }, "peerDependencies": { + "lucide-react": "^1.17.0", "react": "^18.3.0" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "tailwind-merge": "^3.6.0" } } \ No newline at end of file diff --git a/packages/ui/src/DatePicker.tsx b/packages/ui/src/DatePicker.tsx index 225ee2f23..e17d67f44 100644 --- a/packages/ui/src/DatePicker.tsx +++ b/packages/ui/src/DatePicker.tsx @@ -1,5 +1,3 @@ -import React from 'react' - interface DatePickerProps { value?: string onChange?: (value: string) => void diff --git a/packages/ui/src/components/app-shell/AppShell.tsx b/packages/ui/src/components/app-shell/AppShell.tsx new file mode 100644 index 000000000..c7666039a --- /dev/null +++ b/packages/ui/src/components/app-shell/AppShell.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react' +import { cn } from '../../lib/utils' +import { AppSidebar } from './AppSidebar' +import { AppTopbar } from './AppTopbar' +import type { NavItemConfig } from './NavItem' + +type AppShellProps = { + navItems: NavItemConfig[] + activePath: string + onNavigate: (href: string) => void + pageTitle: string + pageSubtitle?: string + brand?: React.ReactNode + user?: { name: string; email: string; role?: string } + onSignOut?: () => void + topbarActions?: React.ReactNode + children: React.ReactNode + className?: string +} + +export function AppShell({ + navItems, + activePath, + onNavigate, + pageTitle, + pageSubtitle, + brand, + user, + onSignOut, + topbarActions, + children, + className, +}: AppShellProps) { + const [mobileOpen, setMobileOpen] = useState(false) + + return ( +
+ setMobileOpen(false)} + /> + +
+ setMobileOpen(true)} + actions={topbarActions} + /> +
+ {children} +
+
+
+ ) +} diff --git a/packages/ui/src/components/app-shell/AppSidebar.tsx b/packages/ui/src/components/app-shell/AppSidebar.tsx new file mode 100644 index 000000000..e990a9f85 --- /dev/null +++ b/packages/ui/src/components/app-shell/AppSidebar.tsx @@ -0,0 +1,95 @@ +import { cn } from '../../lib/utils' +import { NavItem, type NavItemConfig } from './NavItem' +import { UserMenu } from './UserMenu' + +type AppSidebarProps = { + navItems: NavItemConfig[] + activePath: string + onNavigate: (href: string) => void + brand?: React.ReactNode + user?: { name: string; email: string; role?: string } + onSignOut?: () => void + className?: string + mobileOpen?: boolean + onMobileClose?: () => void +} + +export function AppSidebar({ + navItems, + activePath, + onNavigate, + brand, + user, + onSignOut, + className, + mobileOpen, + onMobileClose, +}: AppSidebarProps) { + const sidebar = ( + + ) + + return ( + <> + {/* Desktop: fixed sidebar */} +
{sidebar}
+ + {/* Mobile: drawer */} + {mobileOpen && ( + <> +
diff --git a/apps/admin-panel/src/pages/VenueApprovals.tsx b/apps/admin-panel/src/pages/VenueApprovals.tsx index 5a0aa5ba9..aaf0fc365 100644 --- a/apps/admin-panel/src/pages/VenueApprovals.tsx +++ b/apps/admin-panel/src/pages/VenueApprovals.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { Building2, MapPin, Users, Clock, IndianRupee, CheckCircle2, XCircle, ShieldOff, ShieldCheck, @@ -195,7 +195,7 @@ export default function VenueApprovals() { { label: 'Pending Approval', value: stats?.pending_approval, accent: 'amber' as const, icon: }, { label: 'Approved', value: stats?.approved, accent: 'emerald' as const, icon: }, { label: 'Suspended', value: stats?.suspended, accent: 'rose' as const, icon: }, - { label: 'Total Venues', value: stats?.total, accent: 'blue' as const, icon: }, + { label: 'Total Venues', value: stats?.total, accent: 'brand' as const, icon: }, ].map((m, i) => (
@@ -238,7 +238,7 @@ export default function VenueApprovals() { {count !== undefined && count > 0 && ( {count} diff --git a/apps/admin-panel/src/pages/VenueOwners.tsx b/apps/admin-panel/src/pages/VenueOwners.tsx index 703894421..51f84a80a 100644 --- a/apps/admin-panel/src/pages/VenueOwners.tsx +++ b/apps/admin-panel/src/pages/VenueOwners.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { ShieldOff, ShieldCheck, UserCheck, Search, Clock, CheckCircle2, XCircle, Users as UsersIcon, @@ -44,7 +44,7 @@ function initials(u: AdminUserSummary): string { } const AVATAR_COLORS = [ - 'bg-blue-100 text-blue-700', + 'bg-brand-light-strong text-brand', 'bg-violet-100 text-violet-700', 'bg-amber-100 text-amber-700', 'bg-emerald-100 text-emerald-700', @@ -199,7 +199,7 @@ export default function VenueOwners() { value={stats ? String(stats.total) : '—'} description="All registered venue owners" icon={} - accent="blue" + accent="brand" />
@@ -258,7 +258,7 @@ export default function VenueOwners() { className={[ 'relative px-3.5 py-2.5 text-sm font-medium transition-colors focus:outline-none', isActive - ? 'text-zinc-900 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:rounded-full after:bg-blue-600' + ? 'text-zinc-900 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:rounded-full after:bg-brand' : 'text-zinc-400 hover:text-zinc-600', ].join(' ')} > diff --git a/apps/owner-portal/src/pages/Login.tsx b/apps/owner-portal/src/pages/Login.tsx index ec29ff6cd..d2aaa5287 100644 --- a/apps/owner-portal/src/pages/Login.tsx +++ b/apps/owner-portal/src/pages/Login.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect } from 'react' import { useNavigate, Link } from 'react-router-dom' import { useAuth } from '../lib/AuthContext' import { AuthLayout, AuthCard, AuthStatusPanel, Logo } from '@venue404/ui' @@ -46,7 +46,7 @@ export default function Login() { footer={ <> New here?{' '} - + Register as a venue owner @@ -101,7 +101,7 @@ export default function Login() { diff --git a/apps/owner-portal/src/pages/Register.tsx b/apps/owner-portal/src/pages/Register.tsx index 6a468e076..4d1e93a5b 100644 --- a/apps/owner-portal/src/pages/Register.tsx +++ b/apps/owner-portal/src/pages/Register.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState } from 'react' import { useNavigate, Link } from 'react-router-dom' import { useAuth } from '../lib/AuthContext' import { createClient, authEndpoints } from '@venue404/api-client' @@ -46,7 +46,7 @@ export default function Register() { footer={ <> Already have an account?{' '} - + Sign in @@ -138,7 +138,7 @@ export default function Register() {
@@ -139,7 +139,7 @@ export default function Home() { {CATEGORIES.map((c) => ( + } + /> + ) + } + + return +} diff --git a/apps/user-web/src/routes.tsx b/apps/user-web/src/routes.tsx index 83bae7858..c9acbe26d 100644 --- a/apps/user-web/src/routes.tsx +++ b/apps/user-web/src/routes.tsx @@ -4,6 +4,7 @@ import Home from './pages/Home' import VenueDetails from './pages/VenueDetails' import MyBookings from './pages/MyBookings' import Login from './pages/Login' +import LoginSuccess from './pages/LoginSuccess' import Register from './pages/Register' import RegisterSuccess from './pages/RegisterSuccess' @@ -12,6 +13,7 @@ export const router = createBrowserRouter([ { path: '/', element: }, { path: '/venues/:id', element: }, { path: '/login', element: }, + { path: '/login/success', element: }, { path: '/register', element: }, { path: '/register/success', element: }, From 6521c288e63fd28a7245fff167ff1991e376b4b7 Mon Sep 17 00:00:00 2001 From: sankar Date: Tue, 9 Jun 2026 23:21:11 +0100 Subject: [PATCH 077/243] fix: login/registration style fix for owner and user portals --- .../src/components/OwnerFlowPanel.tsx | 184 ++++++++++++++++++ apps/owner-portal/src/pages/Login.tsx | 20 +- apps/owner-portal/src/pages/Register.tsx | 20 +- .../src/components/VenueFlowPanel.tsx | 181 +++++++++++++++++ apps/user-web/src/pages/Login.tsx | 19 +- apps/user-web/src/pages/Register.tsx | 19 +- 6 files changed, 377 insertions(+), 66 deletions(-) create mode 100644 apps/owner-portal/src/components/OwnerFlowPanel.tsx create mode 100644 apps/user-web/src/components/VenueFlowPanel.tsx diff --git a/apps/owner-portal/src/components/OwnerFlowPanel.tsx b/apps/owner-portal/src/components/OwnerFlowPanel.tsx new file mode 100644 index 000000000..10e164ec9 --- /dev/null +++ b/apps/owner-portal/src/components/OwnerFlowPanel.tsx @@ -0,0 +1,184 @@ +export function OwnerFlowPanel() { + return ( +
+ {/* Dot-grid */} + } - right={ - - } + right={} /> ) } diff --git a/apps/user-web/src/components/VenueFlowPanel.tsx b/apps/user-web/src/components/VenueFlowPanel.tsx new file mode 100644 index 000000000..2d31bdd1d --- /dev/null +++ b/apps/user-web/src/components/VenueFlowPanel.tsx @@ -0,0 +1,181 @@ +export function VenueFlowPanel() { + return ( +
+ {/* Dot-grid */} +