diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..7d208bae52
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+venv/
+__pycache__/
+.env
+*.pyc
+dist/
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/.gitignore b/bookmyvenue/apps/Frontend/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/bookmyvenue/apps/Frontend/README.md b/bookmyvenue/apps/Frontend/README.md
new file mode 100644
index 0000000000..a36934d874
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/bookmyvenue/apps/Frontend/eslint.config.js b/bookmyvenue/apps/Frontend/eslint.config.js
new file mode 100644
index 0000000000..ea36dd3dc4
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/eslint.config.js
@@ -0,0 +1,21 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ parserOptions: { ecmaFeatures: { jsx: true } },
+ },
+ },
+])
diff --git a/bookmyvenue/apps/Frontend/index.html b/bookmyvenue/apps/Frontend/index.html
new file mode 100644
index 0000000000..f94d687d35
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/bookmyvenue/apps/Frontend/package-lock.json b/bookmyvenue/apps/Frontend/package-lock.json
new file mode 100644
index 0000000000..74a3b32d9b
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/package-lock.json
@@ -0,0 +1,3115 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.2",
+ "axios": "^1.17.0",
+ "jwt-decode": "^4.0.0",
+ "lucide-react": "^1.24.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-router-dom": "^7.17.0",
+ "tailwindcss": "^4.3.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "vite": "^8.0.12"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.132.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
+ "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
+ "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
+ "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
+ "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
+ "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
+ "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
+ "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
+ "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
+ "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
+ "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
+ "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
+ "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
+ "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
+ "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
+ "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
+ "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz",
+ "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.2",
+ "@tailwindcss/oxide": "4.3.2",
+ "tailwindcss": "4.3.2"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.15",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
+ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
+ "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.363",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.363.tgz",
+ "integrity": "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
+ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
+ "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
+ "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
+ "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
+ "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.17.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
+ "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.132.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.2",
+ "@rolldown/binding-darwin-arm64": "1.0.2",
+ "@rolldown/binding-darwin-x64": "1.0.2",
+ "@rolldown/binding-freebsd-x64": "1.0.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.2",
+ "@rolldown/binding-linux-arm64-musl": "1.0.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-musl": "1.0.2",
+ "@rolldown/binding-openharmony-arm64": "1.0.2",
+ "@rolldown/binding-wasm32-wasi": "1.0.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.2",
+ "@rolldown/binding-win32-x64-msvc": "1.0.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
+ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.2",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/bookmyvenue/apps/Frontend/package.json b/bookmyvenue/apps/Frontend/package.json
new file mode 100644
index 0000000000..e58941be00
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.2",
+ "axios": "^1.17.0",
+ "jwt-decode": "^4.0.0",
+ "lucide-react": "^1.24.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-router-dom": "^7.17.0",
+ "tailwindcss": "^4.3.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "vite": "^8.0.12"
+ }
+}
diff --git a/bookmyvenue/apps/Frontend/public/favicon.svg b/bookmyvenue/apps/Frontend/public/favicon.svg
new file mode 100644
index 0000000000..6893eb1323
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/public/icons.svg b/bookmyvenue/apps/Frontend/public/icons.svg
new file mode 100644
index 0000000000..e9522193d9
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bookmyvenue/apps/Frontend/src/App.jsx b/bookmyvenue/apps/Frontend/src/App.jsx
new file mode 100644
index 0000000000..61a8f4a753
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/App.jsx
@@ -0,0 +1,118 @@
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+
+import Home from "./pages/Home";
+import Login from "./pages/Login";
+import Register from "./pages/Register";
+import VenueDetail from "./pages/venue/VenueDetail";
+
+import BookerDashboard from "./pages/dashboard/BookerDashboard";
+
+import AdminDashboard from "./pages/admin/AdminDashboard";
+
+import OwnerDashboard from "./pages/owner/OwnerDashboard";
+import CreateVenue from "./pages/owner/CreateVenue";
+import EditVenue from "./pages/owner/EditVenue";
+import ManageVenue from "./pages/owner/ManageVenue";
+import ManageAvailability from "./pages/owner/ManageAvailability";
+import OwnerVenueBookings from "./pages/owner/OwnerVenueBookings";
+
+import ProtectedRoute from "./components/ProtectedRoute";
+import { AuthProvider } from "./context/AuthContext";
+
+function App() {
+ return (
+
+
+
+ {/* Public Routes */}
+
+ } />
+ } />
+
+ } />
+ } />
+
+ {/* Booker */}
+
+
+
+
+ }
+ />
+
+ {/* Owner */}
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ {/* Admin */}
+
+
+
+
+ }
+ />
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/admin.js b/bookmyvenue/apps/Frontend/src/api/admin.js
new file mode 100644
index 0000000000..259a9d8106
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/admin.js
@@ -0,0 +1,80 @@
+import axios from "./axios";
+
+export async function getAdminStats(token) {
+ const response = await axios.get("/api/admin/stats", {
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
+
+export async function getAdminUsers(token, role = null, page = 1, limit = 20) {
+ const response = await axios.get("/api/admin/users", {
+ params: {
+ page,
+ limit,
+ ...(role ? {
+ role
+ } : {})
+ },
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
+
+export async function getAllVenuesAdmin(token, status = null, page = 1, limit = 20) {
+ const response = await axios.get("/api/admin/venues", {
+ params: {
+ page,
+ limit,
+ ...(status ? {
+ status
+ } : {})
+ },
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
+
+export async function updateVenueStatus(id, status, token) {
+ const response = await axios.put(`/api/admin/venues/${id}/status`, {
+ status
+ }, {
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
+
+export async function getAllBookingsAdmin(token, status = null, page = 1, limit = 20) {
+ const response = await axios.get("/api/admin/bookings", {
+ params: {
+ page,
+ limit,
+ ...(status ? {
+ status
+ } : {})
+ },
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
+
+export async function updateCategoryStatus(id, is_active, token) {
+ const response = await axios.put(`/api/admin/categories/${id}/status`, {
+ is_active
+ }, {
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ });
+ return response.data;
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/auth.js b/bookmyvenue/apps/Frontend/src/api/auth.js
new file mode 100644
index 0000000000..f14d17739f
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/auth.js
@@ -0,0 +1,30 @@
+import api from "./axios";
+
+/**
+ * Register a new user
+ */
+export async function registerUser(userData) {
+ const response = await api.post("/api/auth/register", userData);
+ return response.data;
+}
+
+/**
+ * Login using FastAPI OAuth2PasswordRequestForm
+ */
+export async function loginUser(email, password) {
+ const formData = new URLSearchParams();
+
+ formData.append("username", email);
+ formData.append("password", password);
+
+ const response = await api.post(
+ "/api/auth/login",
+ formData, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ }
+ );
+
+ return response.data;
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/availability.js b/bookmyvenue/apps/Frontend/src/api/availability.js
new file mode 100644
index 0000000000..50aa8074a8
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/availability.js
@@ -0,0 +1,34 @@
+import api from "./axios";
+
+export async function getAvailability(venueId, date = null) {
+ const response = await api.get(
+ `/api/availability/${venueId}`, {
+ params: date ? {
+ date
+ } : {},
+ }
+ );
+
+ return response.data;
+}
+
+export async function createAvailability(data, token) {
+ const response = await api.post(
+ "/api/availability/",
+ data, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return response.data;
+}
+
+export async function deleteAvailability(id, token) {
+ await api.delete(`/api/availability/${id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/axios.js b/bookmyvenue/apps/Frontend/src/api/axios.js
new file mode 100644
index 0000000000..1ff80d3132
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/axios.js
@@ -0,0 +1,8 @@
+import axios from "axios";
+
+const api = axios.create({
+ baseURL: "http://localhost:8000/",
+
+});
+
+export default api;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/bookings.js b/bookmyvenue/apps/Frontend/src/api/bookings.js
new file mode 100644
index 0000000000..329dd43a10
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/bookings.js
@@ -0,0 +1,53 @@
+import api from "./axios";
+
+export async function createBooking(data, token) {
+ const response = await api.post(
+ "/api/bookings",
+ data,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return response.data;
+}
+
+export async function getMyBookings(token) {
+ const response = await api.get("/api/bookings/my", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+}
+
+export async function getOwnerBookings(token) {
+ const response = await api.get("/api/bookings/owner", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+}
+
+export async function updateBookingStatus(
+ bookingId,
+ status,
+ token
+) {
+ const response = await api.put(
+ `/api/bookings/${bookingId}/status`,
+ { status },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return response.data;
+}
diff --git a/bookmyvenue/apps/Frontend/src/api/categories.js b/bookmyvenue/apps/Frontend/src/api/categories.js
new file mode 100644
index 0000000000..5c91a949ae
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/categories.js
@@ -0,0 +1,7 @@
+import api from "./axios";
+
+export async function getCategories() {
+ const response = await api.get("/api/categories");
+
+ return response.data;
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/api/venues.js b/bookmyvenue/apps/Frontend/src/api/venues.js
new file mode 100644
index 0000000000..bc5dced016
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/api/venues.js
@@ -0,0 +1,79 @@
+import api from "./axios";
+
+export async function createVenue(data, token) {
+ const response = await api.post("/api/venues", data, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+}
+
+export async function getAllVenues() {
+ const response = await api.get("/api/venues");
+
+ return response.data;
+}
+
+export async function getVenueById(id) {
+ const response = await api.get(`/api/venues/${id}`);
+
+ return response.data;
+}
+
+export async function getMyVenues(token) {
+ const response = await api.get("/api/venues/owner/me", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+}
+
+export async function updateVenue(id, data, token) {
+ const response = await api.put(`/api/venues/update/${id}`, data, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data;
+}
+
+// New function for filtered / paginated venues
+export async function getVenues(params = {}) {
+ const response = await api.get("/api/venues", {
+ params: params,
+ });
+
+ return response.data;
+}
+
+// New function for homepage data
+export async function getHomepageData() {
+ const response = await api.get("/api/venues/homepage");
+
+ return response.data;
+}
+
+export async function getOwnerVenues(token) {
+ const response = await api.get(
+ "/api/venues/owner", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return response.data;
+}
+
+export async function deleteVenue(id, token) {
+ await api.delete(`/api/venues/${id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/assets/image.png b/bookmyvenue/apps/Frontend/src/assets/image.png
new file mode 100644
index 0000000000..9740dd66d5
Binary files /dev/null and b/bookmyvenue/apps/Frontend/src/assets/image.png differ
diff --git a/bookmyvenue/apps/Frontend/src/components/ProtectedRoute.jsx b/bookmyvenue/apps/Frontend/src/components/ProtectedRoute.jsx
new file mode 100644
index 0000000000..5a6b830604
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/ProtectedRoute.jsx
@@ -0,0 +1,34 @@
+import { Navigate, useLocation } from "react-router-dom";
+import { useEffect } from "react";
+
+import Loading from "./common/Loading";
+import { useAuth } from "../context/AuthContext";
+
+function ProtectedRoute({ children, allowedRoles }) {
+
+ const { currentUser, loading } = useAuth();
+ const location = useLocation();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [location.pathname]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (!currentUser) {
+ return ;
+ }
+
+ if (
+ allowedRoles &&
+ !allowedRoles.includes(currentUser.role)
+ ) {
+ return ;
+ }
+
+ return children;
+}
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/admin/AdminBookingCard.jsx b/bookmyvenue/apps/Frontend/src/components/admin/AdminBookingCard.jsx
new file mode 100644
index 0000000000..bd0ab4a7f4
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/admin/AdminBookingCard.jsx
@@ -0,0 +1,286 @@
+import React from 'react';
+import { Link } from '@tanstack/react-router';
+import {
+ Calendar,
+ Clock,
+ MapPin,
+ User,
+ DollarSign,
+ MessageSquare,
+ MoreHorizontal,
+ CheckCircle,
+ XCircle,
+ Eye,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+
+const statusConfig = {
+ pending: {
+ label: 'Pending',
+ className: 'bg-amber-100 text-amber-800 border-amber-200',
+ },
+ approved: {
+ label: 'Approved',
+ className: 'bg-blue-100 text-blue-800 border-blue-200',
+ },
+ confirmed: {
+ label: 'Confirmed',
+ className: 'bg-green-100 text-green-800 border-green-200',
+ },
+ cancelled: {
+ label: 'Cancelled',
+ className: 'bg-red-100 text-red-800 border-red-200',
+ },
+ rejected: {
+ label: 'Rejected',
+ className: 'bg-gray-100 text-gray-800 border-gray-200',
+ },
+ completed: {
+ label: 'Completed',
+ className: 'bg-emerald-100 text-emerald-800 border-emerald-200',
+ },
+};
+
+const formatDate = (dateString) => {
+ if (!dateString) return 'N/A';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+};
+
+const formatTime = (timeString) => {
+ if (!timeString) return 'N/A';
+ const [hours, minutes] = timeString.split(':');
+ const date = new Date();
+ date.setHours(parseInt(hours, 10), parseInt(minutes, 10));
+ return date.toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+};
+
+const formatCurrency = (amount) => {
+ if (amount === undefined || amount === null) return '$0.00';
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount);
+};
+
+const AdminBookingCard = ({
+ booking,
+ onApprove,
+ onReject,
+ onCancel,
+ onViewDetails,
+ showActions = true,
+}) => {
+ const status = statusConfig[booking?.status] || statusConfig.pending;
+
+ const handleApprove = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onApprove?.(booking);
+ };
+
+ const handleReject = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onReject?.(booking);
+ };
+
+ const handleCancel = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onCancel?.(booking);
+ };
+
+ const handleViewDetails = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onViewDetails?.(booking);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {booking?.venue?.name || 'Unknown Venue'}
+
+
+ Booking #{booking?.id?.slice(-8) || 'N/A'}
+
+
+
+
+ {status.label}
+
+ {showActions && (
+
+
+
+
+
+
+
+
+
+
+ View details
+
+
+ {booking?.status === 'pending' && (
+ <>
+
+
+ Approve
+
+
+
+ Reject
+
+ >
+ )}
+ {(booking?.status === 'approved' ||
+ booking?.status === 'confirmed') && (
+
+
+ Cancel booking
+
+ )}
+
+
+ )}
+
+
+
+ {/* Details Grid */}
+
+
+
+
+
Customer
+
+ {booking?.customer?.name || 'N/A'}
+
+
+ {booking?.customer?.email || ''}
+
+
+
+
+
+
+
+
Venue Owner
+
+ {booking?.venue?.owner?.name || 'N/A'}
+
+
+ {booking?.venue?.owner?.email || ''}
+
+
+
+
+
+
+
+
Date
+
+ {formatDate(booking?.date)}
+
+
+
+
+
+
+
+
Time
+
+ {formatTime(booking?.startTime)} - {formatTime(booking?.endTime)}
+
+
+
+
+
+
+
+
Total Amount
+
+ {formatCurrency(booking?.totalAmount)}
+
+
+
+
+
+
+
+
Booked On
+
+ {formatDate(booking?.createdAt)}
+
+
+
+
+
+ {/* Message */}
+ {booking?.message && (
+
+
+
+
Customer Message
+
+ {booking.message}
+
+
+
+ )}
+
+
+ {/* Quick Actions */}
+ {showActions && booking?.status === 'pending' && (
+
+
+
+ Approve
+
+
+
+ Reject
+
+
+ )}
+
+ );
+};
+
+export default AdminBookingCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/admin/AdminCategoryCard.jsx b/bookmyvenue/apps/Frontend/src/components/admin/AdminCategoryCard.jsx
new file mode 100644
index 0000000000..e54e210add
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/admin/AdminCategoryCard.jsx
@@ -0,0 +1,142 @@
+import { Link } from "@tanstack/react-router";
+import {
+ FolderOpen,
+ Building2,
+ Tag,
+ Eye,
+ Pencil,
+ Trash2,
+} from "lucide-react";
+
+const statusConfig = {
+ active: {
+ label: "Active",
+ className:
+ "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300",
+ },
+ inactive: {
+ label: "Inactive",
+ className:
+ "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
+ },
+};
+
+const formatNumber = (value) => {
+ if (value === undefined || value === null) return "—";
+ return new Intl.NumberFormat("en-US").format(value);
+};
+
+export function AdminCategoryCard({
+ category,
+ onEdit,
+ onDelete,
+ onViewDetails,
+ showActions = true,
+}) {
+ const status = statusConfig[category?.status] || statusConfig.active;
+
+ const handleAction = (callback) => (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (callback) callback(category);
+ };
+
+ return (
+
+
+ {category?.imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {status.label}
+
+
+
+
+
+
+
+ {category?.name || "Unnamed category"}
+
+ {category?.slug && (
+
+
+
+ {category.slug}
+
+
+ )}
+
+
+
+ {category?.description && (
+
+ {category.description}
+
+ )}
+
+
+ {formatNumber(category?.venueCount)} venues
+
+
+
+ {showActions && (
+
+ {onViewDetails && (
+
e.stopPropagation()}
+ >
+
+ View
+
+ )}
+
+ {onEdit && (
+
e.stopPropagation()}
+ >
+
+ Edit
+
+ )}
+
+ {onDelete && (
+
+
+ Delete
+
+ )}
+
+ )}
+
+
+ );
+}
+
+export default AdminCategoryCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/admin/AdminVenueCard.jsx b/bookmyvenue/apps/Frontend/src/components/admin/AdminVenueCard.jsx
new file mode 100644
index 0000000000..25291b4619
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/admin/AdminVenueCard.jsx
@@ -0,0 +1,237 @@
+import { Link } from "@tanstack/react-router";
+import {
+ MapPin,
+ Users,
+ DollarSign,
+ Building2,
+ Eye,
+ Pencil,
+ Trash2,
+ CheckCircle,
+ XCircle,
+ ShieldAlert,
+ ShieldCheck,
+ User,
+} from "lucide-react";
+
+const statusConfig = {
+ active: {
+ label: "Active",
+ className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300",
+ },
+ pending: {
+ label: "Pending Approval",
+ className: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300",
+ },
+ inactive: {
+ label: "Inactive",
+ className: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
+ },
+ suspended: {
+ label: "Suspended",
+ className: "bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-300",
+ },
+ rejected: {
+ label: "Rejected",
+ className: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300",
+ },
+};
+
+const formatCurrency = (value) => {
+ if (value === undefined || value === null) return "—";
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(value);
+};
+
+const formatNumber = (value) => {
+ if (value === undefined || value === null) return "—";
+ return new Intl.NumberFormat("en-US").format(value);
+};
+
+export function AdminVenueCard({
+ venue,
+ onEdit,
+ onDelete,
+ onApprove,
+ onReject,
+ onSuspend,
+ onActivate,
+ onViewDetails,
+ showActions = true,
+}) {
+ const status = statusConfig[venue?.status] || statusConfig.inactive;
+
+ const handleAction = (callback) => (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (callback) callback(venue);
+ };
+
+ const isPending = venue?.status === "pending";
+ const isSuspended = venue?.status === "suspended";
+ const isActive = venue?.status === "active";
+
+ return (
+
+
+ {venue?.imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {status.label}
+
+
+
+
+
+
+
+ {venue?.name || "Unnamed venue"}
+
+ {venue?.rating && (
+
+ {venue.rating}
+ /5
+
+ )}
+
+
+
+ {venue?.owner && (
+
+
+ {venue.owner.name || venue.owner.email || "Unknown owner"}
+
+ )}
+ {venue?.location && (
+
+
+ {venue.location}
+
+ )}
+
+ {venue?.capacity !== undefined && (
+
+
+ {formatNumber(venue.capacity)}
+
+ )}
+ {venue?.pricePerHour !== undefined && (
+
+
+ {formatCurrency(venue.pricePerHour)}/hr
+
+ )}
+
+
+
+ {showActions && (
+
+ {onViewDetails && (
+
e.stopPropagation()}
+ >
+
+ View
+
+ )}
+
+ {isPending && (
+ <>
+ {onApprove && (
+
+
+ Approve
+
+ )}
+ {onReject && (
+
+
+ Reject
+
+ )}
+ >
+ )}
+
+ {isActive && onSuspend && (
+
+
+ Suspend
+
+ )}
+
+ {(isSuspended || isInactive) && onActivate && (
+
+
+ Activate
+
+ )}
+
+ {onEdit && (
+
e.stopPropagation()}
+ >
+
+ Edit
+
+ )}
+
+ {onDelete && (
+
+
+ Delete
+
+ )}
+
+ )}
+
+
+ );
+}
+
+export default AdminVenueCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/auth/LoginForm.jsx b/bookmyvenue/apps/Frontend/src/components/auth/LoginForm.jsx
new file mode 100644
index 0000000000..c2c7c7e016
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/auth/LoginForm.jsx
@@ -0,0 +1,194 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { Mail, Lock } from "lucide-react";
+import { jwtDecode } from "jwt-decode";
+
+import { loginUser } from "../../api/auth";
+import { useAuth } from "../../context/AuthContext";
+
+function LoginForm({
+ isModal = false,
+ onSuccess,
+ onSwitchToRegister,
+}) {
+ const navigate = useNavigate();
+ const { login } = useAuth();
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] =
+ useState("");
+
+ const [loading, setLoading] =
+ useState(false);
+
+ const [error, setError] =
+ useState("");
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+
+ setError("");
+
+ if (!email.trim()) {
+ setError("Email is required.");
+ return;
+ }
+
+ if (!password) {
+ setError("Password is required.");
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ const response =
+ await loginUser(email, password);
+
+ login(response.access_token);
+
+ if (onSuccess) {
+ onSuccess(response.access_token);
+ return;
+ }
+
+ const decoded = jwtDecode(response.access_token);
+ const role = decoded.role;
+
+ if (role === "admin") navigate("/admin/dashboard");
+ else if (role === "owner") navigate("/owner/dashboard");
+ else if (role === "booker") navigate("/dashboard");
+ else navigate("/");
+ } catch (err) {
+ setError(
+ err.response?.data?.detail ||
+ "Invalid email or password."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {!isModal && (
+ <>
+
+ Welcome Back
+
+
+
+ Login to continue
+
+ >
+ )}
+
+
+
+ );
+}
+
+export default LoginForm;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/auth/RegisterForm.jsx b/bookmyvenue/apps/Frontend/src/components/auth/RegisterForm.jsx
new file mode 100644
index 0000000000..fb8db844a8
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/auth/RegisterForm.jsx
@@ -0,0 +1,336 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import {
+ User,
+ Mail,
+ Lock,
+ CheckCircle,
+ AlertCircle,
+} from "lucide-react";
+
+import {
+ registerUser,
+} from "../../api/auth";
+
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+function RegisterForm({
+ isModal = false,
+ onSuccess,
+ onSwitchToLogin,
+}) {
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ password: "",
+ confirmPassword: "",
+ role: "booker",
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [success, setSuccess] = useState(false);
+
+ function updateField(field, value) {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ }
+
+ function validate() {
+ const errors = [];
+
+ if (!formData.name.trim()) {
+ errors.push("Full name is required.");
+ } else if (formData.name.trim().length < 2) {
+ errors.push("Name must be at least 2 characters.");
+ } else if (formData.name.trim().length > 50) {
+ errors.push("Name must be under 50 characters.");
+ }
+
+ if (!formData.email.trim()) {
+ errors.push("Email is required.");
+ } else if (!EMAIL_REGEX.test(formData.email.trim())) {
+ errors.push("Enter a valid email address.");
+ }
+
+ if (!formData.password) {
+ errors.push("Password is required.");
+ } else if (formData.password.length < 8) {
+ errors.push("Password must be at least 8 characters.");
+ } else if (formData.password.length > 100) {
+ errors.push("Password must be under 100 characters.");
+ }
+
+ if (!formData.confirmPassword) {
+ errors.push("Please confirm your password.");
+ } else if (formData.password !== formData.confirmPassword) {
+ errors.push("Passwords do not match.");
+ }
+
+ return errors;
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+
+ setError("");
+ setSuccess(false);
+
+ const validationErrors = validate();
+ if (validationErrors.length > 0) {
+ setError(validationErrors[0]);
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ await registerUser({
+ name: formData.name.trim(),
+ email: formData.email.trim().toLowerCase(),
+ password: formData.password,
+ role: formData.role,
+ });
+
+ setSuccess(true);
+
+ if (onSuccess) {
+ onSuccess();
+ return;
+ }
+
+ setTimeout(() => {
+ navigate("/login");
+ }, 1500);
+ } catch (err) {
+ const detail = err.response?.data?.detail;
+ if (Array.isArray(detail)) {
+ setError(detail[0]?.msg || "Registration failed.");
+ } else {
+ setError(detail || "Registration failed.");
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {!isModal && !success && (
+ <>
+
+ Create Account
+
+
+ Join BookMyVenue today.
+
+ >
+ )}
+
+ {success ? (
+
+
+
Account Created!
+
+ Redirecting you to login...
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default RegisterForm;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/common/Footer.jsx b/bookmyvenue/apps/Frontend/src/components/common/Footer.jsx
new file mode 100644
index 0000000000..3df17ead11
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/common/Footer.jsx
@@ -0,0 +1,50 @@
+import { Link } from "react-router-dom";
+import Logo from "./Logo";
+
+function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ const footerLinks = [
+ { label: "About", to: "/about" },
+ { label: "Contact", to: "/contact" },
+ { label: "Terms", to: "/terms" },
+ { label: "Privacy", to: "/privacy" },
+ ];
+
+ return (
+
+
+
+
+
+
+ Find and book the perfect venue for your next event.
+
+
+
+
+ {footerLinks.map((link) => (
+
+ {link.label}
+
+ ))}
+
+
+
+
+
+ © {currentYear} BookMyVenue. All rights reserved.
+
+
+
+
+ );
+}
+
+export default Footer;
+
+
diff --git a/bookmyvenue/apps/Frontend/src/components/common/Header.jsx b/bookmyvenue/apps/Frontend/src/components/common/Header.jsx
new file mode 100644
index 0000000000..9b343c1ad5
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/common/Header.jsx
@@ -0,0 +1,76 @@
+// src/components/common/Header.jsx
+import { useNavigate } from "react-router-dom";
+
+import { useAuth } from "../../context/AuthContext";
+import Logo from "./Logo";
+
+function Header({ cta }) {
+ const navigate = useNavigate();
+ const { token, currentUser, logout } = useAuth();
+
+ function handleLogout() {
+ logout();
+ navigate("/");
+ }
+
+ return (
+
+
+ {/* Logo */}
+
navigate("/")} />
+
+ {/* Right side actions */}
+
+ {/* Optional page-specific CTA passed in as a prop */}
+ {cta && (
+
+ {cta.label}
+
+ )}
+
+ {token ? (
+ <>
+ {currentUser?.role === "owner" && (
+ navigate("/owner/dashboard")}
+ className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
+ >
+ Dashboard
+
+ )}
+
+ {currentUser?.role === "booker" && (
+ navigate("/dashboard")}
+ className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
+ >
+ My Bookings
+
+ )}
+
+
+ Logout
+
+ >
+ ) : !cta ? (
+ navigate("/login")}
+ className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700"
+ >
+ Sign in
+
+ ) : null}
+
+
+
+ );
+}
+
+export default Header;
+
diff --git a/bookmyvenue/apps/Frontend/src/components/common/Loading.jsx b/bookmyvenue/apps/Frontend/src/components/common/Loading.jsx
new file mode 100644
index 0000000000..841cee0ea8
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/common/Loading.jsx
@@ -0,0 +1,12 @@
+function Loading({ message = "Loading..." }) {
+ return (
+
+
+ {message && (
+
{message}
+ )}
+
+ );
+}
+
+export default Loading;
diff --git a/bookmyvenue/apps/Frontend/src/components/common/Logo.jsx b/bookmyvenue/apps/Frontend/src/components/common/Logo.jsx
new file mode 100644
index 0000000000..840bc4cb20
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/common/Logo.jsx
@@ -0,0 +1,32 @@
+import { Link } from "react-router-dom";
+
+function Logo({ className = "", to = "/", onClick }) {
+ if (onClick) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default Logo;
diff --git a/bookmyvenue/apps/Frontend/src/components/common/Modal.jsx b/bookmyvenue/apps/Frontend/src/components/common/Modal.jsx
new file mode 100644
index 0000000000..cece37229c
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/common/Modal.jsx
@@ -0,0 +1,83 @@
+import { useEffect } from "react";
+
+function Modal({ isOpen, onClose, title, children, maxWidth = "max-w-md" }) {
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ const handleEscape = (e) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+
+ if (isOpen) {
+ window.addEventListener("keydown", handleEscape);
+ }
+
+ return () => {
+ window.removeEventListener("keydown", handleEscape);
+ };
+ }, [isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+
+
+
+
+
+
{children}
+
+
+ );
+}
+
+export default Modal;
diff --git a/bookmyvenue/apps/Frontend/src/components/home/Hero.jsx b/bookmyvenue/apps/Frontend/src/components/home/Hero.jsx
new file mode 100644
index 0000000000..8bf9c9334b
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/home/Hero.jsx
@@ -0,0 +1,43 @@
+import { MapPin, Star, Users, Clock } from "lucide-react";
+
+function Hero() {
+ return (
+
+ {/* Decorative background elements */}
+
+
+
+
+
+ Find & Book the
+ Perfect Venue
+
+
+ Discover thousands of stunning venues for weddings, conferences,
+ parties, and every special moment. Book with confidence.
+
+
+
+
+
+ Top locations nationwide
+
+
+
+ Verified reviews
+
+
+
+ 50,000+ happy customers
+
+
+
+ Instant booking
+
+
+
+
+ );
+}
+
+export default Hero;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/home/HomepageSections.jsx b/bookmyvenue/apps/Frontend/src/components/home/HomepageSections.jsx
new file mode 100644
index 0000000000..44d9959f0f
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/home/HomepageSections.jsx
@@ -0,0 +1,179 @@
+import { Loader2, AlertCircle, ChevronRight } from "lucide-react";
+import VenueCard from "../venue/VenueCard";
+
+function SectionHeader({ title, subtitle }) {
+ return (
+
+
{title}
+ {subtitle && (
+
{subtitle}
+ )}
+
+ );
+}
+
+function VenueGrid({ venues, loading, error, emptyMessage }) {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!venues || venues.length === 0) {
+ return (
+
+
{emptyMessage || "No venues available yet."}
+
+ );
+ }
+
+ return (
+
+ {venues.map((venue) => (
+
+ ))}
+
+ );
+}
+
+function LoadMoreButton({ visible, total, onClick }) {
+ if (visible >= total) return null;
+
+ return (
+
+
+ Load more ({visible} of {total})
+
+
+
+ );
+}
+
+function HomepageSections({
+ loading,
+ error,
+ topRatedVenues,
+ recentlyAddedVenues,
+ categorySections,
+ visibleTopRated = 4,
+ visibleRecent = 4,
+ visibleCategoryCounts = {},
+ onLoadMore,
+}) {
+ if (loading) {
+ return (
+
+
+
+ Loading venues…
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ const hasContent =
+ (topRatedVenues && topRatedVenues.length > 0) ||
+ (recentlyAddedVenues && recentlyAddedVenues.length > 0) ||
+ (categorySections && categorySections.length > 0);
+
+ if (!hasContent) {
+ return null;
+ }
+
+ return (
+
+ {/* Top Rated */}
+ {topRatedVenues && topRatedVenues.length > 0 && (
+
+
+
+ onLoadMore?.("topRated")}
+ />
+
+ )}
+
+ {/* Recently Added */}
+ {recentlyAddedVenues && recentlyAddedVenues.length > 0 && (
+
+
+
+ onLoadMore?.("recentlyAdded")}
+ />
+
+ )}
+
+ {/* Category Sections */}
+ {categorySections && categorySections.length > 0 && (
+
+ {categorySections.map((section, idx) => {
+ const catId = section.category?.id || idx;
+ const catName = section.category?.name || "Category";
+ const venues = section.venues || [];
+ const visibleCount = visibleCategoryCounts[catId] || 4;
+
+ return (
+
+
+
+ onLoadMore?.("category", catId)}
+ />
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+export default HomepageSections;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/home/SearchBar.jsx b/bookmyvenue/apps/Frontend/src/components/home/SearchBar.jsx
new file mode 100644
index 0000000000..495e8389fa
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/home/SearchBar.jsx
@@ -0,0 +1,130 @@
+import { Search, MapPin, SlidersHorizontal, X } from "lucide-react";
+
+function SearchBar({
+ searchText,
+ setSearchText,
+ city,
+ setCity,
+ categoryId,
+ setCategoryId,
+ availabilityType,
+ setAvailabilityType,
+ categories,
+ onSearch,
+}) {
+ const handleClear = () => {
+ setSearchText("");
+ setCity("");
+ setCategoryId("");
+ setAvailabilityType("");
+ };
+
+ const hasActiveFilters = searchText || city || categoryId || availabilityType;
+
+ return (
+
+ {/* Search input row */}
+
+ {/* Search text */}
+
+
+ Search venues
+
+
+
+ setSearchText(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && onSearch()}
+ className="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-10 pr-3 text-sm text-gray-900 placeholder-gray-400 outline-none transition focus:border-red-600 focus:ring-2 focus:ring-red-100"
+ />
+
+
+
+ {/* City */}
+
+
+ City
+
+
+
+ setCity(e.target.value)}
+ className="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-10 pr-3 text-sm text-gray-900 placeholder-gray-400 outline-none transition focus:border-red-600 focus:ring-2 focus:ring-red-100"
+ />
+
+
+
+ {/* Category */}
+
+
+ Category
+
+ setCategoryId(e.target.value)}
+ className="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-3 pr-8 text-sm text-gray-900 outline-none transition focus:border-red-600 focus:ring-2 focus:ring-red-100"
+ >
+ All categories
+ {categories.map((cat) => (
+
+ {cat.name}
+
+ ))}
+
+
+
+ {/* Search button */}
+
+
+ Search
+
+
+
+ {/* Active filters bar */}
+ {hasActiveFilters && (
+
+ Filters:
+ {searchText && (
+
+ {searchText}
+
+ )}
+ {city && (
+
+
+ {city}
+
+ )}
+ {categoryId && (
+
+ {categories.find((c) => c.id === categoryId)?.name || "Selected"}
+
+ )}
+ {availabilityType && (
+
+ {availabilityType}
+
+ )}
+
+
+ Clear all
+
+
+ )}
+
+ );
+}
+
+export default SearchBar;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/home/SearchResults.jsx b/bookmyvenue/apps/Frontend/src/components/home/SearchResults.jsx
new file mode 100644
index 0000000000..33149fd749
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/home/SearchResults.jsx
@@ -0,0 +1,113 @@
+import { Loader2, AlertCircle, SearchX } from "lucide-react";
+import VenueCard from "../venue/VenueCard";
+
+function SearchResults({
+ venues,
+ loading,
+ error,
+ currentPage,
+ total,
+ limit,
+ onPageClick,
+}) {
+ const totalPages = Math.ceil(total / limit);
+
+ if (loading) {
+ return (
+
+
+
+ Searching venues…
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!venues || venues.length === 0) {
+ return (
+
+
+
+ No venues found
+
+
+ Try adjusting your search or filter criteria.
+
+
+ );
+ }
+
+ return (
+
+
+
+ {total} {" "}
+ {total === 1 ? "venue" : "venues"} found
+
+
+
+
+ {venues.map((venue) => (
+
+ ))}
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ onPageClick(currentPage - 1)}
+ disabled={currentPage <= 1}
+ className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+ Previous
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1)
+ .filter((page) => {
+ if (totalPages <= 7) return true;
+ if (page === 1 || page === totalPages) return true;
+ if (Math.abs(page - currentPage) <= 1) return true;
+ return false;
+ })
+ .map((page, idx, arr) => (
+
+ {idx > 0 && arr[idx - 1] !== page - 1 && (
+ …
+ )}
+ onPageClick(page)}
+ className={`rounded-lg px-3 py-2 text-sm font-medium transition ${
+ page === currentPage
+ ? "bg-red-600 text-white"
+ : "text-gray-700 hover:bg-gray-100"
+ }`}
+ >
+ {page}
+
+
+ ))}
+
+ onPageClick(currentPage + 1)}
+ disabled={currentPage >= totalPages}
+ className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+ Next
+
+
+ )}
+
+ );
+}
+
+export default SearchResults;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/owner/BookingRequestCard.jsx b/bookmyvenue/apps/Frontend/src/components/owner/BookingRequestCard.jsx
new file mode 100644
index 0000000000..dc3ac00e33
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/owner/BookingRequestCard.jsx
@@ -0,0 +1,205 @@
+import { Link } from "@tanstack/react-router";
+import { Calendar, Clock, MapPin, User, DollarSign, MessageSquare } from "lucide-react";
+
+const statusConfig = {
+ pending: {
+ label: "Pending",
+ className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
+ },
+ approved: {
+ label: "Approved",
+ className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+ },
+ confirmed: {
+ label: "Confirmed",
+ className: "bg-green-500/10 text-green-500 border-green-500/20",
+ },
+ cancelled: {
+ label: "Cancelled",
+ className: "bg-red-500/10 text-red-500 border-red-500/20",
+ },
+ rejected: {
+ label: "Rejected",
+ className: "bg-gray-500/10 text-gray-400 border-gray-500/20",
+ },
+ completed: {
+ label: "Completed",
+ className: "bg-emerald-500/10 text-emerald-500 border-emerald-500/20",
+ },
+};
+
+export function BookingRequestCard({
+ booking,
+ onApprove,
+ onReject,
+ onViewDetails,
+ showActions = true,
+}) {
+ const status = booking?.status?.toLowerCase?.() || "pending";
+ const config = statusConfig[status] || statusConfig.pending;
+
+ const formatDate = (value) => {
+ if (!value) return "—";
+ const date = new Date(value);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+ };
+
+ const formatTime = (value) => {
+ if (!value) return "—";
+ const date = new Date(value);
+ return date.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ });
+ };
+
+ const formatCurrency = (value) => {
+ if (value == null) return "$—";
+ return `$${Number(value).toLocaleString("en-US", {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 2,
+ })}`;
+ };
+
+ const handleApprove = (e) => {
+ e.stopPropagation();
+ onApprove?.(booking);
+ };
+
+ const handleReject = (e) => {
+ e.stopPropagation();
+ onReject?.(booking);
+ };
+
+ const handleViewDetails = (e) => {
+ e.stopPropagation();
+ onViewDetails?.(booking);
+ };
+
+ return (
+
+
+
+
+ {booking?.venue?.name || "Venue"}
+
+
+
+
+ {booking?.venue?.location || "Location unavailable"}
+
+
+
+
+ {config.label}
+
+
+
+
+
+
+
+
+
+
{booking?.customer?.name || "Guest"}
+
+ {booking?.customer?.email || "No email provided"}
+
+
+
+
+
+
+
+
+
+
{formatDate(booking?.startTime)}
+
Booking date
+
+
+
+
+
+
+
+
+
+ {formatTime(booking?.startTime)} — {formatTime(booking?.endTime)}
+
+
Time window
+
+
+
+
+
+
+
+
+
{formatCurrency(booking?.totalAmount)}
+
Total
+
+
+
+
+ {booking?.message && (
+
+ )}
+
+ {showActions && (
+
+ {status === "pending" && (
+ <>
+
+ Approve
+
+
+ Reject
+
+ >
+ )}
+
+ {onViewDetails ? (
+
+ View details
+
+ ) : (
+ e.stopPropagation()}
+ >
+ View details
+
+ )}
+
+ )}
+
+ );
+}
+
+export default BookingRequestCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/owner/DashboardStats.jsx b/bookmyvenue/apps/Frontend/src/components/owner/DashboardStats.jsx
new file mode 100644
index 0000000000..36a495d3e5
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/owner/DashboardStats.jsx
@@ -0,0 +1,118 @@
+import {
+ Building2,
+ CalendarDays,
+ Clock,
+ DollarSign,
+ TrendingUp,
+ Users,
+} from "lucide-react";
+
+const defaultStats = {
+ totalVenues: 0,
+ totalBookings: 0,
+ pendingRequests: 0,
+ totalRevenue: 0,
+ occupancyRate: 0,
+ activeListings: 0,
+};
+
+const formatCurrency = (value) => {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(value ?? 0);
+};
+
+const formatNumber = (value) => {
+ return new Intl.NumberFormat("en-US").format(value ?? 0);
+};
+
+const formatPercent = (value) => {
+ return `${Math.round((value ?? 0) * 100) / 100}%`;
+};
+
+export function DashboardStats({ stats = defaultStats, isLoading = false }) {
+ const statItems = [
+ {
+ id: "totalVenues",
+ label: "Total Venues",
+ value: formatNumber(stats.totalVenues),
+ icon: Building2,
+ accent: "bg-blue-500/10 text-blue-500",
+ },
+ {
+ id: "activeListings",
+ label: "Active Listings",
+ value: formatNumber(stats.activeListings),
+ icon: TrendingUp,
+ accent: "bg-emerald-500/10 text-emerald-500",
+ },
+ {
+ id: "totalBookings",
+ label: "Total Bookings",
+ value: formatNumber(stats.totalBookings),
+ icon: CalendarDays,
+ accent: "bg-purple-500/10 text-purple-500",
+ },
+ {
+ id: "pendingRequests",
+ label: "Pending Requests",
+ value: formatNumber(stats.pendingRequests),
+ icon: Clock,
+ accent: "bg-amber-500/10 text-amber-500",
+ },
+ {
+ id: "totalRevenue",
+ label: "Total Revenue",
+ value: formatCurrency(stats.totalRevenue),
+ icon: DollarSign,
+ accent: "bg-rose-500/10 text-rose-500",
+ },
+ {
+ id: "occupancyRate",
+ label: "Occupancy Rate",
+ value: formatPercent(stats.occupancyRate),
+ icon: Users,
+ accent: "bg-cyan-500/10 text-cyan-500",
+ },
+ ];
+
+ return (
+
+
+ {statItems.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+ {item.label}
+
+
+ {isLoading ? (
+
+ ) : (
+ item.value
+ )}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default DashboardStats;
diff --git a/bookmyvenue/apps/Frontend/src/components/owner/OwnerVenueCard.jsx b/bookmyvenue/apps/Frontend/src/components/owner/OwnerVenueCard.jsx
new file mode 100644
index 0000000000..85c0a584ec
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/owner/OwnerVenueCard.jsx
@@ -0,0 +1,194 @@
+import { Link } from "@tanstack/react-router";
+
+function OwnerVenueCard({
+ venue,
+ onEdit,
+ onDelete,
+ onManage,
+ onViewBookings,
+ showActions = true,
+}) {
+ const {
+ id,
+ name,
+ location,
+ imageUrl,
+ rating,
+ reviewCount,
+ capacity,
+ pricePerHour,
+ status = "active",
+ } = venue;
+
+ const statusStyles = {
+ active: "bg-green-100 text-green-800",
+ pending: "bg-yellow-100 text-yellow-800",
+ inactive: "bg-gray-100 text-gray-800",
+ suspended: "bg-red-100 text-red-800",
+ };
+
+ const handleEditClick = (e) => {
+ e.stopPropagation();
+ onEdit?.(venue);
+ };
+
+ const handleDeleteClick = (e) => {
+ e.stopPropagation();
+ onDelete?.(venue);
+ };
+
+ const handleManageClick = (e) => {
+ e.stopPropagation();
+ onManage?.(venue);
+ };
+
+ const handleBookingsClick = (e) => {
+ e.stopPropagation();
+ onViewBookings?.(venue);
+ };
+
+ return (
+
+
+
+
+
+ {status}
+
+
+
+
+
+
+ {rating ?? "—"}
+ {reviewCount != null && (
+
({reviewCount})
+ )}
+
+
+
+
+
{name}
+
+
+
+
+ {capacity != null && (
+
+
+
+
+
Up to {capacity}
+
+ )}
+ {pricePerHour != null && (
+
+
+
+
+
${pricePerHour}/hr
+
+ )}
+
+
+ {showActions && (
+
+
+
+ Edit
+
+
+ Bookings
+
+
+
+
+ Manage
+
+
+ Delete
+
+
+
+ )}
+
+
+ );
+}
+
+export default OwnerVenueCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/VenueCard.jsx b/bookmyvenue/apps/Frontend/src/components/venue/VenueCard.jsx
new file mode 100644
index 0000000000..86cb50b7f0
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/VenueCard.jsx
@@ -0,0 +1,139 @@
+import { Link } from "react-router-dom";
+
+function VenueCard({
+ venue,
+ onViewDetails,
+ showActions = true,
+}) {
+ const {
+ id,
+ name,
+ city,
+ address_line,
+ images,
+ capacity,
+ hourly_price,
+ daily_price,
+ } = venue;
+
+ const imageUrl = images && images.length > 0 ? images[0].image_url : null;
+ const location = city || address_line || "";
+ const supportsHourly = hourly_price != null;
+ const supportsDaily = daily_price != null;
+
+
+
+ const handleDetailsClick = (e) => {
+ e.stopPropagation();
+ onViewDetails?.(venue);
+ };
+
+ return (
+
+
+
+ {/* Rating badge removed — not yet available from backend */}
+
+
+
+
{name}
+
+
+
+
+ {capacity != null && (
+
+
+
+
+
Up to {capacity}
+
+ )}
+
+
+ {supportsHourly && (
+
+
+
+
+
₹{hourly_price}/hr
+
+ )}
+ {supportsDaily && (
+
+ ₹{daily_price}/day
+
+ )}
+
+
+
+ {showActions && (
+
+
+ View details
+
+
+ )}
+
+
+ );
+}
+
+export default VenueCard;
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenueAmenitiesForm.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueAmenitiesForm.jsx
new file mode 100644
index 0000000000..865317bd25
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueAmenitiesForm.jsx
@@ -0,0 +1,166 @@
+import { useState } from "react";
+import {
+ Plus,
+ X,
+ ListChecks,
+ ShieldCheck,
+} from "lucide-react";
+
+function VenueAmenitiesForm({
+ formData,
+ updateField,
+}) {
+ const [amenity, setAmenity] = useState("");
+
+ function addAmenity() {
+ const value = amenity.trim();
+
+ if (!value) return;
+
+ if (
+ formData.amenities.some(
+ (item) =>
+ item.toLowerCase() ===
+ value.toLowerCase()
+ )
+ ) {
+ setAmenity("");
+ return;
+ }
+
+ updateField("amenities", [
+ ...formData.amenities,
+ value,
+ ]);
+
+ setAmenity("");
+ }
+
+ function removeAmenity(index) {
+ updateField(
+ "amenities",
+ formData.amenities.filter(
+ (_, i) => i !== index
+ )
+ );
+ }
+
+ function handleKeyDown(e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ addAmenity();
+ }
+ }
+
+ return (
+
+
+
+
+ Amenities & Policies
+
+
+
+ Tell guests what your venue offers.
+
+
+
+ {/* Amenities */}
+
+
+
+
+
+
+ Amenities
+
+
+
+
+
+ setAmenity(e.target.value)
+ }
+ onKeyDown={handleKeyDown}
+ className="flex-1 rounded-xl border border-gray-300 px-4 py-3 outline-none transition focus:border-red-500"
+ />
+
+
+
+
+
+
+
+ {formData.amenities.length > 0 && (
+
+
+ {formData.amenities.map(
+ (item, index) => (
+
+ {item}
+
+
+ removeAmenity(index)
+ }
+ >
+
+
+
+ )
+ )}
+
+
+ )}
+
+
+
+ {/* Cancellation Policy */}
+
+
+
+
+
+
+ Cancellation Policy
+
+
+
+
+
+ );
+}
+
+export default VenueAmenitiesForm;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenueBasicInfo.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueBasicInfo.jsx
new file mode 100644
index 0000000000..e59cd61f27
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueBasicInfo.jsx
@@ -0,0 +1,145 @@
+import { FileText, Tag, Type } from "lucide-react";
+
+function VenueBasicInfo({
+ formData,
+ updateField,
+ categories,
+ errors,
+}) {
+ return (
+
+
+
+ Basic Information
+
+
+
+ Tell people about your venue.
+
+
+
+
+ {/* Venue Name */}
+
+
+
+
+ Venue Name
+
+
+
+ updateField(
+ "name",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition
+ ${
+ errors.name
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.name && (
+
+ {errors.name}
+
+ )}
+
+
+ {/* Description */}
+
+
+
+ {/* Category */}
+
+
+
+
+ Category
+
+
+
+ updateField(
+ "category_id",
+ Number(e.target.value)
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition
+ ${
+ errors.category_id
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ >
+
+ Select Category
+
+
+ {categories.map((category) => (
+
+ {category.name}
+
+ ))}
+
+
+ {errors.category_id && (
+
+ {errors.category_id}
+
+ )}
+
+
+
+ );
+}
+
+export default VenueBasicInfo;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenueForm.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueForm.jsx
new file mode 100644
index 0000000000..483b8c5ede
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueForm.jsx
@@ -0,0 +1,238 @@
+import { useEffect, useState } from "react";
+
+import VenueBasicInfo from "./VenueBasicInfo";
+import VenueLocation from "./VenueLocation";
+import VenuePricingForm from "./VenuePricingForm";
+import VenueAmenitiesForm from "./VenueAmenitiesForm";
+import VenueImages from "./VenueImages";
+
+function VenueForm({
+ mode = "create",
+ initialData = null,
+ categories = [],
+ loading = false,
+ onSubmit,
+}) {
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+
+ category_id: "",
+
+ address_line: "",
+ city: "",
+ pincode: "",
+
+ capacity: "",
+
+ supports_hourly: false,
+ supports_daily: false,
+
+ hourly_price: "",
+ daily_price: "",
+
+ amenities: [],
+
+ cancellation_policy: "",
+
+ image_urls: [],
+ });
+
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ if (!initialData) return;
+
+ setFormData({
+ name: initialData.name || "",
+ description: initialData.description || "",
+
+ category_id: initialData.category_id || "",
+
+ address_line:
+ initialData.address_line || "",
+
+ city: initialData.city || "",
+
+ pincode: initialData.pincode || "",
+
+ capacity:
+ initialData.capacity || "",
+
+ supports_hourly:
+ initialData.supports_hourly,
+
+ supports_daily:
+ initialData.supports_daily,
+
+ hourly_price:
+ initialData.hourly_price || "",
+
+ daily_price:
+ initialData.daily_price || "",
+
+ amenities:
+ initialData.amenities
+ ? Array.isArray(initialData.amenities)
+ ? initialData.amenities
+ : initialData.amenities.split(",").map((a) => a.trim())
+ : [],
+
+ cancellation_policy:
+ initialData.cancellation_policy ||
+ "",
+
+ image_urls:
+ initialData.images?.map(
+ (img) => img.image_url
+ ) || [],
+ });
+ }, [initialData]);
+
+ function updateField(field, value) {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ }
+
+ function validate() {
+ const newErrors = {};
+
+ if (!formData.name.trim())
+ newErrors.name =
+ "Venue name is required.";
+
+ if (!formData.description.trim())
+ newErrors.description =
+ "Description is required.";
+
+ if (!formData.category_id)
+ newErrors.category_id =
+ "Select a category.";
+
+ if (!formData.address_line.trim())
+ newErrors.address_line =
+ "Address is required.";
+
+ if (!formData.city.trim())
+ newErrors.city =
+ "City is required.";
+
+ if (!formData.pincode.trim())
+ newErrors.pincode =
+ "Pincode is required.";
+
+ if (!formData.capacity || Number(formData.capacity) <= 0)
+ newErrors.capacity =
+ "Capacity is required.";
+
+ if (
+ !formData.supports_hourly &&
+ !formData.supports_daily
+ )
+ newErrors.booking =
+ "Select at least one booking type.";
+
+ if (
+ formData.supports_hourly &&
+ (!formData.hourly_price || Number(formData.hourly_price) <= 0)
+ )
+ newErrors.hourly_price =
+ "Hourly price required.";
+
+ if (
+ formData.supports_daily &&
+ (!formData.daily_price || Number(formData.daily_price) <= 0)
+ )
+ newErrors.daily_price =
+ "Daily price required.";
+
+ if (
+ formData.image_urls.length === 0
+ )
+ newErrors.images =
+ "Add at least one image.";
+
+ setErrors(newErrors);
+
+ return (
+ Object.keys(newErrors).length === 0
+ );
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+
+ if (!validate()) return;
+
+ const payload = {
+ ...formData,
+
+ amenities:
+ formData.amenities.join(","),
+
+ category_id: Number(formData.category_id),
+ capacity: Number(formData.capacity),
+ };
+
+ if (payload.hourly_price) payload.hourly_price = Number(payload.hourly_price);
+ else delete payload.hourly_price;
+
+ if (payload.daily_price) payload.daily_price = Number(payload.daily_price);
+ else delete payload.daily_price;
+
+ await onSubmit(payload);
+ }
+
+ return (
+
+ );
+}
+
+export default VenueForm;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenueImages.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueImages.jsx
new file mode 100644
index 0000000000..892a0bee7b
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueImages.jsx
@@ -0,0 +1,157 @@
+import { useState } from "react";
+import {
+ ImagePlus,
+ Plus,
+ Trash2,
+} from "lucide-react";
+
+function VenueImages({
+ formData,
+ updateField,
+ errors,
+}) {
+ const [imageUrl, setImageUrl] =
+ useState("");
+
+ function addImage() {
+ const url = imageUrl.trim();
+
+ if (!url) return;
+
+ if (
+ formData.image_urls.includes(url)
+ ) {
+ setImageUrl("");
+ return;
+ }
+
+ updateField("image_urls", [
+ ...formData.image_urls,
+ url,
+ ]);
+
+ setImageUrl("");
+ }
+
+ function removeImage(index) {
+ updateField(
+ "image_urls",
+ formData.image_urls.filter(
+ (_, i) => i !== index
+ )
+ );
+ }
+
+ function handleKeyDown(e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ addImage();
+ }
+ }
+
+ return (
+
+
+
+
+ Venue Images
+
+
+
+ Add one or more image URLs for your venue.
+
+
+
+ {/* Add URL */}
+
+
+
+
+ setImageUrl(e.target.value)
+ }
+ onKeyDown={handleKeyDown}
+ className="flex-1 rounded-xl border border-gray-300 px-4 py-3 outline-none transition focus:border-red-500"
+ />
+
+
+
+
+
+
+
+ {errors.images && (
+
+ {errors.images}
+
+ )}
+
+ {/* Image Preview */}
+
+ {formData.image_urls.length > 0 && (
+
+
+ {formData.image_urls.map(
+ (url, index) => (
+
+
+
+
{
+ e.target.style.display =
+ "none";
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {url}
+
+
+
+
+
+ removeImage(index)
+ }
+ className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-50 py-2 font-medium text-red-600 transition hover:bg-red-100"
+ >
+
+
+ Remove
+
+
+
+
+ )
+ )}
+
+
+ )}
+
+
+ );
+}
+
+export default VenueImages;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenueLocation.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueLocation.jsx
new file mode 100644
index 0000000000..ecc603366e
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenueLocation.jsx
@@ -0,0 +1,174 @@
+import {
+ MapPin,
+ Building2,
+ Hash,
+ Users,
+} from "lucide-react";
+
+function VenueLocation({
+ formData,
+ updateField,
+ errors,
+}) {
+ return (
+
+
+
+ Location & Capacity
+
+
+
+ Help guests find your venue.
+
+
+
+
+
+ {/* Address */}
+
+
+
+
+ Address
+
+
+
+ updateField(
+ "address_line",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.address_line
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.address_line && (
+
+ {errors.address_line}
+
+ )}
+
+
+ {/* City */}
+
+
+
+
+ City
+
+
+
+ updateField(
+ "city",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.city
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.city && (
+
+ {errors.city}
+
+ )}
+
+
+ {/* Pincode */}
+
+
+
+
+ Pincode
+
+
+
+ updateField(
+ "pincode",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.pincode
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.pincode && (
+
+ {errors.pincode}
+
+ )}
+
+
+ {/* Capacity */}
+
+
+
+
+ Capacity
+
+
+
+ updateField(
+ "capacity",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.capacity
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.capacity && (
+
+ {errors.capacity}
+
+ )}
+
+
+
+
+ );
+}
+
+export default VenueLocation;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/components/venue/form/VenuePricingForm.jsx b/bookmyvenue/apps/Frontend/src/components/venue/form/VenuePricingForm.jsx
new file mode 100644
index 0000000000..c5a707058a
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/components/venue/form/VenuePricingForm.jsx
@@ -0,0 +1,182 @@
+import {
+ Clock3,
+ CalendarDays,
+ IndianRupee,
+} from "lucide-react";
+
+function VenuePricingForm({
+ formData,
+ updateField,
+ errors,
+}) {
+ return (
+
+
+
+ Booking & Pricing
+
+
+
+ Choose how guests can book your venue and
+ set the pricing.
+
+
+
+ {/* Booking Type */}
+
+
+
+ {/* Hourly */}
+
+
+ updateField(
+ "supports_hourly",
+ !formData.supports_hourly
+ )
+ }
+ className={`rounded-2xl border p-6 text-left transition ${
+ formData.supports_hourly
+ ? "border-red-600 bg-red-50"
+ : "border-gray-300 hover:border-red-400"
+ }`}
+ >
+
+
+
+ Hourly Booking
+
+
+
+ Perfect for meetings, sports and
+ short events.
+
+
+
+ {/* Daily */}
+
+
+ updateField(
+ "supports_daily",
+ !formData.supports_daily
+ )
+ }
+ className={`rounded-2xl border p-6 text-left transition ${
+ formData.supports_daily
+ ? "border-red-600 bg-red-50"
+ : "border-gray-300 hover:border-red-400"
+ }`}
+ >
+
+
+
+ Daily Booking
+
+
+
+ Best for weddings, parties and
+ conferences.
+
+
+
+
+
+ {errors.booking && (
+
+ {errors.booking}
+
+ )}
+
+ {/* Prices */}
+
+
+
+ {formData.supports_hourly && (
+
+
+
+ Hourly Price
+
+
+
+ updateField(
+ "hourly_price",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.hourly_price
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.hourly_price && (
+
+ {errors.hourly_price}
+
+ )}
+
+ )}
+
+ {formData.supports_daily && (
+
+
+
+ Daily Price
+
+
+
+ updateField(
+ "daily_price",
+ e.target.value
+ )
+ }
+ className={`w-full rounded-xl border px-4 py-3 outline-none transition ${
+ errors.daily_price
+ ? "border-red-500"
+ : "border-gray-300 focus:border-red-500"
+ }`}
+ />
+
+ {errors.daily_price && (
+
+ {errors.daily_price}
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+export default VenuePricingForm;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/context/AuthContext.jsx b/bookmyvenue/apps/Frontend/src/context/AuthContext.jsx
new file mode 100644
index 0000000000..df2d19944d
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/context/AuthContext.jsx
@@ -0,0 +1,57 @@
+import { createContext, useContext, useEffect, useState } from "react";
+import { jwtDecode } from "jwt-decode";
+
+const AuthContext = createContext();
+
+export function AuthProvider({ children }) {
+ const [token, setToken] = useState(null);
+ const [currentUser, setCurrentUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const savedToken = localStorage.getItem("authToken");
+
+ if (savedToken) {
+ const decodedToken = jwtDecode(savedToken);
+
+ setToken(savedToken);
+ setCurrentUser({
+ id: decodedToken.sub,
+ role: decodedToken.role,
+ });
+ }
+
+ setLoading(false);
+ }, []);
+
+ function login(newToken) {
+ if (!newToken) return;
+
+ const decodedToken = jwtDecode(newToken);
+
+ localStorage.setItem("authToken", newToken);
+ setToken(newToken);
+ setCurrentUser({
+ id: decodedToken.sub,
+ role: decodedToken.role,
+ });
+ }
+
+ function logout() {
+ localStorage.removeItem("authToken");
+ setToken(null);
+ setCurrentUser(null);
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ return useContext(AuthContext);
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/index.css b/bookmyvenue/apps/Frontend/src/index.css
new file mode 100644
index 0000000000..f1d8c73cdc
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/index.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/bookmyvenue/apps/Frontend/src/main.jsx b/bookmyvenue/apps/Frontend/src/main.jsx
new file mode 100644
index 0000000000..b9a1a6deac
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/bookmyvenue/apps/Frontend/src/pages/AuthModal.jsx b/bookmyvenue/apps/Frontend/src/pages/AuthModal.jsx
new file mode 100644
index 0000000000..16ccf76144
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/AuthModal.jsx
@@ -0,0 +1,375 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { jwtDecode } from "jwt-decode";
+
+import api from "../api/axios";
+import { useAuth } from "../context/AuthContext";
+
+/* ---------- Inline icons ---------- */
+const IconClose = (props) => (
+
+
+
+);
+const IconMail = (props) => (
+
+
+
+
+);
+const IconLock = (props) => (
+
+
+
+
+);
+const IconUser = (props) => (
+
+
+
+
+);
+
+/* ---------- Brand SVG marks for social buttons ---------- */
+const GoogleMark = () => (
+
+
+
+);
+const AppleMark = () => (
+
+
+
+);
+const FacebookMark = () => (
+
+
+
+);
+
+/* ---------- Reusable bits ---------- */
+function SocialButton({ icon, label }) {
+ return (
+
+ {icon}
+ {label}
+
+ );
+}
+
+function Divider({ children }) {
+ return (
+
+ );
+}
+
+function Field({ icon, ...inputProps }) {
+ return (
+
+
+ {icon}
+
+
+
+ );
+}
+
+/* ---------- Login form ---------- */
+function LoginForm({ onSwitch, onClose }) {
+ const navigate = useNavigate();
+ const { login } = useAuth();
+ const [formData, setFormData] = useState({ username: "", password: "" });
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ function handleChange(e) {
+ const { name, value } = e.target;
+ setFormData({ ...formData, [name]: value });
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setError("");
+ setLoading(true);
+ try {
+ const response = await api.post(
+ "/api/auth/login",
+ new URLSearchParams({
+ username: formData.username,
+ password: formData.password,
+ }),
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
+ );
+ const token = response.data.access_token;
+ login(token);
+ const decodedToken = jwtDecode(token);
+ const role = decodedToken.role;
+
+ onClose?.();
+
+ if (role === "booker") navigate("/dashboard");
+ else if (role === "owner") navigate("/owner/dashboard");
+ else if (role === "admin") navigate("/admin/dashboard");
+ } catch (err) {
+ setError(err.response?.data?.detail || "Login failed");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
Sign in
+
+ Welcome back. Continue your venue search.
+
+
+
+ } label="Continue with Google" />
+ } label="Continue with Apple" />
+ } label="Continue with Facebook" />
+
+
+
or
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Don't have an account?{" "}
+
+ Create one
+
+
+
+ );
+}
+
+/* ---------- Register form ---------- */
+function RegisterForm({ onSwitch, onClose }) {
+ const navigate = useNavigate();
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ password: "",
+ role: "booker",
+ });
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ function handleChange(e) {
+ const { name, value } = e.target;
+ setFormData({ ...formData, [name]: value });
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setError("");
+ setLoading(true);
+ try {
+ await api.post("/api/auth/register", formData);
+ onClose?.();
+ navigate("/login");
+ } catch (err) {
+ setError(err.response?.data?.detail || "Registration failed");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
Create account
+
+ Join BookMyVenue and book the perfect space.
+
+
+
+ } label="Sign up with Google" />
+ } label="Sign up with Apple" />
+ } label="Sign up with Facebook" />
+
+
+
or
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Already have an account?{" "}
+
+ Sign in
+
+
+
+ );
+}
+
+/* ---------- Modal shell ---------- */
+function AuthModal({ open, onClose, initialMode = "login" }) {
+ const [mode, setMode] = useState(initialMode);
+
+ useEffect(() => {
+ if (open) setMode(initialMode);
+ }, [open, initialMode]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onKey = (e) => e.key === "Escape" && onClose?.();
+ document.addEventListener("keydown", onKey);
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.removeEventListener("keydown", onKey);
+ document.body.style.overflow = "";
+ };
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+
+
+ {mode === "login" ? (
+
setMode("register")} onClose={onClose} />
+ ) : (
+ setMode("login")} onClose={onClose} />
+ )}
+
+
+ By continuing you agree to our Terms and Privacy Policy.
+
+
+
+ );
+}
+
+export default AuthModal;
diff --git a/bookmyvenue/apps/Frontend/src/pages/Home.jsx b/bookmyvenue/apps/Frontend/src/pages/Home.jsx
new file mode 100644
index 0000000000..d9b9e9a7fd
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/Home.jsx
@@ -0,0 +1,244 @@
+import { useEffect, useState } from "react";
+
+import Header from "../components/common/Header";
+import Footer from "../components/common/Footer";
+import AuthModal from "../pages/AuthModal";
+
+import Hero from "../components/home/Hero";
+import SearchBar from "../components/home/SearchBar";
+import SearchResults from "../components/home/SearchResults";
+import HomepageSections from "../components/home/HomepageSections";
+
+import { useAuth } from "../context/AuthContext";
+import { getCategories } from "../api/categories";
+import { getHomepageData, getVenues } from "../api/venues";
+
+function Home() {
+ const { token, currentUser } = useAuth();
+
+ const [authOpen, setAuthOpen] = useState(false);
+ const [authMode, setAuthMode] = useState("login");
+
+ const [searchText, setSearchText] = useState("");
+ const [city, setCity] = useState("");
+ const [categoryId, setCategoryId] = useState("");
+ const [availabilityType, setAvailabilityType] = useState("");
+
+ const [categories, setCategories] = useState([]);
+
+ const [venues, setVenues] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [searchError, setSearchError] = useState("");
+
+ const [homepageLoading, setHomepageLoading] = useState(true);
+ const [homepageError, setHomepageError] = useState("");
+
+ const [topRatedVenues, setTopRatedVenues] = useState([]);
+ const [recentlyAddedVenues, setRecentlyAddedVenues] = useState([]);
+ const [categorySections, setCategorySections] = useState([]);
+
+ const [visibleTopRated, setVisibleTopRated] = useState(4);
+ const [visibleRecent, setVisibleRecent] = useState(4);
+ const [visibleCategoryCounts, setVisibleCategoryCounts] = useState({});
+
+ const [isSearchActive, setIsSearchActive] = useState(false);
+
+ const limit = 16;
+
+ const openLogin = () => {
+ setAuthMode("login");
+ setAuthOpen(true);
+ };
+
+ const openRegister = () => {
+ setAuthMode("register");
+ setAuthOpen(true);
+ };
+
+ useEffect(() => {
+ loadCategories();
+ loadHomepage();
+ }, []);
+
+ useEffect(() => {
+ const cleared =
+ searchText.trim() === "" &&
+ city.trim() === "" &&
+ categoryId === "" &&
+ availabilityType === "";
+
+ if (cleared) {
+ setIsSearchActive(false);
+ setVenues([]);
+ setTotal(0);
+ setCurrentPage(1);
+ }
+ }, [searchText, city, categoryId, availabilityType]);
+
+ async function loadCategories() {
+ try {
+ const data = await getCategories();
+ setCategories(data);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ async function loadHomepage() {
+ try {
+ const data = await getHomepageData();
+
+ setTopRatedVenues(data.top_rated || []);
+ setRecentlyAddedVenues(data.recently_added || []);
+ setCategorySections(data.categories || []);
+ } catch (err) {
+ setHomepageError("Failed to load homepage.");
+ } finally {
+ setHomepageLoading(false);
+ }
+ }
+
+ function buildSearchParams(page) {
+ const params = {
+ page,
+ limit,
+ };
+
+ if (searchText.trim()) params.search = searchText.trim();
+ if (city.trim()) params.city = city.trim();
+ if (categoryId) params.category_id = categoryId;
+ if (availabilityType)
+ params.availability_type = availabilityType;
+
+ return params;
+ }
+
+ function extractVenues(data) {
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data.venues)) return data.venues;
+ if (Array.isArray(data.items)) return data.items;
+ return [];
+ }
+
+ function extractTotal(data) {
+ if (typeof data.total === "number") return data.total;
+ if (typeof data.count === "number") return data.count;
+ if (Array.isArray(data)) return data.length;
+ return 0;
+ }
+
+ async function fetchSearchResults(page) {
+ try {
+ setSearchLoading(true);
+ setSearchError("");
+
+ const data = await getVenues(buildSearchParams(page));
+
+ setVenues(extractVenues(data));
+ setTotal(extractTotal(data));
+ setCurrentPage(page);
+ } catch (err) {
+ setSearchError("Failed to fetch venues.");
+ } finally {
+ setSearchLoading(false);
+ }
+ }
+
+ function handleSearch() {
+ const hasFilters =
+ searchText.trim() ||
+ city.trim() ||
+ categoryId ||
+ availabilityType;
+
+ if (!hasFilters) {
+ setIsSearchActive(false);
+ return;
+ }
+
+ setIsSearchActive(true);
+ fetchSearchResults(1);
+ }
+
+ function handlePageClick(page) {
+ fetchSearchResults(page);
+ }
+
+ function handleLoadMore(type, categoryId) {
+ if (type === "topRated") {
+ setVisibleTopRated((prev) => prev + 4);
+ } else if (type === "recentlyAdded") {
+ setVisibleRecent((prev) => prev + 4);
+ } else if (type === "category") {
+ setVisibleCategoryCounts((prev) => ({
+ ...prev,
+ [categoryId]: (prev[categoryId] || 4) + 4,
+ }));
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {!isSearchActive && }
+
+
+
+ {isSearchActive ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ setAuthOpen(false)}
+ initialMode={authMode}
+ />
+ >
+ );
+}
+
+export default Home;
diff --git a/bookmyvenue/apps/Frontend/src/pages/Login.jsx b/bookmyvenue/apps/Frontend/src/pages/Login.jsx
new file mode 100644
index 0000000000..b5c3fa5a6d
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/Login.jsx
@@ -0,0 +1,19 @@
+import Header from "../components/common/Header";
+import Footer from "../components/common/Footer";
+import LoginForm from "../components/auth/LoginForm";
+
+function Login() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/Register.jsx b/bookmyvenue/apps/Frontend/src/pages/Register.jsx
new file mode 100644
index 0000000000..5914e90a37
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/Register.jsx
@@ -0,0 +1,19 @@
+import Header from "../components/common/Header";
+import Footer from "../components/common/Footer";
+import RegisterForm from "../components/auth/RegisterForm";
+
+function Register() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Register;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/admin/AdminDashboard.jsx b/bookmyvenue/apps/Frontend/src/pages/admin/AdminDashboard.jsx
new file mode 100644
index 0000000000..500f5b424c
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/admin/AdminDashboard.jsx
@@ -0,0 +1,531 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import {
+ CalendarDays, Clock, IndianRupee, User, Building2, CheckCircle, XCircle, Hourglass,
+ Shield, Users, MapPin, Activity, TrendingUp, Filter, Search, LogOut, Menu, X
+} from "lucide-react";
+
+import { useAuth } from "../../context/AuthContext";
+import {
+ getAdminStats,
+ getAdminUsers,
+ getAllVenuesAdmin,
+ getAllBookingsAdmin,
+ updateVenueStatus,
+ updateCategoryStatus,
+} from "../../api/admin";
+import { getCategories } from "../../api/categories";
+
+const TABS = [
+ { id: "overview", label: "Overview", icon: Activity },
+ { id: "users", label: "Users", icon: Users },
+ { id: "venues", label: "Venues", icon: Building2 },
+ { id: "bookings", label: "Bookings", icon: CalendarDays },
+];
+
+function StatCard({ icon: Icon, label, value, color }) {
+ return (
+
+ );
+}
+
+function Badge({ children, color = "gray" }) {
+ const colors = {
+ green: "bg-green-50 text-green-700 border-green-200",
+ red: "bg-red-50 text-red-700 border-red-200",
+ amber: "bg-amber-50 text-amber-700 border-amber-200",
+ blue: "bg-blue-50 text-blue-700 border-blue-200",
+ gray: "bg-gray-50 text-gray-600 border-gray-200",
+ purple: "bg-purple-50 text-purple-700 border-purple-200",
+ };
+ return (
+
+ {children}
+
+ );
+}
+
+function AdminDashboard() {
+ const navigate = useNavigate();
+ const { token, logout } = useAuth();
+
+ const [activeTab, setActiveTab] = useState("overview");
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ // Data
+ const [stats, setStats] = useState(null);
+ const [usersData, setUsersData] = useState({ items: [], total: 0, page: 1, pages: 0 });
+ const [venuesData, setVenuesData] = useState({ items: [], total: 0, page: 1, pages: 0 });
+ const [bookingsData, setBookingsData] = useState({ items: [], total: 0, page: 1, pages: 0 });
+ const [categories, setCategories] = useState([]);
+
+ // Filters
+ const [userFilter, setUserFilter] = useState("");
+ const [venueFilter, setVenueFilter] = useState("");
+ const [bookingFilter, setBookingFilter] = useState("");
+
+ // Pages
+ const [userPage, setUserPage] = useState(1);
+ const [venuePage, setVenuePage] = useState(1);
+ const [bookingPage, setBookingPage] = useState(1);
+
+ const LIMIT = 20;
+
+ async function loadUsers() {
+ try {
+ const data = await getAdminUsers(token, userFilter || null, userPage, LIMIT);
+ setUsersData(data);
+ } catch (err) { console.error(err); }
+ }
+
+ async function loadVenues() {
+ try {
+ const data = await getAllVenuesAdmin(token, venueFilter || null, venuePage, LIMIT);
+ setVenuesData(data);
+ } catch (err) { console.error(err); }
+ }
+
+ async function loadBookings() {
+ try {
+ const data = await getAllBookingsAdmin(token, bookingFilter || null, bookingPage, LIMIT);
+ setBookingsData(data);
+ } catch (err) { console.error(err); }
+ }
+
+ async function loadAll() {
+ try {
+ setLoading(true);
+ const [statsData, catsData] = await Promise.all([
+ getAdminStats(token),
+ getCategories(),
+ ]);
+ setStats(statsData);
+ setCategories(catsData);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => { if (token) loadAll(); }, [token]);
+ useEffect(() => { if (token) loadUsers(); }, [token, userFilter, userPage]);
+ useEffect(() => { if (token) loadVenues(); }, [token, venueFilter, venuePage]);
+ useEffect(() => { if (token) loadBookings(); }, [token, bookingFilter, bookingPage]);
+
+ function goToPage(setter, page, pages) {
+ if (page < 1 || page > pages) return;
+ setter(page);
+ }
+
+ function Pagination({ page, pages, setter }) {
+ if (pages <= 1) return null;
+ return (
+
+ goToPage(setter, page - 1, pages)}
+ disabled={page <= 1}
+ className="rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed"
+ >
+ Previous
+
+
+ Page {page} of {pages}
+
+ goToPage(setter, page + 1, pages)}
+ disabled={page >= pages}
+ className="rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+ );
+ }
+
+ async function handleLogout() {
+ logout();
+ navigate("/login");
+ }
+
+ return (
+
+ {/* Top Bar */}
+
+
+
+ {/* Desktop Tabs */}
+
+ {TABS.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`flex items-center gap-2 rounded-2xl px-6 py-3 text-sm font-bold transition-all ${
+ activeTab === tab.id
+ ? "bg-red-600 text-white shadow-md"
+ : "bg-white text-gray-600 shadow-sm hover:bg-gray-100"
+ }`}
+ >
+
+ {tab.label}
+
+ ))}
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {/* === OVERVIEW === */}
+ {activeTab === "overview" && stats && (
+
+
+
Platform Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Booking breakdown */}
+
+
Bookings by Type
+
+
+
+ Hourly
+
+ {stats.hourly_bookings}
+
+
+
+ Daily
+
+ {stats.daily_bookings}
+
+
+
+ Total
+ {stats.total_bookings}
+
+
+
+
+ {/* Status breakdown */}
+
+
Bookings by Status
+
+
+
+ Pending
+
+ {stats.pending_bookings}
+
+
+
+ Confirmed
+
+ {stats.confirmed_bookings}
+
+
+
+ Rejected
+
+ {stats.rejected_bookings}
+
+
+
+
+ Paid
+
+ {stats.paid_bookings}
+
+
+
+ Unpaid
+
+ {stats.unpaid_bookings}
+
+
+
+
+
+ )}
+
+ {/* === USERS === */}
+ {activeTab === "users" && (
+
+
+
+ {userFilter ? `${userFilter.charAt(0).toUpperCase() + userFilter.slice(1)}s` : "All Users"}
+ ({usersData.total})
+
+
+ {["", "booker", "owner", "admin"].map((role) => (
+ { setUserFilter(role); setUserPage(1); }}
+ className={`rounded-xl px-4 py-2 text-xs font-bold uppercase tracking-wide transition ${
+ userFilter === role
+ ? "bg-red-600 text-white"
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
+ }`}
+ >
+ {role || "All"}
+
+ ))}
+
+
+
+
+
+
+
+
+ Name
+ Email
+ Role
+ Joined
+
+
+
+ {usersData.items.map((user) => (
+
+
+
+
+ {user.name.charAt(0).toUpperCase()}
+
+
{user.name}
+
+
+ {user.email}
+
+
+ {user.role}
+
+
+
+ {new Date(user.created_at).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}
+
+
+ ))}
+
+
+
+ {usersData.items.length === 0 && (
+
No users found.
+ )}
+
+
+
+ )}
+
+ {/* === VENUES === */}
+ {activeTab === "venues" && (
+
+
+
+ All Venues ({venuesData.total})
+
+
+ {["", "active", "inactive"].map((s) => (
+ { setVenueFilter(s); setVenuePage(1); }}
+ className={`rounded-xl px-4 py-2 text-xs font-bold uppercase tracking-wide transition ${
+ venueFilter === s
+ ? "bg-red-600 text-white"
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
+ }`}
+ >
+ {s || "All"}
+
+ ))}
+
+
+
+
+ {venuesData.items.map((venue) => (
+
+
+
{venue.name}
+ {venue.status}
+
+
+
+
+ {venue.city}, {venue.address_line?.slice(0, 30)}
+
+
+
+ Owner ID: {venue.owner_id}
+
+
+
+ Capacity: {venue.capacity}
+
+
+
updateVenueStatus(venue.id, venue.status === "active" ? "inactive" : "active", token).then(loadAll)}
+ className={`mt-5 w-full rounded-xl py-2.5 text-xs font-bold uppercase tracking-wide transition ${
+ venue.status === "active"
+ ? "border border-red-200 bg-red-50 text-red-600 hover:bg-red-100"
+ : "bg-green-600 text-white hover:bg-green-700"
+ }`}
+ >
+ {venue.status === "active" ? "Deactivate" : "Activate"}
+
+
+ ))}
+ {venuesData.items.length === 0 && (
+
No venues found.
+ )}
+
+
+
+ )}
+
+ {/* === BOOKINGS === */}
+ {activeTab === "bookings" && (
+
+
+
+ All Bookings ({bookingsData.total})
+
+
+ {["", "pending", "confirmed", "rejected"].map((s) => (
+ { setBookingFilter(s); setBookingPage(1); }}
+ className={`rounded-xl px-4 py-2 text-xs font-bold uppercase tracking-wide transition ${
+ bookingFilter === s
+ ? "bg-red-600 text-white"
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
+ }`}
+ >
+ {s || "All"}
+
+ ))}
+
+
+
+
+ {bookingsData.items.map((booking) => (
+
+
+
+
+
+ {booking.venue_name || "Venue #" + booking.venue_id}
+
+
+ {booking.status}
+
+
+
+
+
+ {booking.booker_name || "Booker #" + booking.booker_id}
+
+
+
+ {booking.booking_type}
+
+
+
+ {booking.total_amount}
+
+
+ {booking.payment_status}
+
+
+
+
+ {booking.slots?.length > 0 && (
+
+ {booking.slots.map((slot) => (
+
+ {slot.date ? new Date(slot.date + "T00:00:00").toLocaleDateString("en-IN", { weekday: "short", day: "numeric", month: "short" }) : ""}
+ {slot.start_time ? ` ${slot.start_time.slice(0, 5)}-${slot.end_time?.slice(0, 5)}` : " Full Day"}
+
+ ))}
+
+ )}
+
+ {new Date(booking.created_at).toLocaleString("en-IN")}
+
+
+ ))}
+ {bookingsData.items.length === 0 && (
+
No bookings found.
+ )}
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+export default AdminDashboard;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/dashboard/BookerDashboard.jsx b/bookmyvenue/apps/Frontend/src/pages/dashboard/BookerDashboard.jsx
new file mode 100644
index 0000000000..18c334f752
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/dashboard/BookerDashboard.jsx
@@ -0,0 +1,281 @@
+
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { CalendarDays, Clock, IndianRupee } from 'lucide-react';
+import Logo from '../../components/common/Logo';
+
+import { getMyBookings } from '../../api/bookings';
+import { useAuth } from '../../context/AuthContext';
+
+function BookerDashboard() {
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+ const [bookings, setBookings] = useState([]);
+ const [visibleCount, setVisibleCount] = useState(5);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ async function loadBookings() {
+ try {
+ setLoading(true);
+ setError('');
+ const data = await getMyBookings(token);
+ setBookings(data);
+ } catch (err) {
+ setError(err.response?.data?.detail || 'Failed to load bookings');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (token) {
+ loadBookings();
+ }
+ }, [token]);
+
+ function getStatusStyle(status) {
+ if (status === 'confirmed') {
+ return 'bg-green-50 text-green-700 border border-green-200';
+ }
+ if (status === 'rejected') {
+ return 'bg-red-50 text-red-700 border border-red-200';
+ }
+ return 'bg-amber-50 text-amber-700 border border-amber-200';
+ }
+
+ function formatStatus(status) {
+ if (!status) return '';
+ return status.charAt(0).toUpperCase() + status.slice(1);
+ }
+
+ function formatTimeOnly(timeString) {
+ if (!timeString) return '';
+ return timeString.slice(0, 5);
+ }
+
+ if (loading) {
+ return (
+
+
+
+
Loading your bookings...
+
+
+ );
+ }
+
+ return (
+
+ {/* Header bar */}
+
+
+
+ {/* Page title */}
+
+
+ My bookings
+
+
+ Review and manage all venues you have booked in one place.
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Empty state */}
+ {bookings.length === 0 && !error && (
+
+
+
+
+
+ No bookings yet
+
+
+ Start exploring and book your ideal venue today.
+
+
navigate('/')}
+ className='mt-6 rounded-full bg-red-600 px-8 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-red-700'
+ >
+ Browse venues
+
+
+ )}
+
+ {/* Bookings list */}
+
+ {bookings.slice(0, visibleCount).map((booking) => (
+
+ {/* Top row */}
+
+
+
+ {booking.venue_name || ('Venue #' + booking.venue_id)}
+
+
+ {booking.booking_type}
+
+ {booking.payment_status}
+
+
+
+
+ {formatStatus(booking.status)}
+
+
+
+ {/* Body */}
+
+ {/* Price breakdown */}
+
+
+
+ Price breakdown
+
+
+
+ Base price
+
+ ₹{booking.base_price}
+
+
+
+ Tax
+
+ ₹{booking.tax_amount}
+
+
+
+ Platform fee
+
+ ₹{booking.platform_fee}
+
+
+
+
+ Total
+
+ ₹{booking.total_amount}
+
+
+
+
+
+ {/* Slots */}
+
+
+
+ Booked slots
+
+ {booking.slots?.length > 0 ? (
+
+ {booking.slots.map((slot) => {
+ const slotDate = slot.date ? new Date(slot.date + 'T00:00:00') : null;
+ const dayNum = slotDate ? slotDate.toLocaleDateString('en-IN', { day: 'numeric' }) : '';
+ const monthStr = slotDate ? slotDate.toLocaleDateString('en-IN', { month: 'short' }) : '';
+ const timeDisplay =
+ slot.booking_type === 'daily'
+ ? 'Full Day'
+ : formatTimeOnly(slot.start_time) + ' - ' + formatTimeOnly(slot.end_time);
+
+ return (
+
+ {/* Date badge */}
+
+ {dayNum}
+ {monthStr}
+
+
+ {/* Slot details */}
+
+
+
+ {timeDisplay}
+
+
+ {slot.booking_type === 'daily' ? 'Full day booking' : 'Hourly slot'}
+
+
+
+ {/* Booking type chip */}
+
+ {slot.booking_type}
+
+
+ );
+ })}
+
+ ) : (
+
+ No slots recorded.
+
+ )}
+ {booking.slots?.length > 3 && (
+
+ Scroll for all {booking.slots.length} slots
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+ Created on{' '}
+
+ {new Date(booking.created_at).toLocaleDateString('en-IN', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })}
+
+
+
+
+ ))}
+
+
+ {/* Load more */}
+ {visibleCount < bookings.length && (
+
+ setVisibleCount((prev) => prev + 5)}
+ className='inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-6 py-2.5 text-sm font-semibold text-gray-700 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700'
+ >
+ Load more ({visibleCount} of {bookings.length})
+
+
+ )}
+
+
+ );
+}
+
+export default BookerDashboard;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/BookingRequestCard.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/BookingRequestCard.jsx
new file mode 100644
index 0000000000..29493546c9
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/BookingRequestCard.jsx
@@ -0,0 +1,325 @@
+function BookingRequestCard({
+ booking,
+ onStatusUpdate,
+ updatingBookingId,
+}) {
+
+
+ function getStatusStyle(status) {
+
+ if (status === "confirmed") {
+
+ return "bg-green-50 text-green-700 border-green-200";
+
+ }
+
+
+ if (status === "rejected") {
+
+ return "bg-red-50 text-red-700 border-red-200";
+
+ }
+
+
+ return "bg-yellow-50 text-yellow-700 border-yellow-200";
+
+ }
+
+
+
+ const isUpdating =
+ updatingBookingId === booking.id;
+
+
+
+ return (
+
+
+
+
+
+
+
+ {/* Booking Details */}
+
+
+
+
+
+
+
+
+
+ Booking #{booking.id}
+
+
+
+
+
+
+
+ {booking.status}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Venue ID
+
+
+
+ {booking.venue_id}
+
+
+
+
+
+
+
+
+
+
+
+ Booker ID
+
+
+
+ {booking.booker_id}
+
+
+
+
+
+
+
+
+
+
+
+ Booking Type
+
+
+
+ {booking.booking_type}
+
+
+
+
+
+
+
+
+
+
+
+ Total Amount
+
+
+
+ ₹{booking.total_amount}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Created At:{" "}
+
+ {booking.created_at
+ ? new Date(
+ booking.created_at
+ ).toLocaleDateString()
+ : "N/A"
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+ {booking.status === "pending" && (
+
+
+
+
+
+ onStatusUpdate(
+ booking.id,
+ "confirmed"
+ )
+ }
+
+ className="
+ rounded-full
+ bg-green-600
+ px-5
+ py-2
+ text-sm
+ font-semibold
+ text-white
+ transition
+ hover:bg-green-700
+ disabled:cursor-not-allowed
+ disabled:opacity-50
+ "
+
+ >
+
+ {isUpdating
+ ? "Updating..."
+ : "Confirm"
+ }
+
+
+
+
+
+
+
+
+ onStatusUpdate(
+ booking.id,
+ "rejected"
+ )
+ }
+
+ className="
+ rounded-full
+ bg-red-600
+ px-5
+ py-2
+ text-sm
+ font-semibold
+ text-white
+ transition
+ hover:bg-red-700
+ disabled:cursor-not-allowed
+ disabled:opacity-50
+ "
+
+ >
+
+ Reject
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Additional Information */}
+
+
+
+
+
+
+ Payment Status:{" "}
+
+
+
+ {booking.payment_status || "Pending"}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+}
+
+
+export default BookingRequestCard;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/CreateVenue.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/CreateVenue.jsx
new file mode 100644
index 0000000000..e3e699fc97
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/CreateVenue.jsx
@@ -0,0 +1,131 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import Loading from "../../components/common/Loading";
+
+import VenueForm from "../../components/venue/form/VenueForm";
+
+import { useAuth } from "../../context/AuthContext";
+
+import { createVenue } from "../../api/venues";
+import { getCategories } from "../../api/categories";
+
+function CreateVenue() {
+ const navigate = useNavigate();
+
+ const { token } = useAuth();
+
+ const [categories, setCategories] =
+ useState([]);
+
+ const [loading, setLoading] =
+ useState(true);
+
+ const [saving, setSaving] =
+ useState(false);
+
+ const [error, setError] =
+ useState("");
+
+ useEffect(() => {
+ async function loadCategories() {
+ try {
+ const data =
+ await getCategories();
+
+ setCategories(data);
+ } catch (err) {
+ console.error(err);
+
+ setError(
+ "Unable to load categories."
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ loadCategories();
+ }, []);
+
+ async function handleCreate(
+ formData
+ ) {
+ try {
+ setSaving(true);
+
+ await createVenue(
+ formData,
+ token
+ );
+
+ navigate(
+ "/owner/dashboard",
+ { replace: true }
+ );
+ } catch (err) {
+ console.error("Create venue error:", err);
+
+ const message =
+ err?.response?.data?.detail ||
+ err?.message ||
+ JSON.stringify(err);
+
+ alert(message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Create Venue
+
+
+
+ Fill in the details below to
+ publish your venue.
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default CreateVenue;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/DashboardStats.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/DashboardStats.jsx
new file mode 100644
index 0000000000..76428d2bd1
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/DashboardStats.jsx
@@ -0,0 +1,135 @@
+function DashboardStats({
+ venues = [],
+ bookings = [],
+}) {
+
+
+ const totalVenues = venues.length;
+
+
+ const totalBookings = bookings.length;
+
+
+ const pendingBookings = bookings.filter(
+ (booking) =>
+ booking.status === "pending"
+ ).length;
+
+
+
+ const confirmedBookings = bookings.filter(
+ (booking) =>
+ booking.status === "confirmed"
+ ).length;
+
+
+
+ const stats = [
+ {
+ title: "Total Venues",
+ value: totalVenues,
+ description: "Your listed venues",
+ },
+ {
+ title: "Total Bookings",
+ value: totalBookings,
+ description: "All booking requests",
+ },
+ {
+ title: "Pending Requests",
+ value: pendingBookings,
+ description: "Need your action",
+ },
+ {
+ title: "Confirmed",
+ value: confirmedBookings,
+ description: "Approved bookings",
+ },
+ ];
+
+
+
+ return (
+
+
+
+ {stats.map((stat) => (
+
+
+
+
+
+
+ {stat.title}
+
+
+
+
+
+
+
+ {stat.value}
+
+
+
+
+
+
+
+ {stat.description}
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+
+}
+
+
+export default DashboardStats;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/EditVenue.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/EditVenue.jsx
new file mode 100644
index 0000000000..8aacf50591
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/EditVenue.jsx
@@ -0,0 +1,147 @@
+import { useState, useEffect } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import Loading from "../../components/common/Loading";
+
+import VenueForm from "../../components/venue/form/VenueForm";
+
+import { useAuth } from "../../context/AuthContext";
+
+import {
+ getVenueById,
+ updateVenue,
+} from "../../api/venues";
+
+import { getCategories } from "../../api/categories";
+
+function EditVenue() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+ const [venue, setVenue] = useState(null);
+ const [categories, setCategories] = useState([]);
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] =useState(false);
+
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ async function loadData() {
+ try {
+ const [venueData, categoryData] = await Promise.all([
+ getVenueById(id),
+ getCategories(),
+ ]);
+
+ setVenue({
+ ...venueData,
+ amenities: venueData.amenities
+ ? venueData.amenities
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean)
+ : [],
+ image_urls: venueData.image_urls || [],
+ });
+
+ setCategories(categoryData);
+ } catch (err) {
+ console.error(err);
+ setError("Unable to load venue.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ loadData();
+ }, [id]);
+
+ async function handleUpdate(formData) {
+ try {
+ setSaving(true);
+
+ await updateVenue(id, formData, token);
+
+ navigate("/owner/dashboard");
+ } catch (err) {
+ console.error(err);
+
+ alert(
+ err.response?.data?.detail ||
+ "Unable to update venue."
+ );
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!venue) {
+ return (
+ <>
+
+
+
+
+ Venue not found.
+
+
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Edit Venue
+
+
+
+ Update your venue information.
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default EditVenue;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/ManageAvailability.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/ManageAvailability.jsx
new file mode 100644
index 0000000000..95d8f307bf
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/ManageAvailability.jsx
@@ -0,0 +1,524 @@
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { CalendarDays, Clock, Trash2, Plus, CheckCircle, AlertCircle } from "lucide-react";
+
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import Loading from "../../components/common/Loading";
+
+import {
+ getAvailability,
+ createAvailability,
+ deleteAvailability,
+} from "../../api/availability";
+
+import { getVenueById } from "../../api/venues";
+import { useAuth } from "../../context/AuthContext";
+
+const DATE_PRESETS = [
+ { label: "Today", delta: 0 },
+ { label: "Tomorrow", delta: 1 },
+ { label: "Next 3 days", delta: 3 },
+ { label: "Next 7 days", delta: 7 },
+ { label: "Next 14 days", delta: 14 },
+];
+
+function formatDateString(date) {
+ return date.toISOString().split("T")[0];
+}
+
+function addDays(date, days) {
+ const result = new Date(date);
+ result.setDate(result.getDate() + days);
+ return result;
+}
+
+function ManageAvailability() {
+ const { id } = useParams();
+ const { token } = useAuth();
+
+ const [venue, setVenue] = useState(null);
+ const [availability, setAvailability] = useState([]);
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [visibleCount, setVisibleCount] = useState(10);
+ const [successMsg, setSuccessMsg] = useState("");
+ const [errorMsg, setErrorMsg] = useState("");
+
+ // Form state
+ const [selectedDates, setSelectedDates] = useState([""]);
+ const [bookingType, setBookingType] = useState("hourly");
+ const [slots, setSlots] = useState([{ start_time: "08:00", end_time: "09:00" }]);
+
+ useEffect(() => {
+ loadData();
+ }, [id]);
+
+ useEffect(() => {
+ if (!venue) return;
+ // Default to hourly if supported, else daily
+ if (venue.supports_hourly) {
+ setBookingType("hourly");
+ } else if (venue.supports_daily) {
+ setBookingType("daily");
+ }
+ }, [venue]);
+
+ async function loadData() {
+ try {
+ setLoading(true);
+ const [venueData, availabilityData] = await Promise.all([
+ getVenueById(id),
+ getAvailability(id),
+ ]);
+ setVenue(venueData);
+ setAvailability(availabilityData);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function getSupportedTypes() {
+ if (!venue) return ["hourly"];
+ const types = [];
+ if (venue.supports_hourly) types.push("hourly");
+ if (venue.supports_daily) types.push("daily");
+ return types;
+ }
+
+ // Multiple date helpers
+ function addDateField() {
+ setSelectedDates([...selectedDates, ""]);
+ }
+
+ function removeDateField(index) {
+ if (selectedDates.length <= 1) return;
+ setSelectedDates(selectedDates.filter((_, i) => i !== index));
+ }
+
+ function updateDateField(index, value) {
+ const updated = [...selectedDates];
+ updated[index] = value;
+ setSelectedDates(updated);
+ }
+
+ function applyPreset(delta) {
+ const today = new Date();
+ const dates = [];
+ for (let i = 0; i < delta; i++) {
+ dates.push(formatDateString(addDays(today, i)));
+ }
+ if (delta === 0 || dates.length === 0) {
+ dates.push(formatDateString(today));
+ }
+ setSelectedDates(dates);
+ }
+
+ // Hourly slot helpers
+ function addSlot() {
+ setSlots([...slots, { start_time: "08:00", end_time: "09:00" }]);
+ }
+
+ function updateSlot(index, field, value) {
+ const updated = [...slots];
+ updated[index][field] = value;
+ setSlots(updated);
+ }
+
+ function removeSlot(index) {
+ if (slots.length <= 1) return;
+ setSlots(slots.filter((_, i) => i !== index));
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setSuccessMsg("");
+ setErrorMsg("");
+
+ const validDates = selectedDates.filter((d) => d.trim() !== "");
+ if (validDates.length === 0) {
+ setErrorMsg("Select at least one date.");
+ return;
+ }
+
+ // Frontend validation: check for overlapping or invalid hourly slots
+ if (bookingType === "hourly") {
+ for (let i = 0; i < slots.length; i++) {
+ const slot = slots[i];
+ if (slot.start_time && slot.end_time && slot.start_time >= slot.end_time) {
+ setErrorMsg(`Slot #${i + 1}: End time must be after start time.`);
+ return;
+ }
+ }
+
+ if (slots.length > 1) {
+ for (let i = 0; i < slots.length; i++) {
+ for (let j = i + 1; j < slots.length; j++) {
+ const a = slots[i];
+ const b = slots[j];
+ if (a.start_time && a.end_time && b.start_time && b.end_time) {
+ if (a.start_time < b.end_time && a.end_time > b.start_time) {
+ setErrorMsg(
+ `Slots #${i + 1} (${a.start_time.slice(0, 5)}-${a.end_time.slice(0, 5)}) and #${j + 1} (${b.start_time.slice(0, 5)}-${b.end_time.slice(0, 5)}) overlap.`
+ );
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ try {
+ setSaving(true);
+ let created = 0;
+ let errors = [];
+
+ for (const date of validDates) {
+ const payload = {
+ venue_id: Number(id),
+ date,
+ booking_type: bookingType,
+ };
+
+ if (bookingType === "hourly") {
+ payload.slots = slots;
+ }
+
+ try {
+ await createAvailability(payload, token);
+ created++;
+ } catch (err) {
+ const detail = err?.response?.data?.detail || "Unknown error";
+ errors.push(`${date}: ${detail}`);
+ }
+ }
+
+ if (created > 0) {
+ setSuccessMsg(`Availability saved for ${created} date(s).`);
+ setSelectedDates([""]);
+ setSlots([{ start_time: "08:00", end_time: "09:00" }]);
+ loadData();
+ }
+
+ if (errors.length > 0) {
+ setErrorMsg(errors[0]);
+ }
+ } catch (err) {
+ console.error(err);
+ setErrorMsg(err?.response?.data?.detail || "Unable to save availability.");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handleDeleteSlot(slotId) {
+ const confirmed = window.confirm("Delete this availability slot?");
+ if (!confirmed) return;
+ try {
+ await deleteAvailability(slotId, token);
+ setSuccessMsg("Slot deleted.");
+ loadData();
+ } catch (err) {
+ setErrorMsg(err.response?.data?.detail || "Unable to delete slot.");
+ }
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ const supportedTypes = getSupportedTypes();
+
+ return (
+ <>
+
+
+
+
+
+ {/* Header */}
+
+
+ Manage Availability
+
+
+ {venue?.name} — Set which dates and time slots are available for booking.
+
+
+
+ {/* Success / Error Messages */}
+ {successMsg && (
+
+
+ {successMsg}
+
+ )}
+ {errorMsg && (
+
+ )}
+
+ {/* Availability Form */}
+
+
+ {/* Existing Availability */}
+
+
+
+
+ Existing Availability
+
+
+ {availability.length} date{availability.length !== 1 ? "s" : ""}
+
+
+
+ {availability.length === 0 ? (
+
+
+
+ No availability set yet. Use the form above to add dates.
+
+
+ ) : (
+
+ {availability.slice(0, visibleCount).map((day, index) => (
+
+
+
+
+
+ {new Date(day.date + "T00:00:00").getDate()}
+
+
+ {new Date(day.date + "T00:00:00").toLocaleDateString("en-IN", { month: "short" })}
+
+
+
+ {new Date(day.date + "T00:00:00").toLocaleDateString("en-IN", { weekday: "short", year: "numeric" })}
+
+
+ {day.booking_type}
+
+
+
+ {day.slots?.length || 1} slot{(day.slots?.length || 1) > 1 ? "s" : ""}
+
+
+
+ {day.booking_type === "daily" ? (
+
+
+
+ Available — Full Day
+
+ {day.slots?.[0] && !day.slots[0].is_booked && (
+ handleDeleteSlot(day.slots[0].id)}
+ className="flex items-center gap-1.5 rounded-lg border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-semibold text-red-600 transition hover:bg-red-100"
+ >
+
+ Delete
+
+ )}
+
+ ) : (
+
+ {day.slots.map((slot) => (
+
+
+
+
+ {slot.start_time?.slice(0, 5)} — {slot.end_time?.slice(0, 5)}
+
+ {slot.is_booked && (
+
+ Booked
+
+ )}
+
+ {!slot.is_booked && (
+
handleDeleteSlot(slot.id)}
+ className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1 text-xs font-semibold text-gray-500 transition hover:border-red-200 hover:bg-red-50 hover:text-red-600"
+ >
+
+ Delete
+
+ )}
+
+ ))}
+
+ )}
+
+ ))}
+
+ {visibleCount < availability.length && (
+
+ setVisibleCount((prev) => prev + 10)}
+ className="inline-flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-6 py-2.5 text-sm font-semibold text-gray-700 shadow-sm transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
+ >
+ Load more ({visibleCount} of {availability.length})
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ManageAvailability;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/ManageVenue.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/ManageVenue.jsx
new file mode 100644
index 0000000000..5f480204b4
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/ManageVenue.jsx
@@ -0,0 +1,153 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import Loading from "../../components/common/Loading";
+
+import { useAuth } from "../../context/AuthContext";
+
+import {
+ getMyVenues,
+ deleteVenue,
+} from "../../api/venues";
+
+function ManageVenue() {
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+ const [venues, setVenues] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadVenues();
+ }, []);
+
+ async function loadVenues() {
+ try {
+ setLoading(true);
+
+ const data = await getMyVenues(token);
+
+ setVenues(data);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleDelete(id) {
+ alert("Deletion is not available yet.");
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ My Venues
+
+
+
+ navigate("/owner/create-venue")
+ }
+ className="rounded-lg bg-red-600 px-5 py-2 text-white hover:bg-red-700"
+ >
+ Create Venue
+
+
+
+
+ {venues.length === 0 ? (
+
+ ) : (
+
+
+ {venues.map((venue) => (
+
+
+
+
+
+ {venue.name}
+
+
+
+ {venue.city}
+
+
+
+
+
+
+ navigate(`/owner/edit-venue/${venue.id}`)
+ }
+ className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
+ >
+ Edit
+
+
+
+ navigate(`/owner/availability/${venue.id}`)
+ }
+ className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
+ >
+ Availability
+
+
+
+ navigate("/owner/bookings")
+ }
+ className="rounded bg-yellow-600 px-4 py-2 text-white hover:bg-yellow-700"
+ >
+ Bookings
+
+
+
+ handleDelete(venue.id)
+ }
+ className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
+ >
+ Delete
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ >
+ );
+}
+
+export default ManageVenue;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/OwnerDashboard.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerDashboard.jsx
new file mode 100644
index 0000000000..e293977f5e
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerDashboard.jsx
@@ -0,0 +1,205 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { CalendarDays, Clock, IndianRupee, MapPin, Building2, CheckCircle, Hourglass } from "lucide-react";
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import { getMyVenues } from "../../api/venues";
+import { getOwnerBookings } from "../../api/bookings";
+import { useAuth } from "../../context/AuthContext";
+
+function OwnerDashboard() {
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+ const [stats, setStats] = useState({
+ totalVenues: 0,
+ totalBookings: 0,
+ pending: 0,
+ confirmed: 0,
+ totalRevenue: 0,
+ hourlyCount: 0,
+ dailyCount: 0,
+ });
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function loadStats() {
+ try {
+ setLoading(true);
+ const [venues, bookings] = await Promise.all([
+ getMyVenues(token),
+ getOwnerBookings(token),
+ ]);
+
+ const totalVenues = venues.length;
+ const totalBookings = bookings.length;
+ const pending = bookings.filter((b) => b.status === "pending").length;
+ const confirmed = bookings.filter((b) => b.status === "confirmed").length;
+ const totalRevenue = bookings
+ .filter((b) => b.status === "confirmed")
+ .reduce((sum, b) => sum + (b.total_amount || 0), 0);
+ const hourlyCount = bookings.filter((b) => b.booking_type === "hourly").length;
+ const dailyCount = bookings.filter((b) => b.booking_type === "daily").length;
+
+ setStats({
+ totalVenues,
+ totalBookings,
+ pending,
+ confirmed,
+ totalRevenue,
+ hourlyCount,
+ dailyCount,
+ });
+ } catch (err) {
+ console.error("Failed to load stats", err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (token) {
+ loadStats();
+ }
+ }, [token]);
+
+ const actions = [
+ {
+ title: "Manage Venues",
+ description: "View, edit or delete your venues.",
+ path: "/owner/manage-venues",
+ },
+ {
+ title: "Create Venue",
+ description: "Add a new venue.",
+ path: "/owner/create-venue",
+ },
+ {
+ title: "Manage Bookings",
+ description: "Approve or reject customer bookings.",
+ path: "/owner/bookings",
+ },
+ ];
+
+ const statCards = [
+ {
+ label: "Total Venues",
+ value: stats.totalVenues,
+ icon: Building2,
+ color: "bg-blue-50 text-blue-700",
+ },
+ {
+ label: "Total Bookings",
+ value: stats.totalBookings,
+ icon: CalendarDays,
+ color: "bg-purple-50 text-purple-700",
+ },
+ {
+ label: "Pending",
+ value: stats.pending,
+ icon: Hourglass,
+ color: "bg-amber-50 text-amber-700",
+ },
+ {
+ label: "Confirmed",
+ value: stats.confirmed,
+ icon: CheckCircle,
+ color: "bg-green-50 text-green-700",
+ },
+ {
+ label: "Total Revenue",
+ value: "\u20b9" + stats.totalRevenue.toLocaleString("en-IN"),
+ icon: IndianRupee,
+ color: "bg-red-50 text-red-700",
+ },
+ {
+ label: "Hourly Bookings",
+ value: stats.hourlyCount,
+ icon: Clock,
+ color: "bg-cyan-50 text-cyan-700",
+ },
+ {
+ label: "Daily Bookings",
+ value: stats.dailyCount,
+ icon: CalendarDays,
+ color: "bg-indigo-50 text-indigo-700",
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Owner Dashboard
+
+
+ Overview and quick actions for your venues.
+
+
+
+ {/* Stats grid */}
+
+ {statCards.map((card) => (
+
+
+
+
+ {card.label}
+
+
+ {loading ? "-" : card.value}
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Action cards */}
+
+ {actions.map((action) => (
+
+
+ {action.title}
+
+
+
+ {action.description}
+
+
+
navigate(action.path)}
+ className="mt-6 rounded bg-red-600 px-5 py-2 text-white hover:bg-red-700"
+ >
+ Open
+
+
+ ))}
+
+
+
+
+
+
+ >
+ );
+}
+
+export default OwnerDashboard;
+
+
+
+
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/OwnerHeader.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerHeader.jsx
new file mode 100644
index 0000000000..b2d85f35e1
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerHeader.jsx
@@ -0,0 +1,122 @@
+import { useNavigate } from "react-router-dom";
+
+import { useAuth } from "../../context/AuthContext";
+
+import logo from "../../assets/image.png";
+
+
+function OwnerHeader() {
+
+ const navigate = useNavigate();
+
+ const { logout } = useAuth();
+
+
+
+ function handleLogout() {
+
+ logout();
+
+ navigate("/login");
+
+ }
+
+
+
+ return (
+
+
+
+ );
+
+}
+
+
+export default OwnerHeader;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueBookings.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueBookings.jsx
new file mode 100644
index 0000000000..85ad8a976c
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueBookings.jsx
@@ -0,0 +1,274 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { CalendarDays, Clock, IndianRupee, User, MapPin } from "lucide-react";
+
+import Header from "../../components/common/Header";
+import Footer from "../../components/common/Footer";
+import Loading from "../../components/common/Loading";
+
+import {
+ getOwnerBookings,
+ updateBookingStatus,
+} from "../../api/bookings";
+
+import { useAuth } from "../../context/AuthContext";
+
+function OwnerVenueBookings() {
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+ const [bookings, setBookings] = useState([]);
+ const [visibleCount, setVisibleCount] = useState(10);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadBookings();
+ }, []);
+
+ async function loadBookings() {
+ try {
+ setLoading(true);
+ const data = await getOwnerBookings(token);
+ setBookings(data);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function changeStatus(id, status) {
+ try {
+ await updateBookingStatus(id, status, token);
+ loadBookings();
+ } catch (err) {
+ console.error(err);
+ alert(err.response?.data?.detail || "Unable to update booking.");
+ }
+ }
+
+ function getStatusStyle(status) {
+ if (status === "confirmed") return "bg-green-50 text-green-700 border border-green-200";
+ if (status === "rejected") return "bg-red-50 text-red-700 border border-red-200";
+ return "bg-amber-50 text-amber-700 border border-amber-200";
+ }
+
+ function formatStatus(status) {
+ if (!status) return "";
+ return status.charAt(0).toUpperCase() + status.slice(1);
+ }
+
+ function formatTimeOnly(timeString) {
+ if (!timeString) return "";
+ return timeString.slice(0, 5);
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Venue Bookings
+
+
+ Review, approve or reject booking requests for your venues.
+
+
+
+ {bookings.length === 0 ? (
+
+
+
No bookings yet
+
Bookings will appear here when customers book your venues.
+
+ ) : (
+
+ {bookings.slice(0, visibleCount).map((booking) => (
+
+ {/* Top row */}
+
+
+
+ {booking.venue_name || "Venue #" + booking.venue_id}
+
+
+
+
+ {booking.booker_name || "Booker #" + booking.booker_id}
+
+
+ {booking.booking_type}
+
+ {booking.payment_status}
+
+
+
+ {formatStatus(booking.status)}
+
+
+
+ {/* Body */}
+
+ {/* Left column: price & booking details */}
+
+ {/* Price breakdown */}
+
+
+
+ Price breakdown
+
+
+
+ Base price
+ ₹{booking.base_price}
+
+
+ Tax
+ ₹{booking.tax_amount}
+
+
+ Platform fee
+ ₹{booking.platform_fee}
+
+
+
+ Total
+ ₹{booking.total_amount}
+
+
+
+
+ {/* Created date */}
+
+
Booked on
+
+ {new Date(booking.created_at).toLocaleDateString("en-IN", {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+
+ {/* Right column: slots */}
+
+
+
+
+ Booked slots
+
+ {booking.slots?.length > 0 ? (
+
+ {booking.slots.map((slot) => {
+ const slotDate = slot.date ? new Date(slot.date + "T00:00:00") : null;
+ const dayNum = slotDate ? slotDate.toLocaleDateString("en-IN", { day: "numeric" }) : "";
+ const monthStr = slotDate ? slotDate.toLocaleDateString("en-IN", { month: "short" }) : "";
+ const timeDisplay =
+ slot.booking_type === "daily"
+ ? "Full Day"
+ : formatTimeOnly(slot.start_time) + " - " + formatTimeOnly(slot.end_time);
+
+ return (
+
+ {/* Date badge */}
+
+ {dayNum}
+ {monthStr}
+
+
+ {/* Slot details */}
+
+
+
+ {timeDisplay}
+
+
+ {slot.booking_type === "daily" ? "Full day booking" : "Hourly slot"}
+
+
+
+ {/* Booking type chip */}
+
+ {slot.booking_type}
+
+
+ );
+ })}
+
+ ) : (
+
No slots recorded.
+ )}
+ {booking.slots?.length > 3 && (
+
+ Scroll for all {booking.slots.length} slots
+
+ )}
+
+
+
+
+ {/* Action buttons for pending bookings */}
+ {booking.status === "pending" && (
+
+ changeStatus(booking.id, "confirmed")}
+ className="rounded-full bg-green-600 px-6 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700"
+ >
+ Confirm
+
+ changeStatus(booking.id, "rejected")}
+ className="rounded-full border border-red-300 bg-white px-6 py-2 text-sm font-semibold text-red-600 transition hover:bg-red-50"
+ >
+ Reject
+
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Load more */}
+ {visibleCount < bookings.length && (
+
+ setVisibleCount((prev) => prev + 10)}
+ className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-6 py-2.5 text-sm font-semibold text-gray-700 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
+ >
+ Load more ({visibleCount} of {bookings.length})
+
+
+ )}
+
+
+
+
+
+ >
+ );
+}
+
+export default OwnerVenueBookings;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueCard.jsx b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueCard.jsx
new file mode 100644
index 0000000000..42709bcb8a
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/owner/OwnerVenueCard.jsx
@@ -0,0 +1,335 @@
+import { useNavigate } from "react-router-dom";
+
+
+function OwnerVenueCard({ venue }) {
+
+ const navigate = useNavigate();
+
+
+ function getStatusStyle(status) {
+
+ if (status === "active") {
+ return "bg-green-50 text-green-700 border-green-200";
+ }
+
+
+ if (status === "rejected") {
+ return "bg-red-50 text-red-700 border-red-200";
+ }
+
+
+ return "bg-yellow-50 text-yellow-700 border-yellow-200";
+
+ }
+
+
+
+ const image =
+ venue.images && venue.images.length > 0
+ ? venue.images[0].image_url || venue.images[0].url
+ : null;
+
+
+
+ return (
+
+
+
+
+ {/* Image */}
+
+
+
+ {image ? (
+
+
+
+ ) : (
+
+
+
+ No Image
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+ {venue.name}
+
+
+
+
+
+
+ {venue.city}
+
+
+
+
+
+
+
+
+
+
+ {venue.status || "pending"}
+
+
+
+
+
+
+
+
+
+
+ {/* Details */}
+
+
+
+
+
+
+
+ Capacity
+
+
+
+ {venue.capacity}
+
+
+
+
+
+
+
+
+
+
+ Category
+
+
+
+ #{venue.category_id}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Pricing */}
+
+
+
+
+ {venue.supports_hourly && (
+
+
+
+
+ Hourly
+
+
+
+
+
+ ₹{venue.hourly_price}/hr
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {venue.supports_daily && (
+
+
+
+
+ Daily
+
+
+
+
+
+ ₹{venue.daily_price}/day
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ navigate(`/owner/venues/${venue.id}/manage`)
+ }
+ className="
+ rounded-full
+ bg-red-600
+ px-4
+ py-2
+ text-sm
+ font-semibold
+ text-white
+ transition
+ hover:bg-red-700
+ "
+ >
+ Manage Venue
+
+
+
+
+
+
+
+
+
+ navigate(`/owner/venues/${venue.id}/availability`)
+ }
+ className="
+ rounded-full
+ border
+ border-gray-300
+ px-4
+ py-2
+ text-sm
+ font-semibold
+ text-gray-700
+ transition
+ hover:border-red-600
+ hover:text-red-600
+ "
+ >
+ Availability
+
+
+
+
+
+
+ navigate(`/owner/venues/${venue.id}/bookings`)
+ }
+ className="
+ rounded-full
+ border
+ border-gray-300
+ px-4
+ py-2
+ text-sm
+ font-semibold
+ text-gray-700
+ transition
+ hover:border-red-600
+ hover:text-red-600
+ "
+ >
+ Bookings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+}
+
+
+export default OwnerVenueCard;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/BookingPanel.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/BookingPanel.jsx
new file mode 100644
index 0000000000..b5a56cb77f
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/BookingPanel.jsx
@@ -0,0 +1,145 @@
+import { CalendarDays, Clock3 } from "lucide-react";
+import { getTodayDate, formatTime } from "./utils";
+
+function BookingPanel({
+ venue,
+ selectedDate,
+ onDateChange,
+ availabilityGroups,
+ selectedBookingType,
+ onBookingTypeChange,
+ bookingLoading,
+ bookingSummary,
+ bookingError,
+ bookingSuccess,
+ selectedSlotIds,
+ toggleSlot,
+ handleBookNow,
+}) {
+ const currentGroup = availabilityGroups.find(
+ (g) => g.booking_type === selectedBookingType
+ );
+
+ const slots = currentGroup?.slots || [];
+
+ const showTypeSelector =
+ venue.supports_hourly && venue.supports_daily;
+
+ return (
+
+
Book Venue
+
Select a date and your preferred time slot.
+
+ {/* Pricing */}
+
+ {venue.supports_hourly && (
+
+ Hourly
+ ₹{venue.hourly_price}/hr
+
+ )}
+ {venue.supports_daily && (
+
+ Daily
+ ₹{venue.daily_price}/day
+
+ )}
+
+
+ {/* Date */}
+
+
+ Booking Date
+
+
+
+
+ {/* Booking Type Selector */}
+ {showTypeSelector && selectedDate && (
+
+
Booking Type
+
+ {venue.supports_hourly && (
+ onBookingTypeChange("hourly")}
+ className={'flex-1 rounded-xl border-2 px-4 py-3 text-center font-semibold transition ' + (
+ selectedBookingType === "hourly"
+ ? "border-red-600 bg-red-50 text-red-700"
+ : "border-gray-300 text-gray-600 hover:border-red-500"
+ )}>Hourly
+ )}
+ {venue.supports_daily && (
+ onBookingTypeChange("daily")}
+ className={'flex-1 rounded-xl border-2 px-4 py-3 text-center font-semibold transition ' + (
+ selectedBookingType === "daily"
+ ? "border-red-600 bg-red-50 text-red-700"
+ : "border-gray-300 text-gray-600 hover:border-red-500"
+ )}>Daily
+ )}
+
+
+ )}
+
+ {bookingLoading &&
Loading availability...
}
+
+ {/* Slots */}
+ {!bookingLoading && selectedBookingType && slots.length > 0 && (
+
+
Available Slots
+
+ {slots.map((slot) => {
+ const selected = selectedSlotIds.includes(slot.id);
+ const slotLabel = selectedBookingType === "daily"
+ ? "Full Day"
+ : (formatTime(slot.start_time) + " - " + formatTime(slot.end_time));
+ return (
+
toggleSlot(slot)}
+ className={'rounded-xl border p-4 text-left transition ' + (
+ slot.is_booked
+ ? "cursor-not-allowed border-gray-200 bg-gray-100 text-gray-400"
+ : selected
+ ? "border-red-600 bg-red-50 text-red-700"
+ : "border-gray-300 hover:border-red-500 hover:bg-red-50"
+ )}>
+
+ {slotLabel}
+ {slot.is_booked && Booked }
+
+
+ );
+ })}
+
+
+ )}
+
+ {!bookingLoading && selectedBookingType && slots.length === 0 && (
+
No {selectedBookingType} slots available for this date.
+ )}
+
+ {bookingSummary && (
+
+
Booking Summary
+
+
Base Price ₹{bookingSummary.base_price.toFixed(2)}
+
GST ₹{bookingSummary.tax_amount.toFixed(2)}
+
Platform Fee ₹{bookingSummary.platform_fee.toFixed(2)}
+
+
Total ₹{bookingSummary.total_amount.toFixed(2)}
+
+
+ )}
+
+ {bookingError &&
{bookingError}
}
+ {bookingSuccess &&
Booking completed successfully.
}
+
+
+ Book Now
+
+
+ );
+}
+
+export default BookingPanel;
+
+
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/LoginBookingModal.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/LoginBookingModal.jsx
new file mode 100644
index 0000000000..add0232cfb
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/LoginBookingModal.jsx
@@ -0,0 +1,25 @@
+import Modal from "../../components/common/Modal";
+import LoginForm from "../../components/auth/LoginForm";
+
+function LoginBookingModal({
+ isOpen,
+ onClose,
+ onSuccess,
+ onSwitchToRegister,
+}) {
+ return (
+
+
+
+ );
+}
+
+export default LoginBookingModal;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/RegisterBookingModal.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/RegisterBookingModal.jsx
new file mode 100644
index 0000000000..42172cbd84
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/RegisterBookingModal.jsx
@@ -0,0 +1,25 @@
+import Modal from "../../components/common/Modal";
+import RegisterForm from "../../components/auth/RegisterForm";
+
+function RegisterBookingModal({
+ isOpen,
+ onClose,
+ onSuccess,
+ onSwitchToLogin,
+}) {
+ return (
+
+
+
+ );
+}
+
+export default RegisterBookingModal;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/VenueAmenities.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/VenueAmenities.jsx
new file mode 100644
index 0000000000..529acf75ad
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/VenueAmenities.jsx
@@ -0,0 +1,106 @@
+import {
+ CheckCircle2,
+ ShieldCheck,
+ Info,
+} from "lucide-react";
+
+function VenueAmenities({ venue }) {
+ if (!venue) return null;
+
+ const amenities = Array.isArray(venue.amenities)
+ ? venue.amenities
+ : typeof venue.amenities === "string"
+ ? venue.amenities
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean)
+ : [];
+
+ return (
+
+ {/* Amenities */}
+
+
+
+
+
+
+
+
+ Amenities
+
+
+
+ Everything included with this venue.
+
+
+
+
+ {amenities.length > 0 ? (
+
+ {amenities.map((amenity, index) => (
+
+
+
+
+ {amenity}
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ No amenities have been listed for this
+ venue.
+
+
+ )}
+
+
+ {/* Cancellation Policy */}
+
+
+
+
+
+
+
+
+ Cancellation Policy
+
+
+
+ Please review before confirming your
+ booking.
+
+
+
+
+
+
+ {venue.cancellation_policy?.trim()
+ ? venue.cancellation_policy
+ : "No cancellation policy has been provided by the venue owner."}
+
+
+
+
+ );
+}
+
+export default VenueAmenities;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/VenueDetail.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/VenueDetail.jsx
new file mode 100644
index 0000000000..c6ac828ab6
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/VenueDetail.jsx
@@ -0,0 +1,403 @@
+import { useEffect, useState } from "react";
+import { ArrowLeft } from "lucide-react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import { getVenueById } from "../../api/venues";
+import { getAvailability } from "../../api/availability";
+import { createBooking } from "../../api/bookings";
+
+import { useAuth } from "../../context/AuthContext";
+
+import Loading from "../../components/common/Loading";
+
+import VenueGallery from "./VenueGallery";
+import VenueOverview from "./VenueOverview";
+import VenuePricing from "./VenuePricing";
+import VenueAmenities from "./VenueAmenities";
+import BookingPanel from "./BookingPanel";
+import LoginBookingModal from "./LoginBookingModal";
+import RegisterBookingModal from "./RegisterBookingModal";
+
+import { calculateBookingSummary } from "./utils";
+
+function VenueDetail() {
+ const navigate = useNavigate();
+ const { venueId } = useParams();
+
+ const { token } = useAuth();
+
+ const [venue, setVenue] = useState(null);
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const [currentImageIndex, setCurrentImageIndex] =
+ useState(0);
+
+ const [selectedDate, setSelectedDate] =
+ useState("");
+
+ const [availabilityGroups, setAvailabilityGroups] =
+ useState([]);
+
+ const [selectedBookingType, setSelectedBookingType] =
+ useState("");
+
+ const [selectedSlotIds, setSelectedSlotIds] =
+ useState([]);
+
+ const [bookingSummary, setBookingSummary] =
+ useState(null);
+
+ const [bookingLoading, setBookingLoading] =
+ useState(false);
+
+ const [bookingSuccess, setBookingSuccess] =
+ useState(false);
+
+ const [bookingError, setBookingError] =
+ useState("");
+
+ const [showLoginModal, setShowLoginModal] =
+ useState(false);
+
+ const [showRegisterModal, setShowRegisterModal] =
+ useState(false);
+
+ const [pendingBooking, setPendingBooking] =
+ useState(false);
+
+ /**
+ * Load Venue
+ */
+
+ useEffect(() => {
+ async function loadVenue() {
+ try {
+ setLoading(true);
+ setError("");
+
+ const data = await getVenueById(venueId);
+
+ if (!data) {
+ setError("Venue not found.");
+ return;
+ }
+
+ setVenue(data);
+ } catch (err) {
+ console.error(err);
+ setError("Failed to load venue.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (venueId) {
+ loadVenue();
+ }
+ }, [venueId]);
+
+ /**
+ * Load availability
+ */
+
+ async function handleDateChange(e) {
+ const date = e.target.value;
+
+ setSelectedDate(date);
+ setAvailabilityGroups([]);
+ setSelectedBookingType("");
+ setSelectedSlotIds([]);
+ setBookingSummary(null);
+ setBookingError("");
+
+ if (!date) return;
+
+ try {
+ setBookingLoading(true);
+
+ const response =
+ await getAvailability(
+ venueId,
+ date
+ );
+
+ setAvailabilityGroups(response || []);
+
+ if (response && response.length === 1) {
+ setSelectedBookingType(response[0].booking_type);
+ }
+ } catch (err) {
+ console.error(err);
+
+ setBookingError(
+ "Unable to load availability."
+ );
+ } finally {
+ setBookingLoading(false);
+ }
+ }
+
+ function handleBookingTypeChange(type) {
+ setSelectedBookingType(type);
+ setSelectedSlotIds([]);
+ setBookingSummary(null);
+ }
+
+ /**
+ * Slot selection
+ */
+
+ function toggleSlot(slot) {
+ if (slot.is_booked) return;
+
+ if (
+ selectedBookingType ===
+ "daily"
+ ) {
+ setSelectedSlotIds((prev) =>
+ prev.includes(slot.id)
+ ? []
+ : [slot.id]
+ );
+
+ return;
+ }
+
+ setSelectedSlotIds((prev) =>
+ prev.includes(slot.id)
+ ? prev.filter(
+ (id) => id !== slot.id
+ )
+ : [...prev, slot.id]
+ );
+ }
+
+ /**
+ * Booking Summary
+ */
+
+ useEffect(() => {
+ const currentGroup = availabilityGroups.find(
+ (g) => g.booking_type === selectedBookingType
+ );
+
+ setBookingSummary(
+ calculateBookingSummary(
+ venue,
+ currentGroup,
+ selectedSlotIds,
+ selectedBookingType
+ )
+ );
+ }, [
+ venue,
+ availabilityGroups,
+ selectedBookingType,
+ selectedSlotIds,
+ ]);
+
+ /**
+ * Booking
+ */
+
+ async function submitBooking(
+ authToken = token
+ ) {
+ if (!authToken) {
+ setPendingBooking(true);
+ setShowLoginModal(true);
+ return;
+ }
+
+ try {
+ setBookingLoading(true);
+ setBookingError("");
+
+ await createBooking(
+ {
+ venue_id: venue.id,
+ availability_ids:
+ selectedSlotIds,
+ },
+ authToken
+ );
+
+ setBookingSuccess(true);
+
+ setSelectedSlotIds([]);
+ setBookingSummary(null);
+
+ setTimeout(() => {
+ navigate("/dashboard");
+ }, 1500);
+ } catch (err) {
+ console.error(err);
+
+ setBookingError(
+ err.response?.data?.detail ||
+ "Booking failed."
+ );
+ } finally {
+ setBookingLoading(false);
+ }
+ }
+
+ function handleBookNow() {
+ if (selectedSlotIds.length === 0)
+ return;
+
+ if (!token) {
+ setPendingBooking(true);
+ setShowLoginModal(true);
+ return;
+ }
+
+ submitBooking();
+ }
+
+ /**
+ * Loading
+ */
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ /**
+ * Error
+ */
+
+ if (error || !venue) {
+ return (
+
+
+ Venue not found
+
+
+ navigate("/")}
+ className="rounded-xl bg-red-600 px-6 py-3 font-semibold text-white"
+ >
+ Back to Home
+
+
+ );
+ }
+ return (
+ <>
+
+
+
+ {/* Back Button */}
+
+
navigate("/")}
+ className="mb-8 inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium transition hover:bg-gray-100"
+ >
+
+ Back to Home
+
+
+ {/* Gallery */}
+
+
+
+ {/* Content */}
+
+
+
+ {/* Left */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Right */}
+
+
+
+
+
+
+
+ {/* Login */}
+
+ {
+ setShowLoginModal(false);
+ setPendingBooking(false);
+ }}
+ onSuccess={(accessToken) => {
+ setShowLoginModal(false);
+
+ if (pendingBooking) {
+ setPendingBooking(false);
+ submitBooking(accessToken);
+ }
+ }}
+ onSwitchToRegister={() => {
+ setShowLoginModal(false);
+ setShowRegisterModal(true);
+ }}
+ />
+
+ {/* Register */}
+
+ {
+ setShowRegisterModal(false);
+ }}
+ onSuccess={(accessToken) => {
+ setShowRegisterModal(false);
+
+ if (pendingBooking) {
+ setPendingBooking(false);
+ submitBooking(accessToken);
+ }
+ }}
+ onSwitchToLogin={() => {
+ setShowRegisterModal(false);
+ setShowLoginModal(true);
+ }}
+ />
+ >
+ );
+}
+
+export default VenueDetail;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/VenueGallery.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/VenueGallery.jsx
new file mode 100644
index 0000000000..521ee18f58
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/VenueGallery.jsx
@@ -0,0 +1,94 @@
+import { ChevronLeft, ChevronRight, ImageOff } from "lucide-react";
+import { getImageUrl } from "./utils";
+
+function VenueGallery({
+ venue,
+ currentImageIndex,
+ setCurrentImageIndex,
+}) {
+ const images = venue?.images || [];
+
+ const hasImages = images.length > 0;
+ const hasMultipleImages = images.length > 1;
+
+ const currentImage = hasImages
+ ? getImageUrl(images[currentImageIndex])
+ : "";
+
+ function previousImage() {
+ if (!hasMultipleImages) return;
+
+ setCurrentImageIndex((prev) =>
+ prev === 0 ? images.length - 1 : prev - 1
+ );
+ }
+
+ function nextImage() {
+ if (!hasMultipleImages) return;
+
+ setCurrentImageIndex((prev) =>
+ prev === images.length - 1 ? 0 : prev + 1
+ );
+ }
+
+ return (
+
+ {hasImages ? (
+
+ ) : (
+
+
+
+ No images available
+
+
+ )}
+
+ {hasMultipleImages && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+ {hasImages && (
+
+ {currentImageIndex + 1} / {images.length}
+
+ )}
+
+ {hasMultipleImages && (
+
+ {images.map((_, index) => (
+ setCurrentImageIndex(index)}
+ className={`h-2.5 w-2.5 rounded-full transition ${
+ currentImageIndex === index
+ ? "bg-white"
+ : "bg-white/40 hover:bg-white"
+ }`}
+ />
+ ))}
+
+ )}
+
+ );
+}
+
+export default VenueGallery;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/VenueOverview.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/VenueOverview.jsx
new file mode 100644
index 0000000000..618636cbfd
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/VenueOverview.jsx
@@ -0,0 +1,76 @@
+import {
+ MapPin,
+ Users,
+ Clock3,
+ CalendarDays,
+} from "lucide-react";
+
+function VenueOverview({ venue }) {
+ if (!venue) return null;
+
+ return (
+
+
+
+
+ {venue.name}
+
+
+
+
+
+
+
+ {venue.address_line}, {venue.city},{" "}
+ {venue.pincode}
+
+
+
+
+
+
+
+ {venue.capacity} Guests
+
+
+
+
+
+
+ {venue.supports_hourly && (
+
+
+ Hourly Booking
+
+ )}
+
+ {venue.supports_daily && (
+
+
+ Daily Booking
+
+ )}
+
+
+
+
+
+ About this venue
+
+
+
+ {venue.description ||
+ "No description available for this venue."}
+
+
+
+ );
+}
+
+export default VenueOverview;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/VenuePricing.jsx b/bookmyvenue/apps/Frontend/src/pages/venue/VenuePricing.jsx
new file mode 100644
index 0000000000..83e8a2fb4a
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/VenuePricing.jsx
@@ -0,0 +1,105 @@
+import { Clock3, CalendarDays, BadgeIndianRupee } from "lucide-react";
+
+function VenuePricing({ venue }) {
+ if (!venue) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ Pricing
+
+
+
+ Transparent pricing with no hidden charges.
+
+
+
+
+
+ {venue.supports_hourly && (
+
+
+
+
+
+
+
+ Hourly Booking
+
+
+
+
+ Perfect for short events and meetings.
+
+
+
+
+
+ ₹{venue.hourly_price}
+
+
+
+ / hour
+
+
+
+
+ )}
+
+ {venue.supports_daily && (
+
+
+
+
+
+
+
+ Daily Booking
+
+
+
+
+ Best value for full-day events.
+
+
+
+
+
+ ₹{venue.daily_price}
+
+
+
+ / day
+
+
+
+
+ )}
+
+
+ {!venue.supports_hourly &&
+ !venue.supports_daily && (
+
+ Pricing information is currently unavailable.
+
+ )}
+
+ );
+}
+
+export default VenuePricing;
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/src/pages/venue/utils.js b/bookmyvenue/apps/Frontend/src/pages/venue/utils.js
new file mode 100644
index 0000000000..3251b61940
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/src/pages/venue/utils.js
@@ -0,0 +1,66 @@
+export function getImageUrl(image) {
+ if (!image) return "";
+
+ if (typeof image === "string") return image;
+
+ return image.url || image.image_url || "";
+}
+
+export function getTodayDate() {
+ return new Date().toISOString().split("T")[0];
+}
+
+export function formatTime(dateString) {
+ if (!dateString) return "Full Day";
+
+ // Handle "HH:MM:SS" string from backend (Python time serialized as JSON)
+ if (typeof dateString === "string" && dateString.match(/^\d{2}:\d{2}/)) {
+ return dateString.slice(0, 5);
+ }
+
+ const date = new Date(dateString);
+
+ if (isNaN(date.getTime())) return "Invalid Date";
+
+ return date.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ timeZone: "UTC",
+ });
+}
+
+export function calculateBookingSummary(
+ venue,
+ availabilityGroup,
+ selectedSlotIds,
+ selectedBookingType
+) {
+ if (
+ !venue ||
+ !availabilityGroup ||
+ selectedSlotIds.length === 0
+ ) {
+ return null;
+ }
+
+ let basePrice = 0;
+
+ if (selectedBookingType === "hourly") {
+ basePrice =
+ (venue.hourly_price || 0) * selectedSlotIds.length;
+ } else if (selectedBookingType === "daily") {
+ basePrice =
+ (venue.daily_price || 0) * selectedSlotIds.length;
+ }
+
+ const taxAmount = basePrice * 0.12;
+ const platformFee = 100;
+
+ return {
+ base_price: basePrice,
+ tax_amount: taxAmount,
+ platform_fee: platformFee,
+ total_amount: basePrice + taxAmount + platformFee,
+ };
+}
\ No newline at end of file
diff --git a/bookmyvenue/apps/Frontend/vite.config.js b/bookmyvenue/apps/Frontend/vite.config.js
new file mode 100644
index 0000000000..2d1c7e278c
--- /dev/null
+++ b/bookmyvenue/apps/Frontend/vite.config.js
@@ -0,0 +1,9 @@
+import {
+ defineConfig
+} from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});
\ No newline at end of file
diff --git a/bookmyvenue/apps/write_booking.py b/bookmyvenue/apps/write_booking.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/__init__.py b/bookmyvenue/services/api/app/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/config.py b/bookmyvenue/services/api/app/config.py
new file mode 100644
index 0000000000..a102409f5f
--- /dev/null
+++ b/bookmyvenue/services/api/app/config.py
@@ -0,0 +1,2 @@
+TAX_PERCENT = 12
+PLATFORM_FEE = 100
\ No newline at end of file
diff --git a/bookmyvenue/services/api/app/database.py b/bookmyvenue/services/api/app/database.py
new file mode 100644
index 0000000000..b68229a353
--- /dev/null
+++ b/bookmyvenue/services/api/app/database.py
@@ -0,0 +1,21 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker, declarative_base
+from dotenv import load_dotenv
+import os
+
+load_dotenv()
+
+engine = create_engine(os.getenv("DATABASE_URL"))
+
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+
+def get_db():
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+
+Base = declarative_base()
diff --git a/bookmyvenue/services/api/app/main.py b/bookmyvenue/services/api/app/main.py
new file mode 100644
index 0000000000..9000824cdc
--- /dev/null
+++ b/bookmyvenue/services/api/app/main.py
@@ -0,0 +1,54 @@
+from fastapi import FastAPI, Depends
+from database import get_db
+from sqlalchemy.orm import Session
+from utils.dependencies import get_current_user
+from models.user import User
+from models.availability import Availability
+from models.venue import Venue
+from models.venue_category import VenueCategory
+from models.venue_image import VenueImage
+from models.booking import Booking
+from models.booking_slot import BookingSlot
+
+
+from database import Base, engine
+from routes import auth, venue_category, venue, availability, booking, admin
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+Base.metadata.create_all(bind=engine)
+
+app = FastAPI()
+app.include_router(auth.router, prefix="/api/auth", tags=["Auth"])
+app.include_router(venue_category.router,
+ prefix="/api/categories", tags=[" Venue Categories"])
+app.include_router(venue.router, prefix="/api/venues", tags=["Venue"])
+app.include_router(availability.router,
+ prefix="/api/availability", tags=["Availability"])
+app.include_router(booking.router,
+ prefix="/api/bookings", tags=["Bookings"])
+app.include_router(admin.router, prefix="/api/admin", tags=["Admin"])
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.get("/health")
+def health_check():
+ return {"status": "okay"}
+
+
+@app.get("/db-check")
+def db_check(db: Session = Depends(get_db)):
+ return {"status": f"{db}connected"}
+
+
+@app.get("/me")
+def get_user(current_user: User = Depends(get_current_user)):
+ return current_user
diff --git a/bookmyvenue/services/api/app/models/__init__.py b/bookmyvenue/services/api/app/models/__init__.py
new file mode 100644
index 0000000000..57f1a9e888
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/__init__.py
@@ -0,0 +1,7 @@
+from .user import User
+from .venue_category import VenueCategory
+from .venue import Venue
+from .venue_image import VenueImage
+from .availability import Availability
+from .booking import Booking
+from .booking_slot import BookingSlot
diff --git a/bookmyvenue/services/api/app/models/availability.py b/bookmyvenue/services/api/app/models/availability.py
new file mode 100644
index 0000000000..5f96d801e8
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/availability.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from datetime import UTC, datetime, time
+from sqlalchemy import DateTime, Integer, ForeignKey, Date, Time, Boolean, Enum as SqlEnum
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+from enum import Enum
+
+from models.booking import BookingTypeEnum
+
+from database import Base
+from .venue import Venue
+
+
+class Availability(Base):
+ __tablename__ = "availabilities"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, nullable=False, autoincrement=True, index=True)
+ venue_id: Mapped[int] = mapped_column(
+ ForeignKey(Venue.id), nullable=False, index=True)
+ date: Mapped[datetime] = mapped_column(
+ Date, nullable=False, default=lambda: datetime.now(UTC).date())
+ start_time: Mapped[time | None] = mapped_column(
+ Time, nullable=False, default=lambda: datetime.now(UTC))
+ end_time: Mapped[time | None] = mapped_column(
+ Time, nullable=True)
+ booking_type: Mapped[BookingTypeEnum] = mapped_column(
+ SqlEnum(BookingTypeEnum), nullable=False)
+ is_booked: Mapped[bool] = mapped_column(
+ Boolean, nullable=False, default=False)
+
+ venue = relationship("Venue", back_populates="availabilities")
+ booking_slot = relationship("BookingSlot", back_populates="availability")
diff --git a/bookmyvenue/services/api/app/models/booking.py b/bookmyvenue/services/api/app/models/booking.py
new file mode 100644
index 0000000000..77275bb030
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/booking.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from datetime import datetime, UTC
+from sqlalchemy import DateTime, Integer, String, ForeignKey, Float, Enum as SqlEnum
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+from enum import Enum
+
+from database import Base
+from .user import User
+from .venue import Venue
+
+
+class BookingTypeEnum(str, Enum):
+ HOURLY = "hourly"
+ DAILY = "daily"
+
+
+class BookingStatusEnum(str, Enum):
+ PENDING = "pending"
+ CONFIRMED = "confirmed"
+ REJECTED = "rejected"
+ CANCELLED = "cancelled"
+
+
+class PaymentEnum(str, Enum):
+ UNPAID = "unpaid"
+ PAID = "paid"
+
+
+class Booking(Base):
+ __tablename__ = "bookings"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True, index=True)
+ venue_id: Mapped[int] = mapped_column(
+ ForeignKey(Venue.id), nullable=False, index=True)
+ booker_id: Mapped[int] = mapped_column(
+ ForeignKey(User.id), nullable=False, index=True)
+ booking_type: Mapped[BookingTypeEnum] = mapped_column(
+ SqlEnum(BookingTypeEnum), nullable=False, default=None
+ )
+ base_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+ tax_amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+ platform_fee: Mapped[float] = mapped_column(
+ Float, nullable=False, default=0)
+ total_amount: Mapped[float] = mapped_column(
+ Float, nullable=False, default=0)
+ status: Mapped[BookingStatusEnum] = mapped_column(
+ SqlEnum(BookingStatusEnum), nullable=False, default=BookingStatusEnum.PENDING)
+ payment_status: Mapped[PaymentEnum] = mapped_column(
+ SqlEnum(PaymentEnum), nullable=False, default=PaymentEnum.UNPAID)
+ created_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=lambda: datetime.now(UTC))
+
+ venue = relationship("Venue", back_populates="bookings")
+ booker = relationship("User", back_populates="bookings")
+ slots = relationship("BookingSlot", back_populates="booking")
diff --git a/bookmyvenue/services/api/app/models/booking_slot.py b/bookmyvenue/services/api/app/models/booking_slot.py
new file mode 100644
index 0000000000..1e3f107ba3
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/booking_slot.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from datetime import datetime, UTC
+from sqlalchemy import Integer, ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from database import Base
+from .availability import Availability
+from .booking import Booking
+
+
+class BookingSlot(Base):
+
+ __tablename__ = "bookingslots"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, nullable=False, autoincrement=True, index=True)
+ booking_id: Mapped[int] = mapped_column(
+ ForeignKey(Booking.id), nullable=False, index=True)
+ availability_id: Mapped[int] = mapped_column(
+ ForeignKey(Availability.id), nullable=False, index=True)
+
+ booking = relationship("Booking", back_populates="slots")
+ availability = relationship("Availability", back_populates="booking_slot")
+
diff --git a/bookmyvenue/services/api/app/models/user.py b/bookmyvenue/services/api/app/models/user.py
new file mode 100644
index 0000000000..f058642adf
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/user.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from sqlalchemy import DateTime, Integer, String, Enum
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+import enum
+
+from database import Base
+
+
+class RoleEnum(str, enum.Enum):
+ BOOKER = "booker"
+ OWNER = "owner"
+ ADMIN = "admin"
+
+
+class User(Base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True, index=True)
+ name: Mapped[str] = mapped_column(String(50), nullable=False)
+ email: Mapped[str] = mapped_column(
+ String(120), unique=True, nullable=False)
+ password_hash: Mapped[str] = mapped_column(String(200), nullable=False)
+ role: Mapped[RoleEnum] = mapped_column(
+ Enum(RoleEnum), nullable=False, default=RoleEnum.BOOKER)
+ created_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=lambda: datetime.now(UTC))
+
+ venues = relationship("Venue", back_populates="owner")
+ bookings = relationship("Booking", back_populates="booker")
diff --git a/bookmyvenue/services/api/app/models/venue.py b/bookmyvenue/services/api/app/models/venue.py
new file mode 100644
index 0000000000..9ff4945534
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/venue.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from sqlalchemy import DateTime, Integer, String, ForeignKey, Boolean, Float, Enum
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from database import Base
+from .user import User
+from .venue_category import VenueCategory
+import enum
+
+
+class StatusEnum(str, enum.Enum):
+ ACTIVE = "active"
+ INACTIVE = "inactive"
+
+
+class Venue(Base):
+ __tablename__ = "venues"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True, index=True)
+ owner_id: Mapped[int] = mapped_column(
+ ForeignKey(User.id), nullable=False, index=True)
+ category_id: Mapped[int] = mapped_column(
+ ForeignKey(VenueCategory.id), nullable=False)
+ name: Mapped[str] = mapped_column(String(100), nullable=False)
+ description: Mapped[str] = mapped_column(String(500), nullable=True)
+ address_line: Mapped[str] = mapped_column(String, nullable=False)
+ city: Mapped[str] = mapped_column(String, nullable=False)
+ pincode: Mapped[str] = mapped_column(String, nullable=False)
+ capacity: Mapped[int] = mapped_column(Integer, nullable=False)
+ supports_hourly: Mapped[bool] = mapped_column(
+ Boolean, default=False, nullable=False)
+ supports_daily: Mapped[bool] = mapped_column(
+ Boolean, default=False, nullable=False)
+ hourly_price: Mapped[float] = mapped_column(Float, nullable=True)
+ daily_price: Mapped[float] = mapped_column(Float, nullable=True)
+ amenities: Mapped[str] = mapped_column(String(500), nullable=True)
+ cancellation_policy: Mapped[str | None] = mapped_column(
+ String(1000), nullable=True)
+ status: Mapped[StatusEnum] = mapped_column(
+ Enum(StatusEnum), nullable=False, default=StatusEnum.ACTIVE)
+ created_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=lambda: datetime.now(UTC))
+
+ owner = relationship("User", back_populates="venues")
+ category = relationship("VenueCategory", back_populates="venues")
+ images = relationship("VenueImage", back_populates="venue")
+ availabilities = relationship("Availability", back_populates="venue")
+ bookings = relationship("Booking", back_populates="venue")
diff --git a/bookmyvenue/services/api/app/models/venue_category.py b/bookmyvenue/services/api/app/models/venue_category.py
new file mode 100644
index 0000000000..4618b8862a
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/venue_category.py
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+from datetime import datetime, UTC
+from sqlalchemy import DateTime, Integer, String, Boolean
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from database import Base
+
+
+class VenueCategory(Base):
+
+ __tablename__ = "venue_categories"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True, nullable=False)
+ name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
+ slug: Mapped[str] = mapped_column(String, unique=True, nullable=False)
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
+ created_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=lambda: datetime.now(UTC))
+
+ venues = relationship("Venue", back_populates="category")
diff --git a/bookmyvenue/services/api/app/models/venue_image.py b/bookmyvenue/services/api/app/models/venue_image.py
new file mode 100644
index 0000000000..3a0af948d3
--- /dev/null
+++ b/bookmyvenue/services/api/app/models/venue_image.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+from sqlalchemy import Integer, String, ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from database import Base
+from .venue import Venue
+
+
+class VenueImage(Base):
+ __tablename__ = "venue_images"
+
+ id: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True, index=True)
+ venue_id: Mapped[int] = mapped_column(
+ Integer, ForeignKey(Venue.id), nullable=False)
+ image_url: Mapped[str] = mapped_column(String, nullable=False)
+ display_order: Mapped[int] = mapped_column(Integer, default=0)
+
+ venue = relationship("Venue", back_populates="images")
+
diff --git a/bookmyvenue/services/api/app/routes/__init__.py b/bookmyvenue/services/api/app/routes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/routes/admin.py b/bookmyvenue/services/api/app/routes/admin.py
new file mode 100644
index 0000000000..eb45956e14
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/admin.py
@@ -0,0 +1,204 @@
+from sqlalchemy import select, desc, func
+from sqlalchemy.orm import Session, selectinload
+from fastapi import APIRouter, Depends, HTTPException, status, Query
+from typing import Annotated
+
+from database import get_db
+from models.user import User, RoleEnum
+from models.venue import Venue, StatusEnum, VenueCategory
+from schemas.venue import VenueCategoryOut, VenueOut
+from schemas.auth import UserOut
+from schemas.booking import BookingOut
+from models.booking import Booking, BookingStatusEnum, BookingTypeEnum, PaymentEnum
+from models.booking_slot import BookingSlot
+from schemas.admin import VenueStatusUpdate, CategoryStatusUpdate, PaginatedResponse
+from utils.dependencies import require_role
+
+DEFAULT_PAGE = 1
+DEFAULT_LIMIT = 20
+
+router = APIRouter()
+
+
+def paginated_response(items, total, page, limit):
+ return {
+ "items": items,
+ "total": total,
+ "page": page,
+ "limit": limit,
+ "pages": (total + limit - 1) // limit,
+ }
+
+
+@router.get("/stats", status_code=status.HTTP_200_OK)
+def get_admin_stats(
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))],
+):
+ total_users = db.execute(select(func.count(User.id))).scalar_one()
+ total_owners = db.execute(
+ select(func.count(User.id)).where(User.role == RoleEnum.OWNER.value)
+ ).scalar_one()
+ total_bookers = db.execute(
+ select(func.count(User.id)).where(User.role == RoleEnum.BOOKER.value)
+ ).scalar_one()
+
+ total_venues = db.execute(select(func.count(Venue.id))).scalar_one()
+ active_venues = db.execute(
+ select(func.count(Venue.id)).where(
+ Venue.status == StatusEnum.ACTIVE.value)
+ ).scalar_one()
+
+ total_bookings = db.execute(select(func.count(Booking.id))).scalar_one()
+ hourly_bookings = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.booking_type == BookingTypeEnum.HOURLY.value)
+ ).scalar_one()
+ daily_bookings = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.booking_type == BookingTypeEnum.DAILY.value)
+ ).scalar_one()
+ pending_bookings = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.status == BookingStatusEnum.PENDING.value)
+ ).scalar_one()
+ confirmed_bookings = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.status == BookingStatusEnum.CONFIRMED.value)
+ ).scalar_one()
+ rejected_bookings = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.status == BookingStatusEnum.REJECTED.value)
+ ).scalar_one()
+
+ total_revenue = db.execute(
+ select(func.coalesce(func.sum(Booking.total_amount), 0)).where(
+ Booking.status == BookingStatusEnum.CONFIRMED.value)
+ ).scalar_one()
+ paid_count = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.payment_status == PaymentEnum.PAID.value)
+ ).scalar_one()
+ unpaid_count = db.execute(
+ select(func.count(Booking.id)).where(
+ Booking.payment_status == PaymentEnum.UNPAID.value)
+ ).scalar_one()
+
+ return {
+ "total_users": total_users,
+ "total_owners": total_owners,
+ "total_bookers": total_bookers,
+ "total_venues": total_venues,
+ "active_venues": active_venues,
+ "total_bookings": total_bookings,
+ "hourly_bookings": hourly_bookings,
+ "daily_bookings": daily_bookings,
+ "pending_bookings": pending_bookings,
+ "confirmed_bookings": confirmed_bookings,
+ "rejected_bookings": rejected_bookings,
+ "total_revenue": float(total_revenue),
+ "paid_bookings": paid_count,
+ "unpaid_bookings": unpaid_count,
+ }
+
+
+@router.get("/users", status_code=status.HTTP_200_OK)
+def get_all_users(
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))],
+ role: str | None = Query(default=None),
+ page: int = Query(default=DEFAULT_PAGE, ge=1),
+ limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=100),
+):
+ q = select(User).order_by(User.created_at.desc())
+ if role:
+ q = q.where(User.role == role)
+ total = db.execute(select(func.count()).select_from(User).where(
+ *([User.role == role] if role else []))).scalar_one()
+ offset = (page - 1) * limit
+ users = db.execute(q.offset(offset).limit(limit)).scalars().all()
+ return paginated_response([UserOut.model_validate(u) for u in users], total, page, limit)
+
+
+@router.get("/venues", status_code=status.HTTP_200_OK)
+def get_all_venues(
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))],
+ venue_status: StatusEnum | None = Query(default=None, alias="status"),
+ page: int = Query(default=DEFAULT_PAGE, ge=1),
+ limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=100),
+):
+ q = select(Venue).options(selectinload(Venue.images)
+ ).order_by(desc(Venue.created_at))
+ where_clauses = []
+ if venue_status:
+ where_clauses.append(Venue.status == venue_status)
+ if where_clauses:
+ q = q.where(*where_clauses)
+ total = db.execute(select(func.count()).select_from(Venue).where(
+ *where_clauses)).scalar_one()
+ offset = (page - 1) * limit
+ venues = db.execute(q.offset(offset).limit(limit)).scalars().all()
+ return paginated_response([VenueOut.model_validate(v) for v in venues], total, page, limit)
+
+
+@router.put("/venues/{id}/status", response_model=VenueOut, status_code=status.HTTP_200_OK)
+def update_venue_status(
+ id: int,
+ venue_status: VenueStatusUpdate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))],
+):
+ venue = db.execute(
+ select(Venue).options(selectinload(Venue.images)).where(Venue.id == id)
+ ).scalars().first()
+ if not venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail="Venue not found")
+ venue.status = venue_status.status
+ db.commit()
+ db.refresh(venue)
+ return venue
+
+
+@router.get("/bookings", status_code=status.HTTP_200_OK)
+def get_all_bookings(
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))],
+ status: BookingStatusEnum | None = Query(default=None),
+ page: int = Query(default=DEFAULT_PAGE, ge=1),
+ limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=100),
+):
+ q = select(Booking).options(
+ selectinload(Booking.slots).selectinload(BookingSlot.availability),
+ selectinload(Booking.venue),
+ selectinload(Booking.booker),
+ ).order_by(desc(Booking.created_at))
+ where_clauses = []
+ if status:
+ where_clauses.append(Booking.status == status)
+ if where_clauses:
+ q = q.where(*where_clauses)
+ total = db.execute(select(func.count()).select_from(Booking).where(
+ *where_clauses)).scalar_one()
+ offset = (page - 1) * limit
+ bookings = db.execute(q.offset(offset).limit(limit)).scalars().all()
+ return paginated_response([BookingOut.model_validate(b) for b in bookings], total, page, limit)
+
+
+@router.put("/categories/{id}/status", response_model=VenueCategoryOut, status_code=status.HTTP_200_OK)
+def update_category_status(
+ id: int,
+ category_status: CategoryStatusUpdate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.ADMIN))]
+):
+ category = db.execute(select(VenueCategory).where(
+ VenueCategory.id == id)).scalars().first()
+ if not category:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
+ category.is_active = category_status.is_active
+ db.commit()
+ db.refresh(category)
+ return category
diff --git a/bookmyvenue/services/api/app/routes/auth.py b/bookmyvenue/services/api/app/routes/auth.py
new file mode 100644
index 0000000000..45e4ae5e90
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/auth.py
@@ -0,0 +1,86 @@
+from datetime import timedelta
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordRequestForm
+from sqlalchemy import func, select
+from sqlalchemy.orm import Session
+from schemas.auth import UserRegister, UserOut, UserLogin, Token
+
+from database import get_db
+import os
+
+from models.user import User, RoleEnum
+from utils.hashing import hash_password, verify_password
+from utils.jwt import verify_access_token, create_access_token
+
+ACCESS_TOKEN_EXPIRE_MINUTES = int(
+ os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", default=30))
+
+router = APIRouter()
+
+
+@router.post(
+ "/register",
+ response_model=UserOut,
+ status_code=status.HTTP_201_CREATED,
+)
+def create_user(user: UserRegister, db: Annotated[Session, Depends(get_db)]):
+ result = db.execute(select(User).where(func.lower(User.email) == user.email.lower(),
+ ),
+ )
+ print(user.role, "---role")
+ existing_user = result.scalars().first()
+ if existing_user:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Email already registered"
+ )
+
+ new_user = User(
+ name=user.name,
+ email=user.email.lower(),
+ password_hash=hash_password(user.password),
+ role=RoleEnum(user.role.lower()),
+ )
+ db.add(new_user)
+ db.commit()
+ db.refresh(new_user)
+ return new_user
+
+
+@router.post(
+ "/login",
+ response_model=Token,
+ status_code=status.HTTP_200_OK
+)
+def login_for_access_token(
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
+ db: Annotated[Session, Depends(get_db)]):
+
+ email = form_data.username.strip().lower()
+
+ result = db.execute(select(User).where(User.email == email))
+ user = result.scalars().first()
+ print("user login", user)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect email or passwords",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ if not verify_password(form_data.password, user.password_hash):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect email or passwords",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": str(user.id),
+ "role": str(user.role.value)},
+ expires_delta=access_token_expires,
+
+ )
+ return Token(access_token=access_token, token_type="bearer")
diff --git a/bookmyvenue/services/api/app/routes/availability.py b/bookmyvenue/services/api/app/routes/availability.py
new file mode 100644
index 0000000000..5c6b680f49
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/availability.py
@@ -0,0 +1,208 @@
+from datetime import date as date_type, datetime, UTC
+from fastapi import APIRouter, status, Depends, HTTPException
+from typing import Annotated
+from sqlalchemy.orm import Session
+from sqlalchemy import select, and_
+from schemas.availability import AvailabilityOut, AvailabilityCreate, BookingTypeEnum, AvailabilityByDateResponse
+from models.user import User, RoleEnum
+from models.venue import Venue
+from models.availability import Availability
+from utils.dependencies import require_role
+from database import get_db
+
+
+router = APIRouter()
+
+
+@router.post(
+ "/",
+ response_model=list[AvailabilityOut],
+ status_code=status.HTTP_201_CREATED
+)
+def availability_create(
+ create_availability: AvailabilityCreate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]
+):
+
+ venue_query = db.execute(select(Venue).where(
+ Venue.id == create_availability.venue_id)).scalars().first()
+
+ if not venue_query:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Venue not found"
+ )
+
+ if venue_query.owner_id != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized"
+ )
+
+ if create_availability.booking_type == BookingTypeEnum.HOURLY and not venue_query.supports_hourly:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="This venue does not support hourly booking"
+ )
+
+ if create_availability.booking_type == BookingTypeEnum.DAILY and not venue_query.supports_daily:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="This venue does not support daily booking"
+ )
+
+ # For daily: check if same date and daily type already exists
+ if create_availability.booking_type == BookingTypeEnum.DAILY:
+ already_exists = db.execute(select(Availability).where(
+ Availability.venue_id == create_availability.venue_id,
+ Availability.date == create_availability.date,
+ Availability.booking_type == BookingTypeEnum.DAILY.value
+ )).scalars().first()
+ if already_exists:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Daily availability already set for this date"
+ )
+
+ # For hourly: check if any of the time slots overlap with existing ones
+ if create_availability.booking_type == BookingTypeEnum.HOURLY and create_availability.slots:
+ existing_slots = db.execute(select(Availability).where(
+ Availability.venue_id == create_availability.venue_id,
+ Availability.date == create_availability.date,
+ Availability.booking_type == BookingTypeEnum.HOURLY.value
+ )).scalars().all()
+
+ for new_slot in create_availability.slots:
+ for existing in existing_slots:
+ if existing.start_time and existing.end_time and new_slot.start_time and new_slot.end_time:
+ # Check overlap: existing start < new end AND existing end > new start
+ if existing.start_time < new_slot.end_time and existing.end_time > new_slot.start_time:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Time slot {new_slot.start_time}-{new_slot.end_time} overlaps with existing slot {existing.start_time}-{existing.end_time}"
+ )
+
+ new_availability = []
+
+ if create_availability.booking_type == BookingTypeEnum.DAILY:
+ new_availability.append(
+ Availability(
+ venue_id=create_availability.venue_id,
+ date=create_availability.date,
+ booking_type=create_availability.booking_type.value,
+ start_time=None,
+ end_time=None
+ )
+ )
+ elif create_availability.booking_type == BookingTypeEnum.HOURLY:
+ if create_availability.slots is not None:
+ for slot in create_availability.slots:
+ new_availability.append(
+ Availability(
+ venue_id=create_availability.venue_id,
+ date=create_availability.date,
+ booking_type=create_availability.booking_type.value,
+ start_time=slot.start_time,
+ end_time=slot.end_time
+ )
+ )
+ db.add_all(new_availability)
+ db.commit()
+
+ for row in new_availability:
+ db.refresh(row)
+
+ return new_availability
+
+
+@router.delete(
+ "/{availability_id}",
+ status_code=status.HTTP_200_OK
+)
+def delete_availability(
+ availability_id: int,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]
+):
+ availability = db.execute(select(Availability).where(
+ Availability.id == availability_id
+ )).scalars().first()
+
+ if not availability:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Availability not found"
+ )
+
+ venue = db.execute(select(Venue).where(
+ Venue.id == availability.venue_id
+ )).scalars().first()
+
+ if not venue or venue.owner_id != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized"
+ )
+
+ if availability.is_booked:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Cannot delete a booked availability slot"
+ )
+
+ db.delete(availability)
+ db.commit()
+
+ return {"message": "Availability deleted"}
+
+
+@router.get(
+ "/{venue_id}",
+ response_model=list[AvailabilityByDateResponse],
+ status_code=status.HTTP_200_OK
+)
+def get_availability(
+ venue_id: int,
+ db: Annotated[Session, Depends(get_db)],
+ date: date_type | None = None
+):
+
+ venue = db.execute(select(Venue).where(
+ Venue.id == venue_id)).scalars().first()
+
+ if not venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Venue not found"
+ )
+
+ query = select(Availability).where(Availability.venue_id == venue_id)
+
+ if date is not None:
+ query = query.where(Availability.date == date)
+ elif date is None:
+ today = datetime.now(UTC).date()
+ query = query.where(Availability.date >= today)
+
+ query = query.order_by(
+ Availability.date.asc(),
+ Availability.start_time.asc()
+ )
+
+ availabilities = db.execute(query).scalars().all()
+
+ grouped = {}
+
+ for availability in availabilities:
+ key = (availability.date, availability.booking_type)
+
+ if key not in grouped:
+ grouped[key] = {
+ "date": availability.date,
+ "booking_type": availability.booking_type,
+ "slots": []
+ }
+ grouped[key]["slots"].append(availability)
+
+ return list(grouped.values())
diff --git a/bookmyvenue/services/api/app/routes/booking.py b/bookmyvenue/services/api/app/routes/booking.py
new file mode 100644
index 0000000000..aa6c41ecd0
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/booking.py
@@ -0,0 +1,230 @@
+from __future__ import annotations
+from fastapi import APIRouter, HTTPException, Depends, status, Query
+from typing import Annotated
+from sqlalchemy import select, desc
+from sqlalchemy.orm import Session, selectinload
+from schemas.booking import BookingCreate, BookingOut, BookingStatusUpdate, BookingStatusEnum, PaymentEnum, BookingStatusEnum
+from models.user import User, RoleEnum
+from models.booking import Booking
+from models.booking_slot import BookingSlot
+from models.availability import Availability, Venue
+from config import TAX_PERCENT, PLATFORM_FEE
+
+
+from utils.dependencies import require_role
+from database import get_db
+
+
+router = APIRouter()
+
+
+@router.post(
+ "/",
+ response_model=BookingOut,
+ status_code=status.HTTP_200_OK
+)
+def create_booking(
+ booking_create: BookingCreate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.BOOKER))]):
+
+ availability_ids = list(set(booking_create.availability_ids))
+
+ availability_rows = db.execute(select(Availability).where(
+ Availability.id.in_(availability_ids))).scalars().all()
+
+ if len(availability_rows) != len(availability_ids):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="One or more invalid availability slots"
+ )
+ venue_ids = {slot.venue_id for slot in availability_rows}
+ if len(venue_ids) > 1:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="All slots must belong to the same venue"
+ )
+
+ slot_venue_id = next(iter(venue_ids))
+ if slot_venue_id != booking_create.venue_id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Slot venue does not match provided venue"
+ )
+
+ slot_type = {slot.booking_type for slot in availability_rows}
+ if len(slot_type) > 1:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="All slots must be of the same booking type"
+ )
+
+ already_booked = any(slot.is_booked for slot in availability_rows)
+ if already_booked:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="One or more slots are already booked"
+ )
+
+ current_venue = db.execute(select(Venue).where(
+ Venue.id == booking_create.venue_id)).scalars().first()
+ if not current_venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Venue not found"
+ )
+
+ booking_type = next(iter(slot_type))
+
+ if hasattr(booking_type, "value"):
+ booking_type_value = booking_type.value
+ else:
+ booking_type_value = booking_type
+
+ number_of_slots = len(availability_ids)
+
+ base_price = 0
+ if booking_type_value == "hourly":
+ base_price = current_venue.hourly_price * number_of_slots
+
+ elif booking_type_value == "daily":
+ base_price = current_venue.daily_price * number_of_slots
+
+ tax_amount = base_price * (TAX_PERCENT/100)
+ platform_fee = PLATFORM_FEE
+ total_amount = base_price + tax_amount + platform_fee
+
+ try:
+ for slot in availability_rows:
+ slot.is_booked = True
+
+ new_booking = Booking(
+ venue_id=booking_create.venue_id,
+ booker_id=current_user.id,
+ booking_type=booking_type,
+ base_price=base_price,
+ tax_amount=tax_amount,
+ platform_fee=platform_fee,
+ total_amount=total_amount,
+ status=BookingStatusEnum.PENDING,
+ payment_status=PaymentEnum.UNPAID,
+ )
+ db.add(new_booking)
+ db.flush()
+
+ for availability_id in availability_ids:
+ new_booking_slot = BookingSlot(
+ booking_id=new_booking.id,
+ availability_id=availability_id
+ )
+ db.add(new_booking_slot)
+ db.commit()
+ db.refresh(new_booking)
+
+ return new_booking
+
+ except Exception as e:
+ db.rollback()
+ raise
+
+
+@router.get(
+ "/my",
+ response_model=list[BookingOut],
+ status_code=status.HTTP_200_OK
+)
+def get_my_bookings(db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.BOOKER))]):
+
+ all_bookings = db.execute(
+ select(Booking)
+ .options(
+ selectinload(Booking.slots).selectinload(BookingSlot.availability),
+ selectinload(Booking.venue)
+ )
+ .where(Booking.booker_id == current_user.id)
+ .order_by(desc(Booking.created_at))
+ ).scalars().all()
+ return all_bookings
+
+
+@router.get(
+ "/owner",
+ response_model=list[BookingOut],
+ status_code=status.HTTP_200_OK
+)
+def get_owner_bookings(db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]):
+ owned_venue_ids = db.execute(select(Venue.id)
+ .where(Venue.owner_id == current_user.id)
+ ).scalars().all()
+ if not owned_venue_ids:
+ return []
+
+ owner_bookings = db.execute(select(Booking)
+ .options(
+ selectinload(Booking.slots).selectinload(
+ BookingSlot.availability),
+ selectinload(Booking.venue),
+ selectinload(Booking.booker)
+ )
+ .where(Booking.venue_id.in_(list(owned_venue_ids)))
+ .order_by(desc(Booking.created_at))
+ ).scalars().all()
+
+ return owner_bookings
+
+
+@router.put(
+ "/{id}/status",
+ response_model=BookingOut,
+ status_code=status.HTTP_200_OK
+)
+def booking_status_update(id: int,
+ booking_status_update: BookingStatusUpdate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]):
+
+ current_booking = db.execute(select(Booking)
+ .options(
+ selectinload(Booking.slots).selectinload(
+ BookingSlot.availability),
+ selectinload(Booking.venue),
+ selectinload(Booking.booker),
+ )
+ .where(Booking.id == id)).scalars().first()
+
+ if not current_booking:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Booking not found"
+ )
+
+ booked_venue = current_booking.venue
+
+ if not booked_venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="No Venue associated with this booking"
+ )
+ if booked_venue.owner_id != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Venue is not owned by logged in user"
+ )
+ if current_booking.status in (BookingStatusEnum.CONFIRMED, BookingStatusEnum.REJECTED):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Booking already finalized"
+ )
+
+ current_booking.status = booking_status_update.status
+
+ if booking_status_update.status == BookingStatusEnum.REJECTED:
+ for slot in current_booking.slots:
+ if slot.availability:
+ slot.availability.is_booked = False
+
+ db.commit()
+ db.refresh(current_booking)
+ return current_booking
diff --git a/bookmyvenue/services/api/app/routes/venue.py b/bookmyvenue/services/api/app/routes/venue.py
new file mode 100644
index 0000000000..7776ef9ace
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/venue.py
@@ -0,0 +1,291 @@
+from typing import Annotated, List, Optional
+from fastapi import APIRouter, HTTPException, Depends, status, Query
+from sqlalchemy.orm import Session, selectinload
+from sqlalchemy import func, select, or_, desc
+from utils.dependencies import require_role
+from models.user import User, RoleEnum
+from models.venue_category import VenueCategory
+from models.venue import Venue, StatusEnum
+from models.venue_image import VenueImage
+from schemas.venue import VenueOut, VenueCreate, VenueUpdate, VenuePaginatedResponse, HomePageResponse
+from database import get_db
+from utils.dependencies import get_current_user
+from enum import Enum
+
+
+router = APIRouter()
+
+
+class AvailabilityEnum(Enum):
+ HOURLY = "hourly"
+ DAILY = "daily"
+ BOTH = "both"
+
+
+@router.post(
+ "/",
+ response_model=VenueOut,
+ status_code=status.HTTP_201_CREATED,
+)
+def create_venue(venue_create: VenueCreate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]):
+ result = db.execute(select(VenueCategory).where(
+ VenueCategory.id == venue_create.category_id,
+ VenueCategory.is_active == True))
+
+ existing_cat = result.scalars().first()
+
+ if not existing_cat:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Category doesn't exist"
+ )
+
+ new_venue = Venue(
+ owner_id=current_user.id,
+ category_id=venue_create.category_id,
+ name=venue_create.name,
+ description=venue_create.description,
+ address_line=venue_create.address_line,
+ city=venue_create.city,
+ pincode=venue_create.pincode,
+ capacity=venue_create.capacity,
+ supports_hourly=venue_create.supports_hourly,
+ supports_daily=venue_create.supports_daily,
+ hourly_price=venue_create.hourly_price,
+ daily_price=venue_create.daily_price,
+ amenities=venue_create.amenities,
+ cancellation_policy=venue_create.cancellation_policy,
+ status=StatusEnum.ACTIVE,
+
+ )
+
+ db.add(new_venue)
+ db.flush()
+
+ for index, image in enumerate(venue_create.image_urls):
+ new_image = VenueImage(
+ venue_id=new_venue.id,
+ image_url=image,
+ display_order=index,
+
+ )
+ db.add(new_image)
+ db.commit()
+ db.refresh(new_venue)
+
+ return new_venue
+
+
+@router.get(
+ "/",
+ response_model=VenuePaginatedResponse,
+ status_code=status.HTTP_200_OK
+)
+def get_venues(db: Annotated[Session, Depends(get_db)],
+ search: Optional[str] = None,
+ city: Optional[str] = None,
+ category_id: Optional[int] = None,
+ availability_type: Optional[AvailabilityEnum] = None,
+ page: int = Query(default=1, ge=1),
+ limit: int = Query(default=16, ge=1, le=200)
+ ):
+
+ query = [
+ Venue.status == StatusEnum.ACTIVE
+ ]
+
+ if search and search.strip():
+ search_value = f"%{search.strip().lower()}%"
+ query.append(
+ or_(
+ Venue.name.ilike(search_value),
+ Venue.description.ilike(search_value),
+ Venue.city.ilike(search_value),
+ Venue.address_line.ilike(search_value),
+ Venue.amenities.ilike(search_value)
+ )
+ )
+
+ if city and city.strip():
+ query.append(Venue.city.ilike(f"%{city.strip()}%"))
+
+ if category_id:
+ query.append(Venue.category_id == category_id)
+
+ if availability_type:
+ if availability_type == AvailabilityEnum.HOURLY:
+ query.append(Venue.supports_hourly == True)
+
+ elif availability_type == AvailabilityEnum.DAILY:
+ query.append(Venue.supports_daily == True)
+
+ elif availability_type == AvailabilityEnum.BOTH:
+ query.append(Venue.supports_hourly == True)
+ query.append(Venue.supports_daily == True)
+
+ offset = (page - 1) * limit
+
+ total = db.execute(
+ select(func.count(Venue.id)).where(*query)
+ ).scalar_one()
+
+ result = db.execute(
+ select(Venue)
+ .options(selectinload(Venue.images))
+ .where(*query)
+ .offset(offset)
+ .limit(limit)
+ )
+
+ venues = result.scalars().all()
+ return {
+ "items": venues,
+ "total": total,
+ "page": page,
+ "limit": limit
+ }
+
+
+@router.get(
+ "/homepage",
+ response_model=HomePageResponse,
+ status_code=status.HTTP_200_OK,
+)
+def get_homepage_venues(db: Annotated[Session, Depends(get_db)]):
+
+ toprated_query = db.execute(
+ select(Venue)
+ .options(selectinload(Venue.images))
+ .where(Venue.status == StatusEnum.ACTIVE)
+ .order_by(desc(Venue.created_at))
+ .limit(12)
+ ).scalars().all()
+
+ recently_added_query = db.execute(
+ select(Venue)
+ .options(selectinload(Venue.images))
+ .where(Venue.status == StatusEnum.ACTIVE)
+ .order_by(desc(Venue.created_at))
+ .limit(12)
+ ).scalars().all()
+
+ active_categories_query = db.execute(
+ select(VenueCategory)
+ .where(VenueCategory.is_active == True)
+ .order_by(VenueCategory.id.asc())
+ .limit(2)
+ ).scalars().all()
+
+ category_sections = []
+
+ for category in active_categories_query:
+ venues = db.execute(
+ select(Venue)
+ .options(selectinload(Venue.images))
+ .where(
+ Venue.status == StatusEnum.ACTIVE,
+ Venue.category_id == category.id
+ )
+ .order_by(desc(Venue.created_at))
+ ).scalars().all()
+
+ category_sections.append(
+ {
+ "category": category,
+ "venues": venues
+ }
+ )
+ return {
+ "top_rated": toprated_query,
+ "recently_added": recently_added_query,
+ "categories": category_sections
+ }
+
+
+@router.get(
+ "/owner/me",
+ dependencies=[Depends(require_role(RoleEnum.OWNER))],
+ response_model=List[VenueOut],
+ status_code=status.HTTP_200_OK
+)
+def owner_venues(db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]):
+
+ result = db.execute(select(Venue).where(Venue.owner_id == current_user.id))
+ existing_venues = result.scalars().all()
+ return existing_venues
+
+
+@router.put(
+ "/update/{venue_id}",
+ response_model=VenueOut,
+ status_code=status.HTTP_200_OK
+)
+def update_venue(venue_id: int,
+ venue_update: VenueUpdate,
+ db: Annotated[Session, Depends(get_db)],
+ current_user: Annotated[User, Depends(require_role(RoleEnum.OWNER))]):
+ print("UPDATE ROUTE HIT")
+
+ result_venue = db.execute(select(Venue).where(Venue.id == venue_id))
+ current_venue = result_venue.scalars().first()
+ print(current_venue, "current venue")
+
+ if not current_venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Venue not found"
+ )
+ if current_venue.owner_id != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to update this venue"
+ )
+ update_data = venue_update.model_dump(
+ exclude_unset=True,
+ exclude_none=True
+ )
+
+ if "category_id" in update_data:
+ category = db.execute(
+ select(VenueCategory).where(
+ VenueCategory.id == update_data["category_id"],
+ VenueCategory.is_active == True
+ )
+ ).scalars().first()
+
+ if not category:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid category"
+ )
+
+ for field, value in update_data.items():
+ setattr(current_venue, field, value)
+
+ db.commit()
+ db.refresh(current_venue)
+
+ return current_venue
+
+
+@router.get(
+ "/{venue_id}",
+ response_model=VenueOut,
+ status_code=status.HTTP_200_OK
+)
+def get_venue(venue_id: int, db: Annotated[Session, Depends(get_db)]):
+
+ result = db.execute(select(Venue).where(
+ Venue.id == venue_id, Venue.status == StatusEnum.ACTIVE))
+ existing_venue = result.scalars().first()
+
+ if not existing_venue:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Venue not found"
+ )
+
+ return existing_venue
diff --git a/bookmyvenue/services/api/app/routes/venue_category.py b/bookmyvenue/services/api/app/routes/venue_category.py
new file mode 100644
index 0000000000..772ecb7185
--- /dev/null
+++ b/bookmyvenue/services/api/app/routes/venue_category.py
@@ -0,0 +1,50 @@
+from typing import Annotated, List
+from fastapi import APIRouter, HTTPException, status, Depends
+from schemas.venue_category import VenueCategoryOut, VenueCategoryCreate
+from sqlalchemy.orm import Session
+from sqlalchemy import func, select
+from utils.dependencies import require_role
+from database import get_db
+from models.user import RoleEnum
+from models.venue_category import VenueCategory
+
+router = APIRouter()
+
+
+@router.post(
+ "/",
+ dependencies=[Depends(require_role(RoleEnum.ADMIN))],
+ response_model=VenueCategoryOut,
+ status_code=status.HTTP_201_CREATED
+
+)
+def create_category(venue_category_create: VenueCategoryCreate, db: Annotated[Session, Depends(get_db)]):
+ result = db.execute(select(VenueCategory).where(func.lower(
+ VenueCategory.slug) == venue_category_create.slug.lower()),)
+
+ existing_slug = result.scalars().first()
+ if existing_slug:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Category already exists"
+ )
+ new_category = VenueCategory(
+ name=venue_category_create.name,
+ slug=venue_category_create.slug
+ )
+ db.add(new_category)
+ db.commit()
+ db.refresh(new_category)
+ return new_category
+
+
+@router.get(
+ "/",
+ response_model=List[VenueCategoryOut],
+ status_code=status.HTTP_200_OK
+)
+def get_categories(db: Annotated[Session, Depends(get_db)]):
+ result = db.execute(select(VenueCategory).where(
+ VenueCategory.is_active == True))
+ categories = result.scalars().all()
+ return categories
diff --git a/bookmyvenue/services/api/app/schemas/__init__.py b/bookmyvenue/services/api/app/schemas/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/schemas/admin.py b/bookmyvenue/services/api/app/schemas/admin.py
new file mode 100644
index 0000000000..1916ce23ee
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/admin.py
@@ -0,0 +1,22 @@
+from pydantic import BaseModel
+from typing import Generic, TypeVar, List
+
+from models.venue import StatusEnum
+
+T = TypeVar("T")
+
+
+class PaginatedResponse(BaseModel, Generic[T]):
+ items: List[T]
+ total: int
+ page: int
+ limit: int
+ pages: int
+
+
+class VenueStatusUpdate(BaseModel):
+ status: StatusEnum
+
+
+class CategoryStatusUpdate(BaseModel):
+ is_active: bool
diff --git a/bookmyvenue/services/api/app/schemas/auth.py b/bookmyvenue/services/api/app/schemas/auth.py
new file mode 100644
index 0000000000..8393059ac1
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/auth.py
@@ -0,0 +1,26 @@
+from datetime import datetime
+from pydantic import BaseModel, EmailStr, ConfigDict, Field
+
+class UserRegister(BaseModel):
+ name: str = Field(min_length=1, max_length=50)
+ email: EmailStr = Field(max_length=120)
+ password: str = Field(min_length=8, max_length=100)
+ role: str = Field(default="booker", pattern="^(booker|owner|admin)$")
+
+
+class UserLogin(BaseModel):
+ email: EmailStr = Field(max_length=120)
+ password: str = Field(min_length=8, max_length=100)
+
+class UserOut(BaseModel):
+ id: int
+ name: str
+ email: EmailStr
+ role: str
+ created_at: datetime
+
+ model_config = ConfigDict(from_attributes=True)
+
+class Token(BaseModel):
+ access_token: str
+ token_type: str
\ No newline at end of file
diff --git a/bookmyvenue/services/api/app/schemas/availability.py b/bookmyvenue/services/api/app/schemas/availability.py
new file mode 100644
index 0000000000..35bfde2193
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/availability.py
@@ -0,0 +1,53 @@
+from typing_extensions import Self
+from datetime import date, time
+from pydantic import BaseModel, Field, model_validator
+from enum import Enum
+from typing import List, Dict, Optional
+
+
+class BookingTypeEnum(str, Enum):
+ HOURLY = "hourly"
+ DAILY = "daily"
+
+
+class AvailabilitySlotCreate(BaseModel):
+ start_time: time
+ end_time: time
+
+
+class AvailabilityCreate(BaseModel):
+ venue_id: int
+ date: date
+ booking_type: BookingTypeEnum
+ start_time: Optional[time] = None
+ end_time: Optional[time] = None
+ slots: Optional[List[AvailabilitySlotCreate]] = None
+
+ @model_validator(mode="after")
+ def booking_type_validator(self) -> Self:
+ if self.booking_type == BookingTypeEnum.HOURLY:
+ if not self.slots:
+ raise ValueError(
+ "If booking type is hourly, slots must not be empty"
+ )
+ if self.booking_type == BookingTypeEnum.DAILY:
+ if self.slots:
+ raise ValueError(
+ "If booking type is daily, slots must be empty"
+ )
+ return self
+
+class AvailabilityOut(BaseModel):
+ id: int
+ venue_id: int
+ date: date
+ booking_type: BookingTypeEnum
+ start_time: Optional[time] = None
+ end_time: Optional[time] = None
+ is_booked: bool
+ model_config = {"from_attributes": True}
+
+class AvailabilityByDateResponse(BaseModel):
+ date: date
+ booking_type: BookingTypeEnum
+ slots: List[AvailabilityOut]
diff --git a/bookmyvenue/services/api/app/schemas/booking.py b/bookmyvenue/services/api/app/schemas/booking.py
new file mode 100644
index 0000000000..25e4143a57
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/booking.py
@@ -0,0 +1,72 @@
+from datetime import date, datetime, time
+from pydantic import BaseModel, Field, model_validator
+from typing import List, Optional
+from enum import Enum
+
+from models.booking import BookingTypeEnum, BookingStatusEnum, PaymentEnum
+
+
+class BookingCreate(BaseModel):
+ venue_id: int
+ availability_ids: List[int] = Field(min_length=1)
+
+
+class BookingStatusUpdate(BaseModel):
+ status: BookingStatusEnum
+
+
+class BookingSlotOut(BaseModel):
+ id: int
+ availability_id: int
+ date: date
+ start_time: Optional[time] = None
+ end_time: Optional[time] = None
+ booking_type: BookingTypeEnum
+ is_booked: bool
+
+ model_config = {"from_attributes": True}
+
+ @model_validator(mode="before")
+ @classmethod
+ def populate_from_availability(cls, data):
+ if hasattr(data, "availability") and data.availability is not None:
+ avail = data.availability
+ cls_dict = {
+ "id": data.id,
+ "availability_id": data.availability_id,
+ "date": avail.date,
+ "start_time": avail.start_time,
+ "end_time": avail.end_time,
+ "booking_type": avail.booking_type,
+ "is_booked": avail.is_booked,
+ }
+ return cls_dict
+ return data
+
+
+class BookingOut(BaseModel):
+ id: int
+ venue_id: int
+ venue_name: str = ""
+ booker_id: int
+ booker_name: str = ""
+ booking_type: BookingTypeEnum
+ base_price: float
+ tax_amount: float
+ platform_fee: float
+ total_amount: float
+ status: BookingStatusEnum
+ payment_status: PaymentEnum
+ created_at: datetime
+ slots: List[BookingSlotOut]
+
+ model_config = {"from_attributes": True}
+
+ @model_validator(mode="before")
+ @classmethod
+ def populate_names(cls, data):
+ if hasattr(data, "venue") and data.venue is not None:
+ data.venue_name = data.venue.name
+ if hasattr(data, "booker") and data.booker is not None:
+ data.booker_name = data.booker.name or ""
+ return data
diff --git a/bookmyvenue/services/api/app/schemas/venue.py b/bookmyvenue/services/api/app/schemas/venue.py
new file mode 100644
index 0000000000..ff022a862b
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/venue.py
@@ -0,0 +1,100 @@
+from typing_extensions import Self
+from datetime import datetime
+from pydantic import BaseModel, Field, PositiveInt, model_validator
+from typing import Optional, List
+from .venue_image import VenueImageOut
+from .venue_category import VenueCategoryOut
+from models.venue import StatusEnum
+from pydantic import field_serializer
+
+
+class VenueCreate(BaseModel):
+ name: str = Field(max_length=100, description="required field")
+ description: str = Field(max_length=1000, description="required field")
+ category_id: int = Field(description="required field")
+ address_line: str = Field(max_length=500, description="required field")
+ city: str = Field(max_length=100, description="required field")
+ pincode: str = Field(min_length=6, max_length=6,
+ description="required field")
+ capacity: PositiveInt = Field(le=200000)
+ supports_hourly: bool = Field(default=False)
+ supports_daily: bool = Field(default=False)
+ hourly_price: Optional[float] = Field(default=None)
+ daily_price: Optional[float] = Field(default=None)
+ amenities: str = Field(max_length=500, description="required field")
+ cancellation_policy: Optional[str] = Field(default=None)
+ image_urls: List[str] = Field(default_factory=list)
+
+ @model_validator(mode='after')
+ def pay_type_validator(self) -> Self:
+
+ if self.supports_hourly is None and self.supports_daily is None:
+ raise ValueError(
+ "Both supports_hourly and supports_daily can't be False at same time")
+ if self.supports_hourly and not self.hourly_price:
+ raise ValueError(
+ "hourly_price required when supports_hourly is True")
+ if self.supports_daily and not self.daily_price:
+ raise ValueError(
+ "daily_price required when supports_daily is True")
+ return self
+
+
+class VenueUpdate(BaseModel):
+ name: Optional[str] = Field(default=None, max_length=100)
+ description: Optional[str] = Field(default=None, max_length=1000)
+ category_id: Optional[int] = None
+ address_line: Optional[str] = Field(default=None, max_length=500)
+ city: Optional[str] = Field(default=None, max_length=100)
+ pincode: Optional[str] = Field(default=None, min_length=6, max_length=6)
+ capacity: Optional[PositiveInt] = Field(default=None, le=200000)
+ supports_hourly: Optional[bool] = None
+ supports_daily: Optional[bool] = None
+ hourly_price: Optional[float] = None
+ daily_price: Optional[float] = None
+ amenities: Optional[str] = Field(default=None, max_length=500)
+ cancellation_policy: Optional[str] = None
+
+
+class VenueOut(BaseModel):
+ id: int
+ owner_id: int
+ category_id: int
+ name: str
+ description: str
+ address_line: str
+ city: str
+ pincode: str
+ capacity: PositiveInt
+ supports_hourly: bool
+ supports_daily: bool
+ hourly_price: Optional[float] = None
+ daily_price: Optional[float] = None
+ amenities: str
+ cancellation_policy: Optional[str] = None
+ status: str = "active"
+ images: list[VenueImageOut] = Field(default_factory=list)
+
+ model_config = {"from_attributes": True}
+
+
+class VenuePaginatedResponse(BaseModel):
+ items: list[VenueOut]
+ total: int
+ page: int
+ limit: int
+
+
+class CategoryWithVenues(BaseModel):
+ category: VenueCategoryOut
+ venues: list[VenueOut]
+
+
+class HomePageResponse(BaseModel):
+ top_rated: list[VenueOut]
+ recently_added: list[VenueOut]
+ categories: list[CategoryWithVenues]
+
+
+class VenueStatusUpdate(BaseModel):
+ status: StatusEnum
diff --git a/bookmyvenue/services/api/app/schemas/venue_category.py b/bookmyvenue/services/api/app/schemas/venue_category.py
new file mode 100644
index 0000000000..4c959eaba1
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/venue_category.py
@@ -0,0 +1,17 @@
+from datetime import datetime
+from pydantic import BaseModel, Field
+
+
+class VenueCategoryCreate(BaseModel):
+ name: str = Field(description='required field')
+ slug: str = Field(description='required field')
+
+
+class VenueCategoryOut(BaseModel):
+ id: int
+ name: str
+ slug: str
+ is_active: bool
+ created_at: datetime
+
+ model_config = {"from_attributes": True}
diff --git a/bookmyvenue/services/api/app/schemas/venue_image.py b/bookmyvenue/services/api/app/schemas/venue_image.py
new file mode 100644
index 0000000000..b21fd92915
--- /dev/null
+++ b/bookmyvenue/services/api/app/schemas/venue_image.py
@@ -0,0 +1,11 @@
+from pydantic import BaseModel, Field
+
+
+class VenueImageOut(BaseModel):
+
+ id: int
+ venue_id: int
+ image_url: str
+ display_order: int
+
+ model_config = {"from_attributes": True}
diff --git a/bookmyvenue/services/api/app/seed.py b/bookmyvenue/services/api/app/seed.py
new file mode 100644
index 0000000000..a4d3a90193
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed.py
@@ -0,0 +1,34 @@
+from sqlalchemy.orm import Session
+
+from database import SessionLocal
+
+from seed.seed_categories import seed_categories
+from seed.seed_users import seed_users
+from seed.seed_venues import seed_venues
+from seed.seed_images import seed_images
+from seed.seed_availability import seed_availability
+from seed.seed_bookings import seed_bookings
+
+
+def seed_database():
+
+ db: Session = SessionLocal()
+
+ try:
+ print("Starting database seeding...\n")
+
+ # seed_categories(db)
+ # seed_users(db)
+ # seed_venues(db)
+ # seed_images(db)
+ # seed_availability(db)
+ seed_bookings(db)
+
+ print("\nDatabase seeded successfully.")
+
+ finally:
+ db.close()
+
+
+if __name__ == "__main__":
+ seed_database()
diff --git a/bookmyvenue/services/api/app/seed/__init__.py b/bookmyvenue/services/api/app/seed/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/seed/seed_availability.py b/bookmyvenue/services/api/app/seed/seed_availability.py
new file mode 100644
index 0000000000..1e07568581
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_availability.py
@@ -0,0 +1,86 @@
+from datetime import date, timedelta, time
+
+from sqlalchemy.orm import Session
+
+from models.availability import Availability
+from models.booking import BookingTypeEnum
+from models.venue import Venue
+
+HOURLY_SLOTS = [
+ (time(9, 0), time(10, 0)),
+ (time(10, 0), time(11, 0)),
+ (time(11, 0), time(12, 0)),
+ (time(12, 0), time(13, 0)),
+ (time(14, 0), time(15, 0)),
+ (time(15, 0), time(16, 0)),
+ (time(16, 0), time(17, 0)),
+ (time(17, 0), time(18, 0)),
+]
+
+
+def seed_availability(db: Session):
+
+ print("Seeding availability...")
+
+ venues = db.query(Venue).all()
+
+ created = 0
+
+ today = date.today()
+
+ for venue in venues:
+
+ for day in range(30):
+
+ current_date = today + timedelta(days=day)
+
+ # Skip if already seeded
+ existing = (
+ db.query(Availability)
+ .filter(
+ Availability.venue_id == venue.id,
+ Availability.date == current_date
+ )
+ .first()
+ )
+
+ if existing:
+ continue
+
+ # DAILY
+ if venue.supports_daily:
+
+ db.add(
+ Availability(
+ venue_id=venue.id,
+ date=current_date,
+ booking_type=BookingTypeEnum.DAILY,
+ start_time=None,
+ end_time=None,
+ is_booked=False
+ )
+ )
+
+ created += 1
+
+ # HOURLY
+ if venue.supports_hourly:
+
+ for start, end in HOURLY_SLOTS:
+
+ db.add(
+ Availability(
+ venue_id=venue.id,
+ date=current_date,
+ booking_type=BookingTypeEnum.HOURLY,
+ start_time=start,
+ end_time=end,
+ is_booked=False
+ )
+ )
+
+ created += 1
+
+ db.commit()
+
+ print(f"✓ {created} availability rows created.")
diff --git a/bookmyvenue/services/api/app/seed/seed_bookings.py b/bookmyvenue/services/api/app/seed/seed_bookings.py
new file mode 100644
index 0000000000..745e27cb6f
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_bookings.py
@@ -0,0 +1,156 @@
+import random
+
+from sqlalchemy.orm import Session
+
+from models.user import User, RoleEnum
+from models.venue import Venue
+from models.availability import Availability
+from models.booking import (
+ Booking,
+ BookingTypeEnum,
+ BookingStatusEnum,
+ PaymentEnum,
+)
+from models.booking_slot import BookingSlot
+
+from config import TAX_PERCENT, PLATFORM_FEE
+
+
+def seed_bookings(db: Session):
+
+ print("Seeding bookings...")
+
+ users = (
+ db.query(User)
+ .filter(User.role == RoleEnum.BOOKER)
+ .all()
+ )
+
+ venues = db.query(Venue).all()
+
+ created = 0
+
+ # create roughly 70 bookings
+ for _ in range(70):
+
+ venue = random.choice(venues)
+ user = random.choice(users)
+
+ booking_type = random.choice(
+ [
+ BookingTypeEnum.HOURLY,
+ BookingTypeEnum.DAILY,
+ ]
+ )
+
+ # venue must support booking type
+ if booking_type == BookingTypeEnum.HOURLY and not venue.supports_hourly:
+ continue
+
+ if booking_type == BookingTypeEnum.DAILY and not venue.supports_daily:
+ continue
+
+ available_slots = (
+ db.query(Availability)
+ .filter(
+ Availability.venue_id == venue.id,
+ Availability.booking_type == booking_type,
+ Availability.is_booked == False,
+ )
+ .order_by(Availability.date, Availability.start_time)
+ .all()
+ )
+
+ if not available_slots:
+ continue
+
+ if booking_type == BookingTypeEnum.DAILY:
+
+ selected_slots = [random.choice(available_slots)]
+
+ else:
+
+ slot_count = random.randint(1, 3)
+
+ if len(available_slots) < slot_count:
+ continue
+
+ selected_slots = random.sample(
+ available_slots,
+ slot_count,
+ )
+
+ number_of_slots = len(selected_slots)
+
+ if booking_type == BookingTypeEnum.HOURLY:
+ base_price = venue.hourly_price * number_of_slots
+ else:
+ base_price = venue.daily_price * number_of_slots
+
+ tax_amount = base_price * (TAX_PERCENT / 100)
+
+ platform_fee = PLATFORM_FEE
+
+ total_amount = (
+ base_price
+ + tax_amount
+ + platform_fee
+ )
+
+ status = random.choices(
+ [
+ BookingStatusEnum.PENDING,
+ BookingStatusEnum.CONFIRMED,
+ BookingStatusEnum.REJECTED,
+ ],
+ weights=[20, 60, 20],
+ )[0]
+
+ payment = (
+ PaymentEnum.PAID
+ if status == BookingStatusEnum.CONFIRMED
+ else random.choice(
+ [
+ PaymentEnum.UNPAID,
+ PaymentEnum.PAID,
+ ]
+ )
+ )
+
+ booking = Booking(
+ venue_id=venue.id,
+ booker_id=user.id,
+ booking_type=booking_type,
+ base_price=base_price,
+ tax_amount=tax_amount,
+ platform_fee=platform_fee,
+ total_amount=total_amount,
+ status=status,
+ payment_status=payment,
+ )
+
+ db.add(booking)
+
+ db.flush()
+
+ for slot in selected_slots:
+
+ slot.is_booked = True
+
+ db.add(
+ BookingSlot(
+ booking_id=booking.id,
+ availability_id=slot.id,
+ )
+ )
+
+ if status == BookingStatusEnum.REJECTED:
+
+ for slot in selected_slots:
+ slot.is_booked = False
+
+ created += 1
+
+ db.commit()
+
+ print(f"✓ {created} bookings created.")
diff --git a/bookmyvenue/services/api/app/seed/seed_categories.py b/bookmyvenue/services/api/app/seed/seed_categories.py
new file mode 100644
index 0000000000..c381ccb664
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_categories.py
@@ -0,0 +1,43 @@
+from sqlalchemy.orm import Session
+
+from models.venue_category import VenueCategory
+
+
+CATEGORIES = [
+ ("Wedding Hall", "wedding-hall"),
+ ("Conference Hall", "conference-hall"),
+ ("Auditorium", "auditorium"),
+ ("Banquet Hall", "banquet-hall"),
+ ("Meeting Room", "meeting-room"),
+ ("Sports Ground", "sports-ground"),
+ ("Party Hall", "party-hall"),
+ ("Studio", "studio"),
+]
+
+
+def seed_categories(db: Session):
+
+ print("Seeding categories...")
+
+ for name, slug in CATEGORIES:
+
+ existing = (
+ db.query(VenueCategory)
+ .filter(VenueCategory.slug == slug)
+ .first()
+ )
+
+ if existing:
+ continue
+
+ category = VenueCategory(
+ name=name,
+ slug=slug,
+ is_active=True,
+ )
+
+ db.add(category)
+
+ db.commit()
+
+ print(f"✓ {len(CATEGORIES)} categories ready.")
diff --git a/bookmyvenue/services/api/app/seed/seed_images.py b/bookmyvenue/services/api/app/seed/seed_images.py
new file mode 100644
index 0000000000..d8c8934394
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_images.py
@@ -0,0 +1,58 @@
+import random
+
+from sqlalchemy.orm import Session
+
+from models.venue import Venue
+from models.venue_image import VenueImage
+IMAGE_URLS = [
+ "https://images.unsplash.com/photo-1519167758481-83f550bb49b3",
+ "https://images.unsplash.com/photo-1464366400600-7168b8af9bc3",
+ "https://images.unsplash.com/photo-1511578314322-379afb476865",
+ "https://images.unsplash.com/photo-1505236858219-8359eb29e329",
+ "https://images.unsplash.com/photo-1492684223066-81342ee5ff30",
+ "https://images.unsplash.com/photo-1517457373958-b7bdd4587205",
+ "https://images.unsplash.com/photo-1528605248644-14dd04022da1",
+ "https://images.unsplash.com/photo-1505373877841-8d25f7d46678",
+ "https://images.unsplash.com/photo-1511795409834-ef04bbd61622",
+ "https://images.unsplash.com/photo-1478145046317-39f10e56b5e9",
+]
+
+
+def seed_images(db: Session):
+
+ print("Seeding venue images...")
+
+ venues = db.query(Venue).all()
+
+ created = 0
+
+ for venue in venues:
+
+ existing = (
+ db.query(VenueImage)
+ .filter(VenueImage.venue_id == venue.id)
+ .count()
+ )
+
+ if existing:
+ continue
+
+ image_count = random.randint(4, 6)
+
+ selected_images = random.sample(IMAGE_URLS, image_count)
+
+ for order, image in enumerate(selected_images):
+
+ db.add(
+ VenueImage(
+ venue_id=venue.id,
+ image_url=image,
+ display_order=order
+ )
+ )
+
+ created += 1
+
+ db.commit()
+
+ print(f"✓ {created} images created.")
diff --git a/bookmyvenue/services/api/app/seed/seed_users.py b/bookmyvenue/services/api/app/seed/seed_users.py
new file mode 100644
index 0000000000..e140cdd3c4
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_users.py
@@ -0,0 +1,83 @@
+from sqlalchemy.orm import Session
+
+from models.user import User, RoleEnum
+from utils.hashing import hash_password
+
+
+PASSWORD = "Password@123"
+
+
+def seed_users(db: Session):
+
+ print("Seeding users...")
+
+ users = [
+ {
+ "name": "System Administrator",
+ "email": "admin@example.com",
+ "role": RoleEnum.ADMIN,
+ }
+ ]
+
+ # Owners
+ for i in range(1, 6):
+ users.append(
+ {
+ "name": f"Owner {i}",
+ "email": f"owner{i}@example.com",
+ "role": RoleEnum.OWNER,
+ }
+ )
+
+ # Bookers
+ for i in range(1, 11):
+ users.append(
+ {
+ "name": f"User {i}",
+ "email": f"user{i}@example.com",
+ "role": RoleEnum.BOOKER,
+ }
+ )
+
+ created = 0
+
+ for user in users:
+
+ existing = (
+ db.query(User)
+ .filter(User.email == user["email"])
+ .first()
+ )
+
+ if existing:
+ continue
+
+ db.add(
+ User(
+ name=user["name"],
+ email=user["email"],
+ password_hash=hash_password(PASSWORD),
+ role=user["role"],
+ )
+ )
+
+ created += 1
+
+ db.commit()
+
+ print(f"✓ {created} users created.")
+
+ print("\nLogin Credentials")
+ print("-----------------------------")
+ print(f"Password for all accounts: {PASSWORD}\n")
+
+ print("ADMIN")
+ print("admin@example.com\n")
+
+ print("OWNERS")
+ for i in range(1, 6):
+ print(f"owner{i}@example.com")
+
+ print("\nBOOKERS")
+ for i in range(1, 11):
+ print(f"user{i}@example.com")
diff --git a/bookmyvenue/services/api/app/seed/seed_venues.py b/bookmyvenue/services/api/app/seed/seed_venues.py
new file mode 100644
index 0000000000..bc38923ac6
--- /dev/null
+++ b/bookmyvenue/services/api/app/seed/seed_venues.py
@@ -0,0 +1,136 @@
+import random
+
+from sqlalchemy.orm import Session
+
+from models.user import User, RoleEnum
+from models.venue import Venue, StatusEnum
+from models.venue_category import VenueCategory
+
+
+VENUE_NAMES = [
+ "Royal Palace",
+ "Grand Arena",
+ "Emerald Hall",
+ "Blue Orchid",
+ "Sky Convention Centre",
+ "Golden Banquet",
+ "Lotus Auditorium",
+ "Galaxy Events",
+ "Sunrise Venue",
+ "Crystal Ballroom",
+ "Elite Convention Hall",
+ "Green Valley Resort",
+ "Majestic Hall",
+ "Imperial Banquet",
+ "Harmony Events",
+ "Prime Sports Arena",
+ "Celebration Hall",
+ "Infinity Convention",
+ "Pearl Wedding Centre",
+ "City Conference Hub",
+]
+
+CITIES = [
+ "Cochin",
+ "Trivandrum",
+ "Kannur",
+ "Kollam",
+ "Munnar",
+ "Alappy",
+]
+
+AMENITIES = [
+ "Parking, WiFi, Air Conditioning",
+ "Parking, Catering, Sound System",
+ "WiFi, Stage, Projector",
+ "Parking, Generator Backup",
+ "Projector, Whiteboard, AC",
+ "Changing Rooms, Parking",
+]
+
+CANCELLATION = [
+ "Full refund up to 7 days before booking.",
+ "50% refund up to 3 days before booking.",
+ "No refund within 24 hours.",
+]
+
+DESCRIPTIONS = [
+ "A spacious premium venue suitable for weddings and celebrations.",
+ "Ideal for conferences, seminars and business meetings.",
+ "Modern venue with premium amenities and ample parking.",
+ "Perfect for birthday parties, receptions and corporate events.",
+]
+
+
+def seed_venues(db: Session):
+
+ print("Seeding venues...")
+
+ owners = (
+ db.query(User)
+ .filter(User.role == RoleEnum.OWNER)
+ .all()
+ )
+
+ categories = db.query(VenueCategory).all()
+
+ created = 0
+
+ for owner in owners:
+
+ number_of_venues = random.randint(4, 7)
+
+ for i in range(number_of_venues):
+
+ venue_name = random.choice(VENUE_NAMES)
+
+ if (
+ db.query(Venue)
+ .filter(Venue.name == venue_name)
+ .first()
+ ):
+ continue
+
+ booking_type = random.choice(
+ [
+ "hourly",
+ "daily",
+ "both",
+ ]
+ )
+
+ supports_hourly = booking_type in ["hourly", "both"]
+ supports_daily = booking_type in ["daily", "both"]
+
+ venue = Venue(
+ owner_id=owner.id,
+ category_id=random.choice(categories).id,
+ name=venue_name,
+ description=random.choice(DESCRIPTIONS),
+ address_line=f"{random.randint(10, 400)} Main Street",
+ city=random.choice(CITIES),
+ pincode=str(random.randint(100000, 999999)),
+ capacity=random.randint(30, 800),
+
+ supports_hourly=supports_hourly,
+ supports_daily=supports_daily,
+
+ hourly_price=random.choice(
+ [500, 750, 1000, 1200, 1500, 2000]
+ ) if supports_hourly else None,
+
+ daily_price=random.choice(
+ [5000, 7000, 9000, 12000, 15000]
+ ) if supports_daily else None,
+
+ amenities=random.choice(AMENITIES),
+ cancellation_policy=random.choice(CANCELLATION),
+ status=StatusEnum.ACTIVE,
+ )
+
+ db.add(venue)
+ created += 1
+
+ db.commit()
+
+ print(f"✓ {created} venues created.")
diff --git a/bookmyvenue/services/api/app/utils/__init__.py b/bookmyvenue/services/api/app/utils/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bookmyvenue/services/api/app/utils/dependencies.py b/bookmyvenue/services/api/app/utils/dependencies.py
new file mode 100644
index 0000000000..77a12b5cbb
--- /dev/null
+++ b/bookmyvenue/services/api/app/utils/dependencies.py
@@ -0,0 +1,59 @@
+from typing import Annotated
+from fastapi.security import OAuth2PasswordBearer
+from fastapi import Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from sqlalchemy import select
+
+from models.user import User, RoleEnum
+from database import get_db
+from utils.jwt import verify_access_token
+
+
+
+oauth2_sheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
+
+
+def get_current_user(
+ token: Annotated[str, Depends(oauth2_sheme)],
+ db: Annotated[Session, Depends(get_db)], ) -> User:
+ user_id = verify_access_token(token)
+ if user_id is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid or expired token",
+ headers={"WWW-Authenticate": "Bearer"}
+ )
+
+ try:
+ user_id_int = int(user_id)
+ except (TypeError, ValueError):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid or expired token",
+ headers={"WWW-Authenticate": "Bearer"}
+ )
+ result = db.execute(
+ select(User).where(User.id == user_id_int),
+ )
+ user = result.scalars().first()
+
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User not found",
+ headers={"WWW-Authenticate": "Bearer"}
+ )
+ return user
+
+
+
+def require_role(required_role: RoleEnum):
+ def role_checker(current_user: User = Depends(get_current_user)):
+ if current_user.role != required_role:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="You are not allowed to perform this action"
+ )
+ return current_user
+
+ return role_checker
\ No newline at end of file
diff --git a/bookmyvenue/services/api/app/utils/hashing.py b/bookmyvenue/services/api/app/utils/hashing.py
new file mode 100644
index 0000000000..7686efcdeb
--- /dev/null
+++ b/bookmyvenue/services/api/app/utils/hashing.py
@@ -0,0 +1,9 @@
+from passlib.hash import bcrypt
+
+
+def hash_password(password: str) -> str:
+ return bcrypt.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ return bcrypt.verify(plain_password, hashed_password)
diff --git a/bookmyvenue/services/api/app/utils/jwt.py b/bookmyvenue/services/api/app/utils/jwt.py
new file mode 100644
index 0000000000..2f9cb3964f
--- /dev/null
+++ b/bookmyvenue/services/api/app/utils/jwt.py
@@ -0,0 +1,42 @@
+from jose import jwt
+from pydantic import ValidationError
+from datetime import UTC, datetime, timedelta
+from typing import Optional
+import os
+
+SECRET_KEY = os.getenv("SECRET_KEY", default="mysecretkey")
+ALGORITHM = os.getenv("ALGORITHM", default="HS256")
+ACCESS_TOKEN_EXPIRE_MINUTES = int(
+ os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", default=30))
+
+
+def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.now(UTC) + expires_delta
+ else:
+ expire = datetime.now(UTC) + timedelta(
+ minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+
+ to_encode.update({"exp": expire})
+
+ encoded_jwt = jwt.encode(
+ to_encode,
+ SECRET_KEY,
+ algorithm=ALGORITHM
+ )
+ return encoded_jwt
+
+
+def verify_access_token(token: str) -> str | None:
+ try:
+ payload = jwt.decode(
+ token,
+ SECRET_KEY,
+ ALGORITHM,
+ options={"require": ["exp", "sub"]}
+ )
+ except ValidationError:
+ return None
+ else:
+ return payload.get("sub")
diff --git a/bookmyvenue/services/api/requirements.txt b/bookmyvenue/services/api/requirements.txt
new file mode 100644
index 0000000000..74a1086fb0
--- /dev/null
+++ b/bookmyvenue/services/api/requirements.txt
@@ -0,0 +1,31 @@
+alembic==1.18.4
+annotated-doc==0.0.4
+annotated-types==0.7.0
+anyio==4.13.0
+bcrypt==4.3.0
+click==8.4.1
+colorama==0.4.6
+dnspython==2.8.0
+ecdsa==0.19.2
+email-validator==2.3.0
+fastapi==0.136.3
+greenlet==3.5.1
+h11==0.16.0
+idna==3.17
+Mako==1.3.12
+MarkupSafe==3.0.3
+passlib==1.7.4
+psycopg2-binary==2.9.12
+pyasn1==0.6.3
+pydantic==2.13.4
+pydantic_core==2.46.4
+python-dotenv==1.2.2
+python-jose==3.5.0
+python-multipart==0.0.32
+rsa==4.9.1
+six==1.17.0
+SQLAlchemy==2.0.50
+starlette==1.2.0
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+uvicorn==0.48.0
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..724985a7b5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "bookmyvenue",
+ "version": "1.0.0",
+ "description": "BookMyVenue is a 100% open-source initiative built by the **WeCode community**, designed to simplify the process of finding and booking venues in local areas. This is a platform built for the community, by the community. Whether you're looking for a cozy cafe for a quick meetup, a spacious auditorium for a community event, or a scenic outdoor space for a personal celebration, BookMyVenue aims to connect people with the perfect space—without the commercial overhead.",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/AswanthAnu/BookMyVenue.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "bugs": {
+ "url": "https://github.com/AswanthAnu/BookMyVenue/issues"
+ },
+ "homepage": "https://github.com/AswanthAnu/BookMyVenue#readme"
+}