diff --git a/README.md b/README.md index 0a3dbf5..5696ffd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # 🎉 Eventique – Event Management Website -Welcome to **Eventique**, a stylish and modern event management platform built using React + Vite + Tailwind CSS + Firebase. -Live Demo: 👉 [https://neweventique.netlify.app](https://neweventique.netlify.app) - ---- - ## 🌟 Overview Eventique is a fast, elegant, and responsive event management frontend built with modern tools. @@ -56,35 +51,4 @@ npm install npm run dev ``` ---- - -## 📸 Screenshots - -### 🔹 Home page -![Home page](src/assets/Home_page_before_login.png) - -### 🔹 Home page Logged In -![Home page](src/assets/Home_page_after_login.png) - -### 🔹 Services -![services](src/assets/services_page.png) - --- - -## 🌐 Live Demo -👉 [https://neweventique.netlify.app](https://neweventique.netlify.app) - ---- - -## 🙌 Acknowledgements - -Built with ❤️ using React, Vite, and Tailwind CSS as part of a personal web development project. - ---- - -## ⭐ Show Your Support - -If you like this project, give it a ⭐ on GitHub or share it! - ---- diff --git a/package-lock.json b/package-lock.json index f7b6883..abd8012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,10 @@ "name": "event-management-ui", "version": "0.0.0", "dependencies": { + "axios": "^1.13.2", "firebase": "^12.0.0", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.556.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hot-toast": "^2.5.2", @@ -699,6 +702,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.0.tgz", "integrity": "sha512-APIAeKvRNFWKJLjIL8wLDjh7u8g6ZjaeVmItyqSjCdEkJj14UuVlus74D8ofsOMWh45HEwxwkd96GYbi+CImEg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -765,6 +769,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.0.tgz", "integrity": "sha512-nUnNpOeRj0KZzVzHsyuyrmZKKHfykZ8mn40FtG28DeSTWeM5b/2P242Va4bmQpJsy5y32vfv50+jvdckrpzy7Q==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.0", "@firebase/component": "0.7.0", @@ -780,7 +785,8 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.11.0", @@ -1231,6 +1237,7 @@ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2053,6 +2060,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2087,6 +2095,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2197,6 +2206,12 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -2235,6 +2250,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2299,6 +2325,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2312,6 +2339,19 @@ "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2498,6 +2538,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2556,7 +2608,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.1", @@ -2583,6 +2636,15 @@ "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/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2597,6 +2659,20 @@ "dev": true, "license": "MIT" }, + "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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2618,6 +2694,51 @@ "dev": true, "license": "MIT" }, + "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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -2687,6 +2808,7 @@ "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3047,6 +3169,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3064,6 +3206,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3097,7 +3255,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3112,6 +3269,43 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3194,6 +3388,18 @@ "csstype": "^3.0.10" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3204,11 +3410,37 @@ "node": ">=8" } }, + "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.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3366,6 +3598,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -3404,6 +3637,15 @@ "dev": true, "license": "MIT" }, + "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", @@ -3490,6 +3732,24 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.556.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.556.0.tgz", + "integrity": "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3527,6 +3787,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3769,6 +4050,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3816,6 +4098,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3980,6 +4263,12 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4016,6 +4305,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4025,6 +4315,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -4687,6 +4978,7 @@ "integrity": "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", diff --git a/package.json b/package.json index 8b8e760..a536756 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.13.2", "firebase": "^12.0.0", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.556.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hot-toast": "^2.5.2", diff --git a/public/icono.png b/public/icono.png new file mode 100644 index 0000000..39b2eb8 Binary files /dev/null and b/public/icono.png differ diff --git a/public/logo.png b/public/logo.png index c6b6b10..9c50812 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/public/servicesData.json b/public/servicesData.json index 273badb..2241f89 100644 --- a/public/servicesData.json +++ b/public/servicesData.json @@ -70,14 +70,5 @@ "cost": "Rs 22,000 onwards", "location": "Pune, Maharashtra", "duration": "3-5 hours" - }, - { - "id": 9, - "image": "https://dittonmanor.com/wp-content/uploads/2024/09/how-to-plan-a-christmas-party.jpg", - "title": "Festive Theme Parties", - "description": "Celebrate festivals like Diwali, Christmas, or Holi with custom-themed decorations, joyful activities, snacks, and giveaways for guests.", - "cost": "Rs 30,000 onwards", - "location": "Ahmedabad, Gujarat", - "duration": "Evening" } ] diff --git a/src/App.css b/src/App.css index e69de29..fec81a4 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,15 @@ +/* Animación para el snackbar */ +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-down { + animation: fadeInDown 0.3s ease-out; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1f9b4d7..e25ffd7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,38 +1,133 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; import Categories from './pages/Categories'; import Contact from './pages/Contact'; import Layout from './components/Layout'; import Home from './pages/Home'; -import Services from './pages/Services'; import SignIn from './pages/SignIn'; import EventDetails from './pages/EventDetails'; import SignUp from './pages/SignUp'; import Profile from './pages/Profile'; import Cart from './pages/Cart'; +import ClientRoute from './components/ClienteRoute'; + +import AdminRoute from './components/AdminRoute'; +import AdminLayout from './pages/admin/Layout'; +import AdminDashboard from './pages/admin/dashboard'; +import EventsList from './pages/admin/events/index'; +import CreateEvent from './pages/admin/events/create'; +import EditEvent from './pages/admin/events/edit'; + +import CreateEstablishment from './pages/admin/establishments/create'; +import EstablishmentsList from './pages/admin/establishments/index'; +import EditEstablishment from './pages/admin/establishments/edit'; + +import CreateEntertainment from './pages/admin/entertainment/create'; +import EntertainmentsList from './pages/admin/entertainment/index'; +import EditEntertainment from './pages/admin/entertainment/edit'; + + +import CreateCatering from './pages/admin/catering/create'; +import CateringsList from './pages/admin/catering/index'; +import EditCatering from './pages/admin/catering/edit'; + +import AdditionalList from './pages/admin/additional/index'; +import CreateAdditional from './pages/admin/additional/create'; +import EditAdditional from './pages/admin/additional/edit'; + +import DecorationList from './pages/admin/decoration/index'; +import CreateDecoration from './pages/admin/decoration/create'; +import EditDecoration from './pages/admin/decoration/edit'; + +import ClientList from './pages/admin/client/clientelist'; + +import Services from "./pages/Services"; +import CategoryServices from "./pages/CategoriesServices"; + +import ReservationForm from './pages/ReservationForm'; + + import './App.css' function App() { - - return( - - + return ( + - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> + {/* Rutas públicas */} + } /> + } /> + + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Ruta de reserva solo para clientes */} + + + + } + /> + + {/* Rutas admin*/} + + + + } + > + + } /> + } /> + + {/* Rutas para Eventos */} + } /> + } /> + } /> + + {/* Rutas para Establecimientos */} + } /> + } /> + } /> + + {/* Rutas para Entretenimiento */} + } /> + } /> + } /> + + {/* Rutas para Catering */} + } /> + } /> + } /> + + {/* Rutas para Adicionales */} + } /> + } /> + } /> + + + {/* Rutas para decoración */} + } /> + } /> + } /> + + {/* Rutas para obtener clientes */} + } /> + - - - ) - + + ); } -export default App +export default App; \ No newline at end of file diff --git a/src/api/additional.js b/src/api/additional.js new file mode 100644 index 0000000..b56e149 --- /dev/null +++ b/src/api/additional.js @@ -0,0 +1,51 @@ +import api from './axios'; + +export const getAdditionals = async () => { + try { + const response = await api.get('/additional'); + return response.data; + } catch (error) { + console.error("Error fetching additionals:", error); + throw error; + } + }; + +export const getAdditionalById = async (id) => { + try { + const response = await api.get(`/additional/${id}`); + return response.data; + } catch (error) { + console.error("Error fetching additionals:", error); + throw error; + } +}; + +export const createAdditional = async (additionalData) => { + try { + const response = await api.post('/additional', additionalData); + return response.data; + } catch (error) { + console.error("Error creating additional:", error); + throw error; + } +}; + +export const updateAdditional = async (id, additionalData) => { + try { + const response = await api.put(`/additional/${id}`, additionalData); + return response.data; + } catch (error) { + console.error("Error updating additional:", error); + throw error; + } +}; + +export const deleteAdditional = async (id) => { + try { + const response = await api.delete(`/additionals/${id}`); + return response.data; + } catch (error) { + console.error("Error deleting additional:", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..11008b9 --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,7 @@ +import api from './axios'; + +export const loginRequest = (email, password) => + api.post('/User/login', { email, password }); + +export const registerRequest = (userData) => + api.post('/User/register', userData); diff --git a/src/api/axios.js b/src/api/axios.js new file mode 100644 index 0000000..060bd0d --- /dev/null +++ b/src/api/axios.js @@ -0,0 +1,46 @@ +import axios from 'axios'; + +const API_BASE_URL = 'http://localhost:8081'; + +const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + } +}); + +// Interceptor para el token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + console.log('🔐 Token agregado automáticamente'); + } + return config; + }, + (error) => { + console.error('❌ Error en interceptor:', error); + return Promise.reject(error); + } +); + +// Interceptor para errores +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + console.log('🔒 Token inválido/vencido'); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + if (error.response?.status === 403) { + console.log('🚫 No tienes permisos para esta acción'); + } + return Promise.reject(error); + } +); + +export default api; \ No newline at end of file diff --git a/src/api/catering.js b/src/api/catering.js new file mode 100644 index 0000000..7720c4a --- /dev/null +++ b/src/api/catering.js @@ -0,0 +1,51 @@ +import api from './axios'; + +export const getCatering = async () => { + try { + const response = await api.get("/catering"); + return response.data; + } catch (error) { + console.error("Error fetching catering:", error); + throw error; + } +}; + +export const getCateringById = async (id) => { + try { + const response = await api.get(`/catering/${id}`); + return response.data; + } catch (error) { + console.error(`Error fetching catering with id ${id}:`, error); + throw error; + } +}; + +export const createCatering = async (cateringData) => { + try { + const response = await api.post("/catering", cateringData); + return response.data; + } catch (error) { + console.error("Error creating catering:", error); + throw error; + } +}; + +export const updateCatering = async (id, cateringData) => { + try { + const response = await api.put(`/catering/${id}`, cateringData); + return response.data; + } catch (error) { + console.error(`Error updating catering with id ${id}:`, error); + throw error; + } +}; + +export const deleteCatering = async (id) => { + try { + const response = await api.delete(`/catering/${id}`); + return response.data; + } catch (error) { + console.error(`Error deleting catering with id ${id}:`, error); + throw error; + } +}; \ No newline at end of file diff --git a/src/api/decoration.js b/src/api/decoration.js new file mode 100644 index 0000000..7d20922 --- /dev/null +++ b/src/api/decoration.js @@ -0,0 +1,51 @@ +import api from './axios'; + +export const getDecorations = async () => { + try { + const response = await api.get('/decoration'); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; + +export const getDecorationById = async (id) => { + try { + const response = await api.get(`/decoration/${id}`); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; + +export const createDecoration = async (decorationData) => { + try { + const response = await api.post('/decoration', decorationData); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; + +export const updateDecoration = async (id, decorationData) => { + try { + const response = await api.put(`/decoration/${id}`, decorationData); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; + +export const deleteDecoration = async (id) => { + try { + const response = await api.delete(`/decoration/${id}`); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/api/entertainments.js b/src/api/entertainments.js new file mode 100644 index 0000000..2458454 --- /dev/null +++ b/src/api/entertainments.js @@ -0,0 +1,51 @@ +import api from './axios'; + +export const getEntertainments = async () => { + try { + const response = await api.get("/entertainment"); + return response.data; + } catch (error) { + console.error("Error fetching entertainments:", error); + throw error; + } +}; + +export const getEntertainmentById = async (id) => { + try { + const response = await api.get(`/entertainment/${id}`); + return response.data; + } catch (error) { + console.error(`Error fetching entertainment with id ${id}:`, error); + throw error; + } +}; + +export const createEntertainment = async (entertainmentData) => { + try { + const response = await api.post("/entertainment", entertainmentData); + return response.data; + } catch (error) { + console.error("Error creating entertainment:", error); + throw error; + } +}; + +export const updateEntertainment = async (id, entertainmentData) => { + try { + const response = await api.put(`/entertainment/${id}`, entertainmentData); + return response.data; + } catch (error) { + console.error(`Error updating entertainment with id ${id}:`, error); + throw error; + } +}; + +export const deleteEntertainment = async (id) => { + try { + const response = await api.delete(`/entertainment/${id}`); + return response.data; + } catch (error) { + console.error(`Error deleting entertainment with id ${id}:`, error); + throw error; + } +}; \ No newline at end of file diff --git a/src/api/establishments.js b/src/api/establishments.js new file mode 100644 index 0000000..91fe0c0 --- /dev/null +++ b/src/api/establishments.js @@ -0,0 +1,51 @@ +import api from './axios'; + +export const getEstablishments = async () => { + try { + const response = await api.get("/establishments"); + return response.data; + } catch (error) { + console.error("Error fetching establishments:", error); + throw error; + } +}; + +export const getEstablishmentById = async (id) => { + try { + const response = await api.get(`/establishments/${id}`); + return response.data; + } catch (error) { + console.error(`Error fetching establishment ${id}:`, error); + throw error; + } +}; + +export const createEstablishment = async (establishmentData) => { + try { + const response = await api.post("/establishments", establishmentData); + return response.data; + } catch (error) { + console.error("Error creating establishment:", error); + throw error; + } +}; + +export const updateEstablishment = async (id, establishmentData) => { + try { + const response = await api.put(`/establishments/${id}`, establishmentData); + return response.data; + } catch (error) { + console.error(`Error updating establishment ${id}:`, error); + throw error; + } +}; + +export const deleteEstablishment = async (id) => { + try { + const response = await api.delete(`/establishments/${id}`); + return response.data; + } catch (error) { + console.error(`Error deleting establishment ${id}:`, error); + throw error; + } +}; \ No newline at end of file diff --git a/src/api/events.js b/src/api/events.js new file mode 100644 index 0000000..38e9819 --- /dev/null +++ b/src/api/events.js @@ -0,0 +1,52 @@ +import api from './axios'; + +export const getEvents = async () => { + try { + const response = await api.get("/events"); + return response.data; + } catch (error) { + console.error("Error fetching events:", error); + throw error; + } +}; + +export const getEventById = async (id) => { + try { + const response = await api.get(`/events/${id}`); + return response.data; + } catch (error) { + console.error(`Error fetching event ${id}:`, error); + throw error; + } +}; + +export const createEvent = async (eventData) => { + try { + + const response = await api.post("/events", eventData); + return response.data; + } catch (error) { + console.error("Error creating event:", error); + throw error; + } +}; + +export const updateEvent = async (id, eventData) => { + try { + const response = await api.put(`/events/${id}`, eventData); + return response.data; + } catch (error) { + console.error(`Error updating event ${id}:`, error); + throw error; + } +}; + +export const deleteEvent = async (id) => { + try { + const response = await api.delete(`/events/${id}`); + return response.data; + } catch (error) { + console.error(`Error deleting event ${id}:`, error); + throw error; + } +}; diff --git a/src/api/reservation.js b/src/api/reservation.js new file mode 100644 index 0000000..898cdfc --- /dev/null +++ b/src/api/reservation.js @@ -0,0 +1,25 @@ +import api from './axios'; + +export const getReservations = async () => { + try { + const response = await api.get('/reserve'); + console.log("API Response:", response); + return response.data; + } catch (error) { + console.error("Error fetching reservations:", error); + throw error; + } +} + +export const createReservations = async () => { + try { + const response = await api.get('/reserve'); + console.log("API Response:", response); + return response.data; + } catch (error) { + console.error("Error fetching reservations:", error); + throw error; + } + + +}; \ No newline at end of file diff --git a/src/api/services.js b/src/api/services.js new file mode 100644 index 0000000..1592423 --- /dev/null +++ b/src/api/services.js @@ -0,0 +1,43 @@ +import api from './axios'; + +//Obtner catering +export const getCatering = async () => { + try { + const response = await api.get("/catering"); + return response.data; + } catch (error) { + console.error("Error fetching catering:", error); + throw error; + } +}; + +export const getEntertainments = async () => { + try { + const response = await api.get("/entertainment"); + return response.data; + } catch (error) { + console.error("Error fetching entertainments:", error); + throw error; + } +}; + +export const getDecorations = async () => { + try { + const response = await api.get('/decoration'); + return response.data; + } catch (error) { + console.error("Error fetching decoration:", error); + throw error; + } +}; + +export const getAdditionals = async () => { + try { + const response = await api.get('/additional'); + return response.data; + } catch (error) { + console.error("Error fetching additionals:", error); + throw error; + } + }; + diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 0000000..a98c90f --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,65 @@ +import api from './axios'; + + +// Obtener usuarios por tipo (CLIENTE, ADMIN, etc.) +export const getUsersByType = async (type) => { + try { + const response = await api.get(`/User/type/${type}`); + return response.data; + } catch (error) { + console.error(`Error fetching users by type ${type}:`, error); + throw error; + } +}; + +// Obtener solo usuarios con type === 'CLIENTE' +export const getClients = async () => { + try { + return await getUsersByType('CLIENTE'); + } catch (error) { + console.error("Error fetching clients:", error); + throw error; + } +}; + + + +// Obtener pagos del usuario +export const getUserPayments = async () => { + try { + const response = await api.get("/payments"); + return response; + } catch (error) { + console.error("Error fetching user payments:", error); + throw error; + } +}; + + + +//Corregir por endpoints reales + +// Obtener perfil del usuario +export const getUserProfile = async () => { + try { + const response = await api.get("/api/users/profile"); + return response; + } catch (error) { + console.error("Error fetching user profile:", error); + throw error; + } +}; + + + + +// Actualizar perfil del usuario (cambiar por uno real) +export const updateUserProfile = async (userData) => { + try { + const response = await api.put("/api/users/profile", userData); + return response; + } catch (error) { + console.error("Error updating user profile:", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/components/AdminRoute.jsx b/src/components/AdminRoute.jsx new file mode 100644 index 0000000..4beca9c --- /dev/null +++ b/src/components/AdminRoute.jsx @@ -0,0 +1,18 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../hooks/useAuth"; + +export default function AdminRoute({ children }) { + const { user, isAuthenticated } = useAuth(); + + // Si no está autenticado, redirigir a login + if (!isAuthenticated) { + return ; + } + + + if (user?.type !== "ADMIN") { + return ; + } + + return children; +} \ No newline at end of file diff --git a/src/components/ClienteRoute.jsx b/src/components/ClienteRoute.jsx new file mode 100644 index 0000000..f728772 --- /dev/null +++ b/src/components/ClienteRoute.jsx @@ -0,0 +1,20 @@ + +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +export default function ClientRoute({ children }) { + const { user, isAuthenticated } = useAuth(); + + if (!isAuthenticated) { + // Si no está autenticado, redirigir al login + return ; + } + + // Si está autenticado pero es administrador, redirigir al dashboard de admin + if (user?.type === 'ADMIN') { + return ; + } + + // Si es cliente, mostrar el componente hijo + return children; +} \ No newline at end of file diff --git a/src/components/DiscountBanner.jsx b/src/components/DiscountBanner.jsx index c3c241d..7269139 100644 --- a/src/components/DiscountBanner.jsx +++ b/src/components/DiscountBanner.jsx @@ -1,22 +1,29 @@ -// src/components/DiscountBanner.jsx import { useState, useEffect } from "react"; -import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "../firebase"; // adjust the path if needed + const DiscountBanner = () => { const [showBanner, setShowBanner] = useState(true); const [isLoggedIn, setIsLoggedIn] = useState(false); - // Check user login state useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - setIsLoggedIn(!!user); // true if user exists, false otherwise - }); + const token = localStorage.getItem("token"); + // Si hay token, asumimos que está logueado + setIsLoggedIn(!!token); + + // Escuchar cambios en el localStorage para actualizar el estado + const handleStorageChange = (e) => { + if (e.key === "token") { + setIsLoggedIn(!!e.newValue); + } + }; - return () => unsubscribe(); + window.addEventListener("storage", handleStorageChange); + + return () => { + window.removeEventListener("storage", handleStorageChange); + }; }, []); - // Don't render the banner if user is logged in or banner is dismissed if (!showBanner || isLoggedIn) { return null; } @@ -37,5 +44,4 @@ const DiscountBanner = () => { ); }; -export default DiscountBanner; - +export default DiscountBanner; \ No newline at end of file diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 37baecf..11ef362 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -50,7 +50,7 @@ export default function Footer() { {/* Bottom Bar */}
-

© 2025 EVENTIQUE. All rights reserved.

+

© 2025 EVENTIFY. All rights reserved.

diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 0167bb9..a96813b 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -1,4 +1,3 @@ -// src/Layout.jsx import { Outlet } from "react-router-dom"; // import DiscountBanner from "./DiscountBanner"; import Footer from "./Footer"; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 2397a78..7418d1b 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,51 +1,47 @@ -import { auth } from "../firebase"; import { Link, useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { onAuthStateChanged, signOut } from "firebase/auth"; +import { useState } from "react"; +import { useAuth } from "../hooks/useAuth"; export default function Navbar() { - const [user, setUser] = useState(null); + const { user, logout, isAuthenticated } = useAuth(); const [isMenuOpen, setIsMenuOpen] = useState(false); const navigate = useNavigate(); - useEffect(() => { - if (isMenuOpen) { + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + if (!isMenuOpen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = 'unset'; } - }, [isMenuOpen]); - - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (currentUser) => { - setUser(currentUser); - }); - return () => unsubscribe(); - }, []); - + }; - const handleLogout = async () => { - await signOut(auth); - navigate("/sign-in"); + const closeMenu = () => { setIsMenuOpen(false); + document.body.style.overflow = 'unset'; }; - const toggleMenu = () => { - setIsMenuOpen(!isMenuOpen); + const handleLogout = () => { + logout(); + navigate("/sign-in"); + closeMenu(); }; return ( ); -} +} \ No newline at end of file diff --git a/src/components/Slider.jsx b/src/components/Slider.jsx index d2b4953..3dd2c10 100644 --- a/src/components/Slider.jsx +++ b/src/components/Slider.jsx @@ -4,35 +4,55 @@ import React, { useState, useEffect, useCallback } from 'react'; export default function App() { const slideData = [ { - imgSrc: "https://www.fiestroevents.com/uploads/24/08/66b21e981eced0608241722949272.png", - title: "Elegant Wedding Receptions", - description: "Crafting unforgettable moments with stunning decor and seamless planning." + imgSrc: "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YmlydGhkYXklMjBwYXJ0eXxlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Cumpleaños", + description: "Celebra tu día especial con una fiesta inolvidable llena de alegría y diversión." }, { - imgSrc: "https://www.mrsfields.com/cdn/shop/articles/shutterstock_2408066691_1.jpg?v=1725556532", - title: "Joyful Birthday Parties", - description: "Celebrate another year with fun, laughter, and a party to remember." + imgSrc: "https://images.unsplash.com/photo-1519225421980-715cb0215aed?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8d2VkZGluZ3xlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Bodas", + description: "El día más importante de tu vida merece una celebración perfecta y mágica." }, { - imgSrc: "https://floweraura-blog-img.s3.ap-south-1.amazonaws.com/Celebrate+Anniversary+travel/cvr.jpg", - title: "Romantic Anniversaries", - description: "Mark your special milestone with a celebration of your love story." + imgSrc: "https://academia.unad.edu.co/images/2024/diplo2024.jpg", + title: "Grados", + description: "Celebra tus logros académicos con una ceremonia digna de tu esfuerzo y dedicación." }, { - imgSrc: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzZg9yKSUy0oKJsxJZblexdfoMYhwUAtY2yA&s", - title: "Corporate Holiday Events", - description: "Boost team morale with a professionally organized and festive gathering." + imgSrc: "https://dulcesfiestas.com.co/wp-content/uploads/elementor/thumbs/2_Ideas-para-decorar-bautizos-y-primera-comunion-qpa0xxnlh99xaia43oqxwwzx48wliev1h99y57voew.png", + title: "Bautizos", + description: "Una celebración espiritual llena de amor para dar la bienvenida a la fe." }, { - imgSrc: "https://5.imimg.com/data5/SELLER/Default/2022/8/QF/JF/PJ/2713142/retirement-party-decoration-service.JPG", - title: "Honorable Retirement Parties", - description: "Celebrate a legacy of hard work and dedication with a fitting send-off." + imgSrc: "https://ladomingaeventos.com/wp-content/uploads/2013/11/fiestaninos2.jpg", + title: "Fiesta Infantil", + description: "Diversión garantizada para los más pequeños con juegos, sorpresas y mucha alegría." }, { - imgSrc: "https://www.parents.com/thmb/JlKRSVpphFei1B17UE35pVnV5Fg=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/shutterstock_762391612-41aca833e9184016833a754be5e7d5c3.jpg", - title: "Baby Shower Celebrations", - description: "Welcome the newest addition to the family with a beautiful and heartwarming event." + imgSrc: "https://m.media-amazon.com/images/I/71se54LSd8L._AC_UF1000,1000_QL80_.jpg", + title: "Baby Shower", + description: "Prepara la llegada del nuevo miembro de la familia con una celebración especial." }, + { + imgSrc: "https://www.mujerde10.com/wp-content/uploads/2025/07/estilo-de-vida-decoracion-para-xv-anos-tendencias--1024x678.jpg", + title: "Fiesta de 15 Años", + description: "Una noche mágica e inolvidable para celebrar esta importante transición." + }, + { + imgSrc: "https://cdn0.bodas.com.mx/usr/4/3/7/0/cfb_2x_104532.jpg", + title: "Despedida de Solter@", + description: "La última celebración de soltería llena de sorpresas y momentos divertidos." + }, + { + imgSrc: "https://images.unsplash.com/photo-1587825140708-dfaf72ae4b04?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8Y29uZmVyZW5jZXxlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Conferencias y Capacitaciones", + description: "Espacios profesionales para el aprendizaje, networking y desarrollo empresarial." + }, + { + imgSrc: "https://images.unsplash.com/photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8Y29ycG9yYXRlJTIwbWVldGluZ3xlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Reunión Corporativa", + description: "Eventos empresariales que impulsan la productividad y el trabajo en equipo." + } ]; return ( @@ -42,7 +62,7 @@ export default function App() { ); } - +// Los componentes ChevronLeftIcon, ChevronRightIcon y EnhancedSlider se mantienen igual const ChevronLeftIcon = () => ( @@ -55,7 +75,7 @@ const ChevronRightIcon = () => ( ); - +// El componente EnhancedSlider se mantiene exactamente igual function EnhancedSlider({ slides }) { const [currentIndex, setCurrentIndex] = useState(0); @@ -139,4 +159,3 @@ function EnhancedSlider({ slides }) {
); } - diff --git a/src/components/Snackbar.jsx b/src/components/Snackbar.jsx new file mode 100644 index 0000000..235a446 --- /dev/null +++ b/src/components/Snackbar.jsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; + +const Snackbar = ({ + message, + type = 'success', // 'success' o 'error' + duration = 4000, + onClose +}) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (message) { + setIsVisible(true); + + // Auto cerrar después de la duración + const timer = setTimeout(() => { + setIsVisible(false); + if (onClose) setTimeout(onClose, 300); // Esperar a la animación + }, duration); + + return () => clearTimeout(timer); + } + }, [message, duration, onClose]); + + if (!message || !isVisible) return null; + + const bgColor = type === 'success' + ? 'bg-green-500' + : type === 'error' + ? 'bg-red-500' + : 'bg-blue-500'; + + return ( +
+
+
+ + {type === 'success' ? '✅' : '❌'} + + {message} + +
+
+
+ ); +}; + +export default Snackbar; \ No newline at end of file diff --git a/src/components/reservation/reservationCard.jsx b/src/components/reservation/reservationCard.jsx new file mode 100644 index 0000000..ae3a1b0 --- /dev/null +++ b/src/components/reservation/reservationCard.jsx @@ -0,0 +1,215 @@ +import { Link } from 'react-router-dom'; + +export default function ReservationCard({ reservation, showUserInfo = false }) { + // Formatear fecha sin date-fns + const formatDate = (dateString) => { + if (!dateString) return "Fecha no definida"; + try { + const date = new Date(dateString); + const options = { + year: 'numeric', + month: 'long', + day: 'numeric' + }; + return date.toLocaleDateString('es-ES', options); + } catch (e) { + console.error("Error formatting date:", e); + return dateString; + } + }; + + // Formatear fecha corta + const formatShortDate = (dateString) => { + if (!dateString) return ""; + try { + const date = new Date(dateString); + return date.toLocaleDateString('es-ES', { + month: 'short', + day: 'numeric' + }); + } catch (e) { + console.error("Error formatting short date:", e); + return ""; + } + }; + + // Obtener color según estado + const getStatusColor = (status) => { + const statusMap = { + 'PENDING': 'bg-yellow-100 text-yellow-800 border-yellow-300', + 'CONFIRMED': 'bg-green-100 text-green-800 border-green-300', + 'COMPLETED': 'bg-blue-100 text-blue-800 border-blue-300', + 'CANCELLED': 'bg-red-100 text-red-800 border-red-300', + 'ACTIVE': 'bg-purple-100 text-purple-800 border-purple-300', + }; + return statusMap[status?.toUpperCase()] || 'bg-gray-100 text-gray-800 border-gray-300'; + }; + + // Formatear texto del estado + const formatStatus = (status) => { + const statusMap = { + 'PENDING': 'Pendiente', + 'CONFIRMED': 'Confirmada', + 'COMPLETED': 'Completada', + 'CANCELLED': 'Cancelada', + 'ACTIVE': 'Activa', + }; + return statusMap[status?.toUpperCase()] || status || 'Desconocido'; + }; + + + const isUpcoming = () => { + try { + const today = new Date(); + const reservationDate = reservation.dates?.[0] || reservation.date; + if (!reservationDate) return false; + return new Date(reservationDate) > today; + } catch (e) { + console.error("Error determining if reservation is upcoming:", e); + return false; + } + }; + + return ( +
+ {/* Header */} +
+
+
+

+ {reservation.event?.name || reservation.establishment?.name || 'Reserva'} +

+ {isUpcoming() && ( + + Próxima + + )} +
+
+ + {formatStatus(reservation.status)} + +
+
+ +
+
+ ${(reservation.totalCost || reservation.total || 0).toLocaleString()} +
+
Costo total
+
+
+ + {/* Información principal */} +
+
+

Fechas

+

+ {reservation.dates?.length > 0 + ? reservation.dates.map(date => formatShortDate(date)).join(', ') + : formatDate(reservation.date)} +

+
+ +
+

Establecimiento

+

+ {reservation.establishment?.name || 'No especificado'} +

+
+ +
+

Tipo de evento

+

+ {reservation.event?.type || reservation.eventType || 'No especificado'} +

+
+
+ + {/* Información del usuario (solo para admin) */} + {showUserInfo && reservation.user && ( +
+

Cliente

+
+
+ {reservation.user.fullName?.charAt(0) || 'C'} +
+
+

{reservation.user.fullName}

+

{reservation.user.email}

+
+
+
+ )} + + {/* Servicios */} +
+

Servicios contratados

+
+ {reservation.services ? ( + <> + {/* Decoración */} + {reservation.services.decoration && reservation.services.decoration.id && ( + + Decoración + + )} + + {/* Entretenimiento */} + {reservation.services.entertainment && + Array.isArray(reservation.services.entertainment) && + reservation.services.entertainment.length > 0 && ( + + Entretenimiento ({reservation.services.entertainment.length}) + + )} + + {/* Catering */} + {reservation.services.catering && + Array.isArray(reservation.services.catering) && + reservation.services.catering.length > 0 && ( + + Catering ({reservation.services.catering.length}) + + )} + + {/* Servicios adicionales */} + {reservation.services.additionalServices && + Array.isArray(reservation.services.additionalServices) && + reservation.services.additionalServices.length > 0 && ( + + Adicionales ({reservation.services.additionalServices.length}) + + )} + + {/* Si no hay servicios */} + {!reservation.services.decoration?.id && + !reservation.services.entertainment?.length && + !reservation.services.catering?.length && + !reservation.services.additionalServices?.length && ( +

No hay servicios especificados

+ )} + + ) : ( +

No hay servicios especificados

+ )} +
+
+ + {/* Acciones */} +
+ + Ver detalles + + {reservation.status === 'PENDING' && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/reservation/reservationLisr.jsx b/src/components/reservation/reservationLisr.jsx new file mode 100644 index 0000000..c63edb4 --- /dev/null +++ b/src/components/reservation/reservationLisr.jsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import ReservationCard from './reservationCard'; + +export default function ReservationList({ reservations, title = "Mis Reservas", showUserInfo = false }) { + const [filter, setFilter] = useState('all'); + + // Filtrar reservas + const filteredReservations = reservations.filter(reservation => { + if (filter === 'all') return true; + if (filter === 'upcoming') { + const today = new Date(); + const reservationDate = reservation.dates?.[0] || reservation.date; + try { + return reservationDate && new Date(reservationDate) >= today; + } catch (e) { + console.error("Error filtering upcoming reservations:", e); + return false; + } + } + return reservation.status === filter; + }); + + // Estadísticas + const stats = { + total: reservations.length, + upcoming: reservations.filter(r => { + const today = new Date(); + const date = r.dates?.[0] || r.date; + try { + return date && new Date(date) >= today; + } catch (e) { + console.error("Error calculating upcoming reservations:", e); + return false; + } + }).length, + pending: reservations.filter(r => r.status === 'PENDING').length, + completed: reservations.filter(r => r.status === 'COMPLETED').length, + cancelled: reservations.filter(r => r.status === 'CANCELLED').length, + }; + + return ( +
+ {/* Header con filtros */} +
+
+

{title}

+
+ Mostrando {filteredReservations.length} de {reservations.length} reservas +
+
+ + {/* Filtros */} +
+ + + + + +
+
+ + {/* Lista de reservas */} + {filteredReservations.length === 0 ? ( +
+
📅
+

+ No hay reservas {filter !== 'all' ? `con filtro "${filter}"` : ''} +

+

+ {filter === 'all' + ? "Aún no tienes reservas en el sistema." + : "Intenta con otro filtro o crea una nueva reserva."} +

+
+ ) : ( +
+ {filteredReservations.map(reservation => ( + + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index f9987dc..d42e58d 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -1,30 +1,90 @@ -import { createContext, useContext, useEffect, useState } from "react"; -import { auth } from "../firebase"; -import { onAuthStateChanged } from "firebase/auth"; +import { createContext, useState, useEffect } from "react"; +import { jwtDecode } from "jwt-decode"; -// Create context const AuthContext = createContext(); +export { AuthContext } -// Hook to use context -export function useAuth() { - return useContext(AuthContext); -} - -// Context Provider -export function AuthProvider({ children }) { +export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, currentUser => { - setUser(currentUser); - }); - - return () => unsubscribe(); + const token = localStorage.getItem("token"); + console.log("🔄 AuthProvider - Cargando estado inicial"); + console.log("🔑 Token en localStorage:", token ? "SÍ" : "NO"); + if (token) { + try { + const decodedToken = jwtDecode(token); + console.log("🎫 Token decodificado:", decodedToken); + + // Verificar si el token no ha expirado + if (decodedToken.exp * 1000 < Date.now()) { + console.log("⏰ Token expirado"); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + } else { + // Si el token es válido, establecer el usuario + const userData = localStorage.getItem("user"); + console.log("📦 User data en localStorage:", userData); + if (userData) { + const parsedUser = JSON.parse(userData); + console.log("👤 Usuario parseado:", parsedUser); + console.log("🎯 Tipo de usuario (desde localStorage):", parsedUser?.type); + // Verificamos que el usuario tenga un tipo, si no, podría ser un error + if (parsedUser && parsedUser.type) { + setUser(parsedUser); + } else { + console.warn("El usuario no tiene tipo definido, no se establecerá el estado de autenticación."); + } + } else { + console.log("No hay user en localStorage"); + } + } + } catch (error) { + console.error("❌ Error decodificando el token:", error); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + } + } + setLoading(false); }, []); + const login = (token, userData) => { + console.log("🔥 AuthContext.login() llamado"); + console.log("🔑 Token recibido:", token ? "SÍ (longitud: " + token.length + ")" : "NO"); + console.log("👤 UserData recibido:", userData); + console.log("🎯 Tipo de usuario recibido:", userData?.type); + + localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(userData)); + setUser(userData); + + // Verificar que se guardó correctamente + const savedUser = JSON.parse(localStorage.getItem('user')); + console.log("💾 Usuario guardado en localStorage:", savedUser); + }; + + const logout = () => { + console.log("👋 AuthContext.logout() llamado"); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + setUser(null); + }; + + const value = { + user, + login, + logout, + isAuthenticated: !!user, + loading + }; + + console.log("🔄 AuthProvider render - user:", user); + console.log("🔐 AuthProvider render - isAuthenticated:", !!user); + return ( - + {children} ); -} +}; \ No newline at end of file diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx new file mode 100644 index 0000000..46ae026 --- /dev/null +++ b/src/hooks/useAuth.jsx @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { AuthContext } from "../context/AuthContext"; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth debe ser usado dentro de un AuthProvider"); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/useSnackbar.jsx b/src/hooks/useSnackbar.jsx new file mode 100644 index 0000000..7236541 --- /dev/null +++ b/src/hooks/useSnackbar.jsx @@ -0,0 +1,91 @@ +import { useState, useCallback, useRef } from 'react'; + +const useSnackbar = () => { + const [snackbar, setSnackbar] = useState({ + message: '', + type: 'success', + isVisible: false, + }); + + const timeoutRef = useRef(null); + + const showSnackbar = useCallback((message, type = 'success') => { + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + setSnackbar({ + message, + type, + isVisible: true, + }); + + // Auto-ocultar después de 4 segundos + timeoutRef.current = setTimeout(() => { + hideSnackbar(); + }, 4000); + }, []); + + const hideSnackbar = useCallback(() => { + setSnackbar(prev => ({ ...prev, isVisible: false })); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }, []); + + const SnackbarComponent = useCallback(({ position = 'top' }) => { + if (!snackbar.isVisible) return null; + + const positionClass = { + top: 'top-4', + bottom: 'bottom-4', + 'top-right': 'top-4 right-4', + 'bottom-right': 'bottom-4 right-4', + 'top-left': 'top-4 left-4', + 'bottom-left': 'bottom-4 left-4', + }[position] || 'top-4'; + + const bgColor = { + success: 'bg-green-500', + error: 'bg-red-500', + warning: 'bg-yellow-500', + info: 'bg-blue-500', + }[snackbar.type] || 'bg-gray-500'; + + const icon = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️', + }[snackbar.type] || '💡'; + + return ( +
+
+ {icon} + {snackbar.message} + +
+
+ ); + }, [snackbar, hideSnackbar]); + + return { + showSnackbar, + hideSnackbar, + SnackbarComponent, + snackbarMessage: snackbar.message, + snackbarType: snackbar.type, + isSnackbarVisible: snackbar.isVisible, + }; +}; + +export default useSnackbar; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..4c2a1c6 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { AuthProvider } from './context/AuthContext' import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( - + + + + + , -) +) \ No newline at end of file diff --git a/src/pages/CategoriesServices.jsx b/src/pages/CategoriesServices.jsx new file mode 100644 index 0000000..456d1ce --- /dev/null +++ b/src/pages/CategoriesServices.jsx @@ -0,0 +1,403 @@ +// src/pages/CategoryServices.jsx +import { useState, useEffect } from "react"; +import { useParams, Link } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import { getCatering, getEntertainments, getDecorations, getAdditionals } from '../api/services'; + +export default function CategoryServices() { + const { categoryId } = useParams(); + const [services, setServices] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [categoryInfo, setCategoryInfo] = useState(null); + + // Mapeo de categorías con información + const categoryMap = { + entretenimiento: { + title: "Entretenimiento", + function: getEntertainments, + description: "Música, shows y animación para tu evento", + icon: "🎭", + color: "purple", + bgColor: "bg-purple-50", + borderColor: "border-purple-200", + textColor: "text-purple-800" + }, + decoracion: { + title: "Decoración", + function: getDecorations, + description: "Ambientación y decoración personalizada", + icon: "🎨", + color: "blue", + bgColor: "bg-blue-50", + borderColor: "border-blue-200", + textColor: "text-blue-800" + }, + catering: { + title: "Catering", + function: getCatering, + description: "Banquete y servicio de alimentos", + icon: "🍽️", + color: "green", + bgColor: "bg-green-50", + borderColor: "border-green-200", + textColor: "text-green-800" + }, + adicionales: { + title: "Servicios Adicionales", + function: getAdditionals, + description: "Servicios complementarios", + icon: "➕", + color: "yellow", + bgColor: "bg-yellow-50", + borderColor: "border-yellow-200", + textColor: "text-yellow-800" + } + }; + + useEffect(() => { + const fetchServicesByCategory = async () => { + if (!categoryId || !categoryMap[categoryId]) { + toast.error("Categoría no válida"); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + const category = categoryMap[categoryId]; + setCategoryInfo(category); + + const response = await category.function(); + + // Normalizar los datos según la categoría + let normalizedServices = []; + + if (categoryId === "entretenimiento" || categoryId === "catering") { + normalizedServices = Array.isArray(response) ? response : []; + } + else if (categoryId === "decoracion") { + if (Array.isArray(response)) { + normalizedServices = response; + } else if (response && typeof response === 'object') { + normalizedServices = [response]; + } else { + normalizedServices = []; + } + } + else if (categoryId === "adicionales") { + if (response && response.additionalServices && Array.isArray(response.additionalServices)) { + normalizedServices = response.additionalServices; + } else if (Array.isArray(response)) { + normalizedServices = response; + } else { + normalizedServices = []; + } + } + + setServices(normalizedServices); + + } catch (error) { + console.error("Error al cargar servicios:", error); + toast.error(`No se pudieron cargar los servicios de ${categoryMap[categoryId]?.title}`); + setServices([]); + } finally { + setIsLoading(false); + } + }; + + fetchServicesByCategory(); + }, [categoryId]); + + // Función para obtener el costo según la categoría + const getServiceCost = (service) => { + switch(categoryId) { + case "entretenimiento": + return service.hourlyRate; + case "decoracion": + return service.cost; + case "catering": + return service.costDish; + case "adicionales": + return service.cost; + default: + return 0; + } + }; + + // Función para renderizar servicios + const renderServiceCard = (service, index) => { + const category = categoryMap[categoryId]; + const serviceCost = getServiceCost(service); + + return ( +
+ {/* Cabecera de la tarjeta con icono y título */} +
+
+
+
+ {category.icon} +
+
+

+ {service.name || service.theme || service.menuType || "Servicio"} +

+
+ {category.title} +
+
+
+ +
+ + {/* Información específica del servicio */} +
+ {renderServiceDetails(service)} +
+ + {/* Precio y botón de acción */} +
+
+

+ ${serviceCost || "Consultar"} +

+

Costo del servicio

+
+ + + Reservar + +
+
+
+ ); + }; + + // Renderizar detalles específicos por categoría + const renderServiceDetails = (service) => { + switch(categoryId) { + case "entretenimiento": + return ( +
+
+
+

Tipo

+

{service.type || "No especificado"}

+
+ {service.hours && ( +
+

Duración

+

{service.hours} horas

+
+ )} +
+ + {service.description && ( +
+

{service.description}

+
+ )} +
+ ); + + case "decoracion": + return ( +
+ {service.theme && ( +
+

Temática

+

{service.theme}

+
+ )} + +
+

Artículos incluidos

+
+

+ {service.articles || "Globos, manteles, decoración temática"} +

+
+
+
+ ); + + case "catering": + return ( +
+ {service.menuType && ( +
+

Tipo de menú

+

{service.menuType}

+
+ )} + +
+

Descripción

+

+ {service.description || "Servicio de catering completo para tu evento"} +

+
+ + {service.numberDish && ( +
+
+

Número de platos

+

{service.numberDish}

+
+ +
+

Para personas

+

{service.numberDish}

+
+
+ )} +
+ ); + + case "adicionales": + return ( +
+
+

Descripción

+

+ {service.description || "Descripción del servicio adicional"} +

+
+ + {service.quantity && ( +
+
+

Cantidad

+

{service.quantity} unidades

+
+
+ )} +
+ ); + + default: + return null; + } + }; + + // Estado de carga + if (isLoading) { + return ( +
+
+
+
+
+
+ +
+ {[...Array(6)].map((_, index) => ( +
+
+
+
+
+
+
+ ))} +
+
+
+ ); + } + + return ( +
+
+ {/* Header simplificado */} +
+ + + + + Volver a categorías + + +
+
+
+ {categoryInfo?.icon} +
+
+

+ {categoryInfo?.title || "Categoría"} +

+

+ {categoryInfo?.description || "Explora nuestros servicios"} +

+
+
+ +
+

+ {services.length} {services.length === 1 ? 'servicio disponible' : 'servicios disponibles'} +

+ + {/* Filtros o ordenación (puedes agregar después) */} +
+ Ordenar por: Relevancia +
+
+
+
+ + {/* Lista de servicios */} + {services.length > 0 ? ( +
+ {services.map((service, index) => + renderServiceCard(service, index) + )} +
+ ) : ( +
+
+ + + +
+

+ No hay servicios disponibles +

+

+ En este momento no contamos con servicios en esta categoría. + Te notificaremos cuando agreguemos nuevas opciones. +

+
+ + Ver otras categorías + + +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx index 43c2e1b..072d217 100644 --- a/src/pages/Contact.jsx +++ b/src/pages/Contact.jsx @@ -78,7 +78,7 @@ export default function Contact() {

Office

Come say hello at our office HQ.

-

MITM Ujjain (M.P), India

+

Tuluá, Valle del Cauca, Colombia

{/* Chat Card */} @@ -94,7 +94,7 @@ export default function Contact() {

Phone

Mon–Fri from 8am to 5pm.

-

+91 1234567890

+

+57 3162561558

diff --git a/src/pages/Establishments.jsx b/src/pages/Establishments.jsx new file mode 100644 index 0000000..a5c3d13 --- /dev/null +++ b/src/pages/Establishments.jsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; + +export default function Establishments() { + const [establishments, setEstablishments] = useState([]); + + useEffect(() => { + axios + .get("http://localhost:8080/establishments") + .then((res) => setEstablishments(res.data)) + .catch((err) => console.error("Error fetching establishments:", err)); + }, []); + + return ( +
+
+

+ Establecimientos +

+
+ + {/* Cards */} +
+ {establishments.map((item) => ( +
+ {/* Imagen */} +
+ {item.name} +
+ + {/* Info */} +

+ {item.name} +

+ +

{item.address}, {item.city}

+

Tel: {item.phone}

+ + {/* Capacidad */} +

+ Capacidad: {item.capacity} personas +

+ + {/* Botón */} + +
+ ))} +
+
+ ); +} diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 185be3a..7871fd6 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,5 +1,6 @@ import Slider from "../components/Slider"; import Categories from "./Categories"; +import Establishments from "./Establishments"; import Services from "./Services"; import Contact from "./Contact"; @@ -9,12 +10,13 @@ export default function Home() { <>
+ {/*

We are always ready to give you a wonderful service

{/*

We provide the best event management services just for you

*/} {/* *
*/} - + diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 6cbab53..e19f4b4 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,82 +1,366 @@ // src/pages/Profile.jsx - import { useEffect, useState } from "react"; -import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "../firebase"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import { useAuth } from "../hooks/useAuth"; +import { getUserPayments } from "../api/user"; +import { getReservations } from "../api/reservation"; +import ReservationCard from "../components/reservation/reservationCard"; export default function Profile() { - const [user, setUser] = useState(null); + const { user, logout, isAuthenticated } = useAuth(); + const [userData, ] = useState(null); + const [reservations, setReservations] = useState([]); + const [payments, setPayments] = useState([]); + const [activeTab, setActiveTab] = useState("overview"); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); - // Fetch and set the authenticated user useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (currentUser) => { - setUser(currentUser); + const fetchUserData = async () => { + if (!user) { + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + + // Obtener reservas del usuario + const reservationsResponse = await getReservations(); + console.log("Reservas recibidas:", reservationsResponse); + console.log("Tipo de datos:", typeof reservationsResponse.data); + console.log("Es array?:", Array.isArray(reservationsResponse.data)); + console.log("Cantidad:", reservationsResponse.data?.length || 0); + + if (reservationsResponse.data && Array.isArray(reservationsResponse.data)) { + setReservations(reservationsResponse.data); + } else if (Array.isArray(reservationsResponse)) { + // Si la API devuelve el array directamente + setReservations(reservationsResponse); + } else { + console.warn("Formato de reservas inesperado:", reservationsResponse); + setReservations([]); + } + + // Obtener pagos del usuario + const paymentsResponse = await getUserPayments(); + setPayments(paymentsResponse.data || []); + + + + } catch (error) { + console.error("Error fetching user data:", error); + toast.error("Error al cargar los datos del perfil"); + } finally { + setIsLoading(false); + } + }; + + fetchUserData(); + }, [user]); + + const handleLogout = async () => { + try { + logout(); + toast.success("¡Sesión cerrada exitosamente!"); + navigate("/"); + } catch (error) { + toast.error("Error al cerrar sesión", error); + } + }; + + const calculateTotalSpent = () => { + return payments.reduce((total, payment) => total + (payment.total || payment.totalCost || 0), 0); + }; + + const getUpcomingEvents = () => { + const today = new Date(); + return reservations.filter(reservation => { + const eventDate = reservation.date || reservation.eventDate || reservation.dates?.[0]; + return eventDate && new Date(eventDate) >= today; }); + }; - return () => unsubscribe(); - }, []); + if (isLoading) { + return ( +
+
+
+

Cargando perfil...

+
+
+ ); + } - if (!user) { + if (!isAuthenticated || !user) { return ( -
- Loading profile... +
+
+
+ + + +
+

Acceso no autorizado

+

Inicia sesión para ver tu perfil

+ +
); } - return ( -
-
+ const upcomingEvents = getUpcomingEvents(); + return ( +
+
{/* Profile Header */} -
- User -
-

{user.displayName || "Your Name"}

-

Registered User

+
+
+
+
+ User +
+ +
+

+ {userData?.fullName || userData?.name || user?.fullName || "Usuario"} +

+

{user?.email}

+

{userData?.phone || "Teléfono no registrado"}

+
+ +
+
+

{reservations.length}

+

Reservas

+
+
+

{upcomingEvents.length}

+

Próximos

+
+
+

${calculateTotalSpent()}

+

Total Gastado

+
+
+
-
- {/* Divider */} -
- - {/* Email and Details */} -
-

- Email: {user.email} -

-

- User Type: Regular User - -

-

- Member Since: {" "} - {new Date(user.metadata?.creationTime).toLocaleDateString()} -

+ {/* Navigation Tabs */} +
+ +
+ {/* Tab Content */} +
+ {activeTab === "overview" && ( +
+

Resumen de Actividad

+ + {/* Upcoming Events */} +
+

Próximos Eventos

+ {upcomingEvents.length > 0 ? ( +
+ {upcomingEvents.slice(0, 3).map((reservation, index) => ( +
+
+
+

{reservation.eventType || reservation.event?.type || "Evento"}

+

+ {reservation.date && new Date(reservation.date).toLocaleDateString('es-ES')} +

+
+ + ${reservation.total || reservation.totalCost || 0} + +
+
+ ))} +
+ ) : ( +

No tienes eventos próximos

+ )} +
+ {/* Recent Payments */} +
+

Pagos Recientes

+ {payments.length > 0 ? ( +
+ {payments.slice(0, 3).map((payment, index) => ( +
+
+
+

+ Pago #{payment.id?.slice(0, 8) || `P${index + 1}`} +

+

+ {payment.description || `${payment.services?.length || 0} servicios`} +

+
+ + ${payment.total || payment.totalCost || 0} + +
+
+ ))} +
+ ) : ( +

No hay historial de pagos

+ )} +
+
+ )} - {/* Action Buttons */} -
- - - +
+ )} +
+ )} - className="bg-red-500 text-white px-4 py-2 rounded-full font-medium hover:bg-red-600" - > - Logout - + {activeTab === "payments" && ( +
+

Historial de Pagos

+ {payments.length > 0 ? ( +
+ {payments.map((payment, index) => ( +
+
+
+

+ Pago #{payment.id?.slice(0, 8) || `P${index + 1}`} +

+

+ {payment.description || "Pago de servicios"} +

+

+ {payment.date && new Date(payment.date).toLocaleDateString('es-ES')} +

+
+ + ${payment.total || payment.totalCost || 0} + +
+
+ Estado: + {payment.status === "completed" ? "Completado" : "Pendiente"} + +
+
+ ))} +
+ ) : ( +

No hay historial de pagos

+ )} +
+ )} + + {activeTab === "settings" && ( +
+

Configuración

+
+
+

Información Personal

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+ )}
); -} +} \ No newline at end of file diff --git a/src/pages/ReservationForm.jsx b/src/pages/ReservationForm.jsx new file mode 100644 index 0000000..655d7e3 --- /dev/null +++ b/src/pages/ReservationForm.jsx @@ -0,0 +1,767 @@ +import { useState, useEffect, useCallback } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import api from "../api/axios"; +import useSnackbar from "../hooks/useSnackbar" + + +function ServiceModal({ isOpen, onClose, onConfirm, serviceType, serviceName, defaultValue }) { + const [value, setValue] = useState(defaultValue); + + useEffect(() => { + setValue(defaultValue); + }, [defaultValue, isOpen]); + + if (!isOpen) return null; + + const getModalConfig = () => { + switch(serviceType) { + case "entretenimiento": + return { + title: "¿Cuántas horas?", + label: "Número de horas", + placeholder: "Ej: 3", + icon: "🎵", + color: "purple" + }; + case "adicionales": + return { + title: "¿Qué cantidad?", + label: "Cantidad", + placeholder: "Ej: 1", + icon: "📦", + color: "yellow" + }; + case "catering": + return { + title: "¿Número de platos?", + label: "Cantidad de platos", + placeholder: "Ej: 10", + icon: "🍽️", + color: "green" + }; + default: + return { + title: "Cantidad", + label: "Cantidad", + placeholder: "1", + icon: "✨", + color: "blue" + }; + } + }; + + const config = getModalConfig(); + const colorClasses = { + purple: "bg-purple-500 hover:bg-purple-600 focus:ring-purple-500", + yellow: "bg-yellow-500 hover:bg-yellow-600 focus:ring-yellow-500", + green: "bg-green-500 hover:bg-green-600 focus:ring-green-500", + blue: "bg-blue-500 hover:bg-blue-600 focus:ring-blue-500" + }; + + return ( +
+
+ {/* Header */} +
+
{config.icon}
+

+ {config.title} +

+

+ Para: {serviceName} +

+
+ + {/* Input */} +
+ + setValue(e.target.value)} + placeholder={config.placeholder} + min="1" + className="w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all" + autoFocus + /> +
+ + {/* Buttons */} +
+ + +
+
+
+ ); +} + +export default function ReservationForm() { + const location = useLocation(); + const navigate = useNavigate(); + const { serviceId } = useParams(); + + const { showSnackbar, SnackbarComponent } = useSnackbar(); + + const [modalState, setModalState] = useState({ + isOpen: false, + serviceType: null, + service: null, + category: null + }); + + const [formData, setFormData] = useState({ + eventType: "", + eventDate: "", + eventTime: "", + guests: "", + comments: "", + establishmentId: "", + selectedServices: serviceId && location.state?.category + ? [{ + id: serviceId, + category: location.state?.category, + name: location.state?.serviceName || "Servicio", + cost: location.state?.serviceCost || 0 + }] + : [], + entertainmentServices: [], + decorationServices: [], + cateringServices: [], + additionalServices: [], + }); + + const [establishments, setEstablishments] = useState([]); + const [eventTypes, setEventTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [totalCost, setTotalCost] = useState(0); + + const getServiceCost = (service, category) => { + switch(category) { + case "entretenimiento": + return service.hourlyRate; + case "decoracion": + return service.cost; + case "catering": + return service.costDish; + case "adicionales": + return service.cost; + default: + return 0; + } + }; + + useEffect(() => { + const loadFormData = async () => { + try { + setIsLoading(true); + + const [establishmentsRes, eventTypesRes] = await Promise.all([ + api.get("/establishments"), + api.get("/events") + ]); + + setEstablishments(establishmentsRes.data || []); + setEventTypes(eventTypesRes.data || []); + + const [entertainmentRes, decorationRes, cateringRes, additionalRes] = await Promise.all([ + api.get("/entertainment"), + api.get("/decoration"), + api.get("/catering"), + api.get("/additional"), + ]); + + setFormData(prev => ({ + ...prev, + entertainmentServices: entertainmentRes.data || [], + decorationServices: decorationRes.data || [], + cateringServices: cateringRes.data || [], + additionalServices: additionalRes.data || [], + })); + + } catch (error) { + console.error("Error cargando datos:", error); + toast.error("Error al cargar datos del formulario"); + } finally { + setIsLoading(false); + } + }; + + loadFormData(); + }, [serviceId]); + + const calculateTotal = useCallback(() => { + let total = 0; + + formData.selectedServices.forEach(service => { + if (service.totalCost) { + total += service.totalCost; + } else { + const cost = service.cost || getServiceCost(service, service.category) || 0; + total += Number(cost) || 0; + } + }); + + if (formData.establishmentId && establishments.length > 0) { + const selectedEstablishment = establishments.find(e => e.id === formData.establishmentId); + if (selectedEstablishment?.cost) { + total += Number(selectedEstablishment.cost) || 0; + } + } + + setTotalCost(total); + }, [formData.selectedServices, formData.establishmentId, establishments]); + + useEffect(() => { + calculateTotal(); + }, [calculateTotal]); + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + + const openServiceModal = (service, category) => { + setModalState({ + isOpen: true, + serviceType: category, + service: service, + category: category + }); + }; + + + const handleModalConfirm = (quantity) => { + const { service, category } = modalState; + + let serviceDetails = { + ...service, + category, + name: service.name || service.theme || service.menuType + }; + + if (category === "entretenimiento") { + serviceDetails.hours = quantity; + serviceDetails.totalCost = quantity * (service.hourlyRate || 0); + } + + if (category === "adicionales") { + serviceDetails.quantity = quantity; + serviceDetails.totalCost = quantity * (service.cost || 0); + } + + if (category === "catering") { + serviceDetails.numberDish = quantity; + serviceDetails.menuType = service.menuType || "GENERAL"; + serviceDetails.totalCost = quantity * (service.costDish || 0); + } + + if (category === "decoracion") { + serviceDetails.totalCost = service.cost || 0; + } + + setFormData(prev => ({ + ...prev, + selectedServices: [...prev.selectedServices, serviceDetails] + })); + + // Cerrar modal + setModalState({ isOpen: false, serviceType: null, service: null, category: null }); + }; + + + const addService = (service, category) => { + if (category === "decoracion") { + let serviceDetails = { + ...service, + category, + name: service.theme || "Paquete Decoración", + totalCost: service.cost || 0 + }; + + setFormData(prev => ({ + ...prev, + selectedServices: [...prev.selectedServices, serviceDetails] + })); + return; + } + + openServiceModal(service, category); + }; + + const removeService = (index) => { + setFormData(prev => ({ + ...prev, + selectedServices: prev.selectedServices.filter((_, i) => i !== index) + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!formData.establishmentId) { + showSnackbar("Debes seleccionar un establecimiento"); + return; + } + + if (!formData.eventDate) { + showSnackbar("Debes seleccionar una fecha para el evento", "error"); + return; + } + + if (!formData.eventType) { + showSnackbar("Selecciona el tipo de evento (Boda, Cumpleaños, etc.)", "error"); + return; + } + + const today = new Date(); + const selectedDate = new Date(formData.eventDate); + if (selectedDate < today) { + showSnackbar("La fecha del evento no puede ser en el pasado", "error"); + return; + } + + try { + setIsLoading(true); + + const servicesPayload = { + entertainment: [], + decoration: {}, + catering: [], + additionalServices: [] + }; + + formData.selectedServices.forEach(service => { + switch(service.category) { + case "entretenimiento": + servicesPayload.entertainment.push({ + id: service.id, + hours: service.hours || 2 + }); + break; + + case "decoracion": + servicesPayload.decoration = { + id: service.id + }; + break; + + case "catering": + servicesPayload.catering.push({ + id: service.id, + menuType: service.menuType || "GENERAL", + numberDish: service.numberDish || formData.guests || 10 + }); + break; + + case "adicionales": + servicesPayload.additionalServices.push({ + id: service.id, + quantity: service.quantity || 1 + }); + break; + } + }); + + const reservationPayload = { + guestNumber: parseInt(formData.guests) || 0, + dates: [formData.eventDate], + eventId: formData.eventType, + establishmentId: formData.establishmentId, + comments: formData.comments, + services: servicesPayload + }; + + console.log("Payload a enviar:", JSON.stringify(reservationPayload, null, 2)); + + const response = await api.post("/reserve", reservationPayload); + + showSnackbar( + `¡Reserva creada exitosamente! ID: ${response.data.id?.slice(0, 8)}...`, + "success" + ); + navigate("/profile", { state: { reservationId: response.data.id } }); + + } catch (error) { + showSnackbar( + error.response?.data?.message || + error.response?.data?.error || + "Error al crear reserva", + "error" + ); + console.error("Error creating reserva:", error); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + + const getDefaultModalValue = () => { + if (modalState.serviceType === "catering") { + return formData.guests || "10"; + } + if (modalState.serviceType === "entretenimiento") { + return "3"; + } + return "1"; + }; + + return ( +
+ + setModalState({ isOpen: false, serviceType: null, service: null, category: null })} + onConfirm={handleModalConfirm} + serviceType={modalState.serviceType} + serviceName={modalState.service?.name || modalState.service?.theme || modalState.service?.menuType || "Servicio"} + defaultValue={getDefaultModalValue()} + /> + +
+
+ + +

+ Completa tu Reserva +

+

+ Llena los detalles de tu evento y personaliza con servicios adicionales +

+
+ +
+
+
+

Información del Evento

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +